@skillsmith/core 0.4.10 → 0.4.12
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/dist/.tsbuildinfo +1 -1
- package/dist/src/analysis/__tests__/incremental.test.d.ts +1 -1
- package/dist/src/analysis/__tests__/incremental.test.js +1 -1
- package/dist/src/analysis/__tests__/integration.test.d.ts +1 -1
- package/dist/src/analysis/__tests__/integration.test.js +1 -1
- package/dist/src/analysis/__tests__/performance.test.d.ts +1 -1
- package/dist/src/analysis/__tests__/performance.test.js +1 -1
- package/dist/src/analysis/adapters/__tests__/python.test.d.ts +1 -1
- package/dist/src/analysis/adapters/__tests__/python.test.js +1 -1
- package/dist/src/analysis/adapters/__tests__/typescript.test.d.ts +1 -1
- package/dist/src/analysis/adapters/__tests__/typescript.test.js +1 -1
- package/dist/src/analysis/adapters/base.d.ts +1 -1
- package/dist/src/analysis/adapters/base.js +1 -1
- package/dist/src/analysis/adapters/factory.d.ts +1 -1
- package/dist/src/analysis/adapters/factory.js +1 -1
- package/dist/src/analysis/adapters/go.d.ts +1 -1
- package/dist/src/analysis/adapters/go.js +1 -1
- package/dist/src/analysis/adapters/index.d.ts +1 -1
- package/dist/src/analysis/adapters/index.js +1 -1
- package/dist/src/analysis/adapters/java-parsers.d.ts +1 -1
- package/dist/src/analysis/adapters/java-parsers.js +1 -1
- package/dist/src/analysis/adapters/java.d.ts +1 -1
- package/dist/src/analysis/adapters/java.js +1 -1
- package/dist/src/analysis/adapters/python-frameworks.d.ts +1 -1
- package/dist/src/analysis/adapters/python-frameworks.js +1 -1
- package/dist/src/analysis/adapters/python.d.ts +1 -1
- package/dist/src/analysis/adapters/python.js +1 -1
- package/dist/src/analysis/adapters/rust-parsers.d.ts +1 -1
- package/dist/src/analysis/adapters/rust-parsers.js +1 -1
- package/dist/src/analysis/adapters/rust.d.ts +1 -1
- package/dist/src/analysis/adapters/rust.js +1 -1
- package/dist/src/analysis/adapters/typescript.d.ts +1 -1
- package/dist/src/analysis/adapters/typescript.js +1 -1
- package/dist/src/analysis/aggregator.d.ts +1 -1
- package/dist/src/analysis/aggregator.js +1 -1
- package/dist/src/analysis/cache.d.ts +1 -1
- package/dist/src/analysis/cache.js +1 -1
- package/dist/src/analysis/file-streamer.d.ts +1 -1
- package/dist/src/analysis/file-streamer.js +1 -1
- package/dist/src/analysis/incremental-parser.d.ts +1 -1
- package/dist/src/analysis/incremental-parser.js +1 -1
- package/dist/src/analysis/incremental.d.ts +1 -1
- package/dist/src/analysis/incremental.js +1 -1
- package/dist/src/analysis/index.d.ts +1 -1
- package/dist/src/analysis/index.js +1 -1
- package/dist/src/analysis/language-detector.d.ts +1 -1
- package/dist/src/analysis/language-detector.js +1 -1
- package/dist/src/analysis/memory-monitor.d.ts +1 -1
- package/dist/src/analysis/memory-monitor.js +1 -1
- package/dist/src/analysis/metrics.d.ts +1 -1
- package/dist/src/analysis/metrics.js +1 -1
- package/dist/src/analysis/router.d.ts +1 -1
- package/dist/src/analysis/router.js +1 -1
- package/dist/src/analysis/tree-cache.d.ts +1 -1
- package/dist/src/analysis/tree-cache.js +1 -1
- package/dist/src/analysis/tree-sitter/manager.d.ts +1 -1
- package/dist/src/analysis/tree-sitter/manager.js +1 -1
- package/dist/src/analysis/types.d.ts +1 -1
- package/dist/src/analysis/types.js +1 -1
- package/dist/src/analysis/worker-pool.d.ts +1 -1
- package/dist/src/analysis/worker-pool.js +1 -1
- package/dist/src/analysis/worker-types.d.ts +1 -1
- package/dist/src/analysis/worker-types.js +1 -1
- package/dist/src/analysis/worker-utils.d.ts +1 -1
- package/dist/src/analysis/worker-utils.js +1 -1
- package/dist/src/config/index.d.ts +49 -1
- package/dist/src/config/index.d.ts.map +1 -1
- package/dist/src/config/index.js +166 -3
- package/dist/src/config/index.js.map +1 -1
- package/dist/src/config/index.test.d.ts +11 -0
- package/dist/src/config/index.test.d.ts.map +1 -0
- package/dist/src/config/index.test.js +288 -0
- package/dist/src/config/index.test.js.map +1 -0
- package/dist/src/db/quarantine-approvals-schema.d.ts +37 -0
- package/dist/src/db/quarantine-approvals-schema.d.ts.map +1 -0
- package/dist/src/db/quarantine-approvals-schema.js +71 -0
- package/dist/src/db/quarantine-approvals-schema.js.map +1 -0
- package/dist/src/embeddings/embedding-utils.d.ts +3 -3
- package/dist/src/embeddings/embedding-utils.d.ts.map +1 -1
- package/dist/src/embeddings/embedding-utils.js +3 -3
- package/dist/src/embeddings/embedding-utils.js.map +1 -1
- package/dist/src/embeddings/index.d.ts +1 -1
- package/dist/src/embeddings/index.js +3 -3
- package/dist/src/embeddings/index.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/repositories/quarantine/ApprovalRepository.d.ts +148 -0
- package/dist/src/repositories/quarantine/ApprovalRepository.d.ts.map +1 -0
- package/dist/src/repositories/quarantine/ApprovalRepository.js +212 -0
- package/dist/src/repositories/quarantine/ApprovalRepository.js.map +1 -0
- package/dist/src/repositories/quarantine/index.d.ts +2 -0
- package/dist/src/repositories/quarantine/index.d.ts.map +1 -1
- package/dist/src/repositories/quarantine/index.js +1 -0
- package/dist/src/repositories/quarantine/index.js.map +1 -1
- package/dist/src/security/audit-types.d.ts +1 -1
- package/dist/src/security/audit-types.d.ts.map +1 -1
- package/dist/src/security/audit-types.js.map +1 -1
- package/dist/src/security/scanner/SecurityScanner.formatters.js +1 -1
- package/dist/src/security/scanner/SecurityScanner.formatters.js.map +1 -1
- package/dist/src/services/quarantine/QuarantineService.d.ts +17 -9
- package/dist/src/services/quarantine/QuarantineService.d.ts.map +1 -1
- package/dist/src/services/quarantine/QuarantineService.js +114 -56
- package/dist/src/services/quarantine/QuarantineService.js.map +1 -1
- package/dist/tests/RawUrlSourceAdapter.security.test.js +2 -1
- package/dist/tests/RawUrlSourceAdapter.security.test.js.map +1 -1
- package/dist/tests/SecurityScanner.test.js +2 -2
- package/dist/tests/SecurityScanner.test.js.map +1 -1
- package/dist/tests/adapters-factory.test.d.ts +1 -1
- package/dist/tests/adapters-factory.test.js +1 -1
- package/dist/tests/integration/QuarantineService.test.js +49 -1
- package/dist/tests/integration/QuarantineService.test.js.map +1 -1
- package/dist/tests/integration/neural/e2e-learning.test.d.ts +1 -1
- package/dist/tests/integration/neural/e2e-learning.test.js +1 -1
- package/dist/tests/integration/neural/personalization.test.d.ts +1 -1
- package/dist/tests/integration/neural/personalization.test.js +1 -1
- package/dist/tests/integration/neural/preference-learner.test.d.ts +1 -1
- package/dist/tests/integration/neural/preference-learner.test.js +1 -1
- package/dist/tests/integration/neural/privacy.test.d.ts +1 -1
- package/dist/tests/integration/neural/privacy.test.js +1 -1
- package/dist/tests/integration/neural/signal-collection.test.d.ts +1 -1
- package/dist/tests/integration/neural/signal-collection.test.js +1 -1
- package/dist/tests/language-detector.test.d.ts +1 -1
- package/dist/tests/language-detector.test.js +1 -1
- package/dist/tests/unit/approval-repository.test.d.ts +9 -0
- package/dist/tests/unit/approval-repository.test.d.ts.map +1 -0
- package/dist/tests/unit/approval-repository.test.js +509 -0
- package/dist/tests/unit/approval-repository.test.js.map +1 -0
- package/package.json +2 -2
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Type definitions for the worker thread pool.
|
|
5
5
|
* Extracted from worker-pool.ts for better modularity.
|
|
6
6
|
*
|
|
7
|
-
* @see docs/architecture/multi-language-analysis.md
|
|
7
|
+
* @see docs/internal/architecture/multi-language-analysis.md
|
|
8
8
|
* @module analysis/worker-types
|
|
9
9
|
*/
|
|
10
10
|
export {};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Utility functions for the worker thread pool.
|
|
5
5
|
* Extracted from worker-pool.ts for better modularity.
|
|
6
6
|
*
|
|
7
|
-
* @see docs/architecture/multi-language-analysis.md
|
|
7
|
+
* @see docs/internal/architecture/multi-language-analysis.md
|
|
8
8
|
* @module analysis/worker-utils
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Utility functions for the worker thread pool.
|
|
5
5
|
* Extracted from worker-pool.ts for better modularity.
|
|
6
6
|
*
|
|
7
|
-
* @see docs/architecture/multi-language-analysis.md
|
|
7
|
+
* @see docs/internal/architecture/multi-language-analysis.md
|
|
8
8
|
* @module analysis/worker-utils
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* @module @skillsmith/core/config
|
|
4
4
|
*
|
|
5
5
|
* SMI-1851: Shared Config Module
|
|
6
|
+
* SMI-2714: CLI Login Device Flow - credential storage
|
|
6
7
|
*
|
|
7
8
|
* Provides cross-platform configuration loading from:
|
|
8
9
|
* - Environment variables (highest precedence)
|
|
9
10
|
* - ~/.skillsmith/config.json
|
|
11
|
+
* - OS keyring (via @isaacs/keytar, optional)
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* ```typescript
|
|
@@ -20,6 +22,12 @@
|
|
|
20
22
|
*
|
|
21
23
|
* // Save config (creates file with 0600 permissions)
|
|
22
24
|
* saveConfig({ apiKey: 'sk_live_...' })
|
|
25
|
+
*
|
|
26
|
+
* // Store API key (tries keyring first, falls back to config file)
|
|
27
|
+
* await storeApiKey('sk_live_...')
|
|
28
|
+
*
|
|
29
|
+
* // Get auth status
|
|
30
|
+
* const status = await getAuthStatus()
|
|
23
31
|
* ```
|
|
24
32
|
*/
|
|
25
33
|
/**
|
|
@@ -109,10 +117,50 @@ export declare function isDebugEnabled(): boolean;
|
|
|
109
117
|
*/
|
|
110
118
|
export declare function isTelemetryEnabled(): boolean;
|
|
111
119
|
/**
|
|
112
|
-
* Validate API key format
|
|
120
|
+
* Validate API key format.
|
|
121
|
+
*
|
|
122
|
+
* Expected format: sk_live_ followed by 32-128 alphanumeric/dash/underscore chars.
|
|
123
|
+
* The 200-char pre-check is a ReDoS guard per security standards.
|
|
113
124
|
*
|
|
114
125
|
* @param key - API key to validate
|
|
115
126
|
* @returns true if key has valid format (sk_live_...)
|
|
116
127
|
*/
|
|
117
128
|
export declare function isValidApiKeyFormat(key: string): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Store an API key securely.
|
|
131
|
+
*
|
|
132
|
+
* Attempts to use the OS keyring first (via @isaacs/keytar).
|
|
133
|
+
* Falls back to saving in ~/.skillsmith/config.json when keyring is unavailable.
|
|
134
|
+
*
|
|
135
|
+
* @param apiKey - The API key to store (must pass isValidApiKeyFormat)
|
|
136
|
+
*/
|
|
137
|
+
export declare function storeApiKey(apiKey: string): Promise<void>;
|
|
138
|
+
/**
|
|
139
|
+
* Clear the stored API key from all storage locations.
|
|
140
|
+
*
|
|
141
|
+
* Attempts to delete from the OS keyring AND removes apiKey from the config file.
|
|
142
|
+
* Returns explicit success/failure info so callers can report partial failures.
|
|
143
|
+
*
|
|
144
|
+
* @returns Result indicating which storage locations were cleared and any errors
|
|
145
|
+
*/
|
|
146
|
+
export declare function clearApiKey(): Promise<{
|
|
147
|
+
success: boolean;
|
|
148
|
+
source: string;
|
|
149
|
+
error?: string;
|
|
150
|
+
}>;
|
|
151
|
+
/**
|
|
152
|
+
* Get current authentication status.
|
|
153
|
+
*
|
|
154
|
+
* Checks in precedence order:
|
|
155
|
+
* 1. SKILLSMITH_API_KEY environment variable
|
|
156
|
+
* 2. OS keyring (via @isaacs/keytar)
|
|
157
|
+
* 3. ~/.skillsmith/config.json apiKey field
|
|
158
|
+
*
|
|
159
|
+
* @returns Authentication status with masked key prefix and storage source
|
|
160
|
+
*/
|
|
161
|
+
export declare function getAuthStatus(): Promise<{
|
|
162
|
+
authenticated: boolean;
|
|
163
|
+
keyPrefix: string | null;
|
|
164
|
+
source: 'keyring' | 'config' | 'env' | 'none';
|
|
165
|
+
}>;
|
|
118
166
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAMH;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,4BAA4B;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,yBAAyB;IACzB,SAAS,CAAC,EAAE;QACV,gDAAgD;QAChD,OAAO,CAAC,EAAE,OAAO,CAAA;KAClB,CAAA;IACD,oBAAoB;IACpB,IAAI,CAAC,EAAE;QACL,6BAA6B;QAC7B,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,oCAAoC;QACpC,UAAU,CAAC,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAuDD;;;;;GAKG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,IAAI,CAKtC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,IAAI,gBAAgB,CAc7C;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,OAAO,CAAC,gBAAgB,CAAC,EACjC,OAAO,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAoB,GAC7C,IAAI,CAmCN;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,IAAI,MAAM,GAAG,SAAS,CAU9C;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,UAAU,SAA+B,GAAG,MAAM,CAQ/E;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAOxC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,OAAO,CAU5C;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGxD;AAMD;;;;;;;GAOG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAW/D;AAED;;;;;;;GAOG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC;IAC3C,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAC,CAgCD;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC;IAC7C,aAAa,EAAE,OAAO,CAAA;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAA;CAC9C,CAAC,CA2CD"}
|
package/dist/src/config/index.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* @module @skillsmith/core/config
|
|
4
4
|
*
|
|
5
5
|
* SMI-1851: Shared Config Module
|
|
6
|
+
* SMI-2714: CLI Login Device Flow - credential storage
|
|
6
7
|
*
|
|
7
8
|
* Provides cross-platform configuration loading from:
|
|
8
9
|
* - Environment variables (highest precedence)
|
|
9
10
|
* - ~/.skillsmith/config.json
|
|
11
|
+
* - OS keyring (via @isaacs/keytar, optional)
|
|
10
12
|
*
|
|
11
13
|
* @example
|
|
12
14
|
* ```typescript
|
|
@@ -20,6 +22,12 @@
|
|
|
20
22
|
*
|
|
21
23
|
* // Save config (creates file with 0600 permissions)
|
|
22
24
|
* saveConfig({ apiKey: 'sk_live_...' })
|
|
25
|
+
*
|
|
26
|
+
* // Store API key (tries keyring first, falls back to config file)
|
|
27
|
+
* await storeApiKey('sk_live_...')
|
|
28
|
+
*
|
|
29
|
+
* // Get auth status
|
|
30
|
+
* const status = await getAuthStatus()
|
|
23
31
|
* ```
|
|
24
32
|
*/
|
|
25
33
|
import { homedir } from 'os';
|
|
@@ -29,6 +37,34 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'f
|
|
|
29
37
|
const CONFIG_DIR = '.skillsmith';
|
|
30
38
|
/** Default config file name */
|
|
31
39
|
const CONFIG_FILE = 'config.json';
|
|
40
|
+
/** Lazy-loaded keytar module; null means unavailable */
|
|
41
|
+
let keytarModule = undefined;
|
|
42
|
+
/**
|
|
43
|
+
* Lazily import @isaacs/keytar and cache the result.
|
|
44
|
+
* Returns null if the module is unavailable (CI, Docker, unsupported platform).
|
|
45
|
+
*/
|
|
46
|
+
async function getKeytar() {
|
|
47
|
+
if (keytarModule !== undefined)
|
|
48
|
+
return keytarModule;
|
|
49
|
+
try {
|
|
50
|
+
// @ts-expect-error — @isaacs/keytar is an optional dependency with no type declarations
|
|
51
|
+
// in @skillsmith/core. We use a structural interface (KeytarLike) to type the
|
|
52
|
+
// result. The import is a standard dynamic import so Vitest can intercept it.
|
|
53
|
+
const mod = (await import('@isaacs/keytar'));
|
|
54
|
+
keytarModule = mod.default ?? mod;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
keytarModule = null;
|
|
58
|
+
}
|
|
59
|
+
return keytarModule;
|
|
60
|
+
}
|
|
61
|
+
/** Service name used in the OS keyring */
|
|
62
|
+
const KEYTAR_SERVICE = 'skillsmith-cli';
|
|
63
|
+
/** Account key used in the OS keyring */
|
|
64
|
+
const KEYTAR_ACCOUNT = 'api-key';
|
|
65
|
+
// ============================================================================
|
|
66
|
+
// Config file helpers
|
|
67
|
+
// ============================================================================
|
|
32
68
|
/**
|
|
33
69
|
* Get the config directory path
|
|
34
70
|
* Cross-platform: uses os.homedir()
|
|
@@ -89,7 +125,15 @@ export function saveConfig(config, options = { merge: true }) {
|
|
|
89
125
|
if (options.merge && existsSync(configPath)) {
|
|
90
126
|
existingConfig = loadConfig();
|
|
91
127
|
}
|
|
92
|
-
|
|
128
|
+
// Remove undefined values so they are omitted from JSON output
|
|
129
|
+
const updates = Object.fromEntries(Object.entries(config).filter(([, v]) => v !== undefined));
|
|
130
|
+
// Explicit undefined fields are deletions — remove them from existing config
|
|
131
|
+
const deletions = Object.keys(config).filter((k) => config[k] === undefined);
|
|
132
|
+
const cleaned = { ...existingConfig };
|
|
133
|
+
for (const key of deletions) {
|
|
134
|
+
delete cleaned[key];
|
|
135
|
+
}
|
|
136
|
+
const mergedConfig = { ...cleaned, ...updates };
|
|
93
137
|
const configJson = JSON.stringify(mergedConfig, null, 2);
|
|
94
138
|
writeFileSync(configPath, configJson, { encoding: 'utf-8', mode: 0o600 });
|
|
95
139
|
// Ensure permissions are set correctly (in case file existed)
|
|
@@ -161,12 +205,131 @@ export function isTelemetryEnabled() {
|
|
|
161
205
|
return config.telemetry?.enabled === true;
|
|
162
206
|
}
|
|
163
207
|
/**
|
|
164
|
-
* Validate API key format
|
|
208
|
+
* Validate API key format.
|
|
209
|
+
*
|
|
210
|
+
* Expected format: sk_live_ followed by 32-128 alphanumeric/dash/underscore chars.
|
|
211
|
+
* The 200-char pre-check is a ReDoS guard per security standards.
|
|
165
212
|
*
|
|
166
213
|
* @param key - API key to validate
|
|
167
214
|
* @returns true if key has valid format (sk_live_...)
|
|
168
215
|
*/
|
|
169
216
|
export function isValidApiKeyFormat(key) {
|
|
170
|
-
|
|
217
|
+
if (key.length > 200)
|
|
218
|
+
return false; // ReDoS guard
|
|
219
|
+
return /^sk_live_[A-Za-z0-9_-]{32,128}$/.test(key);
|
|
220
|
+
}
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Credential storage (SMI-2714)
|
|
223
|
+
// ============================================================================
|
|
224
|
+
/**
|
|
225
|
+
* Store an API key securely.
|
|
226
|
+
*
|
|
227
|
+
* Attempts to use the OS keyring first (via @isaacs/keytar).
|
|
228
|
+
* Falls back to saving in ~/.skillsmith/config.json when keyring is unavailable.
|
|
229
|
+
*
|
|
230
|
+
* @param apiKey - The API key to store (must pass isValidApiKeyFormat)
|
|
231
|
+
*/
|
|
232
|
+
export async function storeApiKey(apiKey) {
|
|
233
|
+
const keytar = await getKeytar();
|
|
234
|
+
if (keytar) {
|
|
235
|
+
try {
|
|
236
|
+
await keytar.setPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT, apiKey);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Keyring failed — fall through to config file
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
saveConfig({ apiKey });
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Clear the stored API key from all storage locations.
|
|
247
|
+
*
|
|
248
|
+
* Attempts to delete from the OS keyring AND removes apiKey from the config file.
|
|
249
|
+
* Returns explicit success/failure info so callers can report partial failures.
|
|
250
|
+
*
|
|
251
|
+
* @returns Result indicating which storage locations were cleared and any errors
|
|
252
|
+
*/
|
|
253
|
+
export async function clearApiKey() {
|
|
254
|
+
const keyringSources = [];
|
|
255
|
+
let keyringError;
|
|
256
|
+
const keytar = await getKeytar();
|
|
257
|
+
if (keytar) {
|
|
258
|
+
try {
|
|
259
|
+
const deleted = await keytar.deletePassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
260
|
+
if (deleted) {
|
|
261
|
+
keyringSources.push('keyring');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
keyringError = err instanceof Error ? err.message : String(err);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Always clear from config file — never leave a stale key there
|
|
269
|
+
saveConfig({ apiKey: undefined });
|
|
270
|
+
keyringSources.push('config file');
|
|
271
|
+
if (keyringError) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
source: keyringSources.join(' and '),
|
|
275
|
+
error: keyringError,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
success: true,
|
|
280
|
+
source: keyringSources.join(' and '),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Get current authentication status.
|
|
285
|
+
*
|
|
286
|
+
* Checks in precedence order:
|
|
287
|
+
* 1. SKILLSMITH_API_KEY environment variable
|
|
288
|
+
* 2. OS keyring (via @isaacs/keytar)
|
|
289
|
+
* 3. ~/.skillsmith/config.json apiKey field
|
|
290
|
+
*
|
|
291
|
+
* @returns Authentication status with masked key prefix and storage source
|
|
292
|
+
*/
|
|
293
|
+
export async function getAuthStatus() {
|
|
294
|
+
// 1. Environment variable (highest precedence)
|
|
295
|
+
const envKey = process.env.SKILLSMITH_API_KEY;
|
|
296
|
+
if (envKey && isValidApiKeyFormat(envKey)) {
|
|
297
|
+
return {
|
|
298
|
+
authenticated: true,
|
|
299
|
+
keyPrefix: envKey.substring(0, 12),
|
|
300
|
+
source: 'env',
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
// 2. OS keyring
|
|
304
|
+
const keytar = await getKeytar();
|
|
305
|
+
if (keytar) {
|
|
306
|
+
try {
|
|
307
|
+
const keyrKey = await keytar.getPassword(KEYTAR_SERVICE, KEYTAR_ACCOUNT);
|
|
308
|
+
if (keyrKey && isValidApiKeyFormat(keyrKey)) {
|
|
309
|
+
return {
|
|
310
|
+
authenticated: true,
|
|
311
|
+
keyPrefix: keyrKey.substring(0, 12),
|
|
312
|
+
source: 'keyring',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch {
|
|
317
|
+
// Keyring unavailable or locked — fall through to config file
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
// 3. Config file
|
|
321
|
+
const config = loadConfig();
|
|
322
|
+
if (config.apiKey && isValidApiKeyFormat(config.apiKey)) {
|
|
323
|
+
return {
|
|
324
|
+
authenticated: true,
|
|
325
|
+
keyPrefix: config.apiKey.substring(0, 12),
|
|
326
|
+
source: 'config',
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
return {
|
|
330
|
+
authenticated: false,
|
|
331
|
+
keyPrefix: null,
|
|
332
|
+
source: 'none',
|
|
333
|
+
};
|
|
171
334
|
}
|
|
172
335
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-2714: Config module credential storage tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for storeApiKey, clearApiKey, getAuthStatus, and the updated
|
|
5
|
+
* isValidApiKeyFormat with ReDoS guard.
|
|
6
|
+
*
|
|
7
|
+
* vi.mock factory is hoisted before module load — never use vi.doMock for the
|
|
8
|
+
* primary keytar mock (it would arrive too late).
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=index.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../src/config/index.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SMI-2714: Config module credential storage tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for storeApiKey, clearApiKey, getAuthStatus, and the updated
|
|
5
|
+
* isValidApiKeyFormat with ReDoS guard.
|
|
6
|
+
*
|
|
7
|
+
* vi.mock factory is hoisted before module load — never use vi.doMock for the
|
|
8
|
+
* primary keytar mock (it would arrive too late).
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import { existsSync, readFileSync } from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
// MUST be declared before any imports of the module under test so Vitest hoists
|
|
15
|
+
// this factory and the mock is in place when config/index.ts is first loaded.
|
|
16
|
+
vi.mock('@isaacs/keytar', () => ({
|
|
17
|
+
default: {
|
|
18
|
+
setPassword: vi.fn(),
|
|
19
|
+
getPassword: vi.fn(),
|
|
20
|
+
deletePassword: vi.fn(),
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
// Retrieve the mocked keytar module using Vitest's module registry.
|
|
24
|
+
// We can't use a static import (no types in core) nor a standard dynamic import
|
|
25
|
+
// (same reason), so we resolve the mock via vi.importMock after the mock factory
|
|
26
|
+
// above has run.
|
|
27
|
+
const keytarMod = await vi.importMock('@isaacs/keytar');
|
|
28
|
+
const mockKeytar = keytarMod.default;
|
|
29
|
+
// Import module under test AFTER mocks are declared
|
|
30
|
+
import { storeApiKey, clearApiKey, getAuthStatus, isValidApiKeyFormat, saveConfig, loadConfig, } from './index.js';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
const VALID_KEY = 'sk_live_' + 'a'.repeat(32);
|
|
35
|
+
const VALID_KEY_LONG = 'sk_live_' + 'b'.repeat(128);
|
|
36
|
+
/** Unique temp config dir per test run to avoid cross-test pollution */
|
|
37
|
+
function makeTempConfigDir() {
|
|
38
|
+
return path.join(os.tmpdir(), `skillsmith-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
39
|
+
}
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// isValidApiKeyFormat
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
describe('isValidApiKeyFormat', () => {
|
|
44
|
+
it('accepts a key with exactly 32 suffix chars', () => {
|
|
45
|
+
expect(isValidApiKeyFormat(VALID_KEY)).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
it('accepts a key with exactly 128 suffix chars', () => {
|
|
48
|
+
expect(isValidApiKeyFormat(VALID_KEY_LONG)).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
it('rejects a key with 129 suffix chars (over cap)', () => {
|
|
51
|
+
const tooLong = 'sk_live_' + 'c'.repeat(129);
|
|
52
|
+
expect(isValidApiKeyFormat(tooLong)).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
it('rejects a key shorter than 32 suffix chars', () => {
|
|
55
|
+
const tooShort = 'sk_live_' + 'a'.repeat(31);
|
|
56
|
+
expect(isValidApiKeyFormat(tooShort)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
it('rejects a key with wrong prefix', () => {
|
|
59
|
+
expect(isValidApiKeyFormat('sk_test_' + 'a'.repeat(32))).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
it('rejects an empty string', () => {
|
|
62
|
+
expect(isValidApiKeyFormat('')).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
it('rejects a string over 200 chars (ReDoS pre-check)', () => {
|
|
65
|
+
const huge = 'sk_live_' + 'a'.repeat(193); // total 201 chars
|
|
66
|
+
expect(isValidApiKeyFormat(huge)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
it('rejects exactly 200 chars if suffix > 128', () => {
|
|
69
|
+
// 200 total — 8 prefix = 192 suffix chars, which exceeds the 128 cap
|
|
70
|
+
const edge = 'sk_live_' + 'a'.repeat(192);
|
|
71
|
+
expect(isValidApiKeyFormat(edge)).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// storeApiKey
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
describe('storeApiKey', () => {
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.clearAllMocks();
|
|
80
|
+
});
|
|
81
|
+
it('stores in keyring when keytar is available', async () => {
|
|
82
|
+
mockKeytar.setPassword.mockResolvedValue(undefined);
|
|
83
|
+
await storeApiKey(VALID_KEY);
|
|
84
|
+
expect(mockKeytar.setPassword).toHaveBeenCalledWith('skillsmith-cli', 'api-key', VALID_KEY);
|
|
85
|
+
});
|
|
86
|
+
it('falls back to config file when keytar.setPassword throws', async () => {
|
|
87
|
+
mockKeytar.setPassword.mockRejectedValue(new Error('keyring locked'));
|
|
88
|
+
const originalHome = process.env.HOME;
|
|
89
|
+
const tmpDir = makeTempConfigDir();
|
|
90
|
+
process.env.HOME = tmpDir;
|
|
91
|
+
try {
|
|
92
|
+
await storeApiKey(VALID_KEY);
|
|
93
|
+
// If config dir was created, the key should be in the file
|
|
94
|
+
const configPath = path.join(tmpDir, '.skillsmith', 'config.json');
|
|
95
|
+
if (existsSync(configPath)) {
|
|
96
|
+
const saved = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
97
|
+
expect(saved.apiKey).toBe(VALID_KEY);
|
|
98
|
+
}
|
|
99
|
+
// Test passed if no exception was thrown
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
process.env.HOME = originalHome;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// clearApiKey
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
describe('clearApiKey', () => {
|
|
110
|
+
beforeEach(() => {
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
});
|
|
113
|
+
it('clears from keyring and reports success', async () => {
|
|
114
|
+
mockKeytar.deletePassword.mockResolvedValue(true);
|
|
115
|
+
const result = await clearApiKey();
|
|
116
|
+
expect(mockKeytar.deletePassword).toHaveBeenCalledWith('skillsmith-cli', 'api-key');
|
|
117
|
+
expect(result.success).toBe(true);
|
|
118
|
+
expect(result.source).toContain('keyring');
|
|
119
|
+
});
|
|
120
|
+
it('always clears apiKey from config file (undefined, not empty string)', async () => {
|
|
121
|
+
mockKeytar.deletePassword.mockResolvedValue(false);
|
|
122
|
+
// Pre-populate a config with an API key
|
|
123
|
+
const originalHome = process.env.HOME;
|
|
124
|
+
const tmpDir = makeTempConfigDir();
|
|
125
|
+
process.env.HOME = tmpDir;
|
|
126
|
+
try {
|
|
127
|
+
saveConfig({ apiKey: VALID_KEY });
|
|
128
|
+
await clearApiKey();
|
|
129
|
+
const config = loadConfig();
|
|
130
|
+
// apiKey must be absent or undefined — NEVER an empty string
|
|
131
|
+
expect(config.apiKey).toBeUndefined();
|
|
132
|
+
// Verify the raw JSON does not contain an empty-string apiKey
|
|
133
|
+
const configPath = path.join(tmpDir, '.skillsmith', 'config.json');
|
|
134
|
+
if (existsSync(configPath)) {
|
|
135
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
136
|
+
expect(raw.apiKey).toBeUndefined();
|
|
137
|
+
expect('apiKey' in raw ? raw.apiKey : 'NOT_PRESENT').not.toBe('');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
finally {
|
|
141
|
+
process.env.HOME = originalHome;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
it('reports partial failure when keyring throws but config still cleared', async () => {
|
|
145
|
+
mockKeytar.deletePassword.mockRejectedValue(new Error('access denied'));
|
|
146
|
+
const originalHome = process.env.HOME;
|
|
147
|
+
const tmpDir = makeTempConfigDir();
|
|
148
|
+
process.env.HOME = tmpDir;
|
|
149
|
+
try {
|
|
150
|
+
saveConfig({ apiKey: VALID_KEY });
|
|
151
|
+
const result = await clearApiKey();
|
|
152
|
+
// Config file should still be cleared
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
expect(config.apiKey).toBeUndefined();
|
|
155
|
+
// Result should reflect the keyring error
|
|
156
|
+
expect(result.success).toBe(false);
|
|
157
|
+
expect(result.error).toBeDefined();
|
|
158
|
+
expect(result.error).toContain('access denied');
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
process.env.HOME = originalHome;
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
it('succeeds when no key is stored in keyring', async () => {
|
|
165
|
+
mockKeytar.deletePassword.mockResolvedValue(false);
|
|
166
|
+
const result = await clearApiKey();
|
|
167
|
+
expect(result.success).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// getAuthStatus
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
describe('getAuthStatus', () => {
|
|
174
|
+
let originalHome;
|
|
175
|
+
let originalApiKey;
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
vi.clearAllMocks();
|
|
178
|
+
originalHome = process.env['HOME'];
|
|
179
|
+
originalApiKey = process.env['SKILLSMITH_API_KEY'];
|
|
180
|
+
// Start each test with no API key set
|
|
181
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
182
|
+
});
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
// Restore HOME
|
|
185
|
+
if (originalHome !== undefined) {
|
|
186
|
+
process.env['HOME'] = originalHome;
|
|
187
|
+
}
|
|
188
|
+
// Restore API key (delete if it wasn't set before)
|
|
189
|
+
if (originalApiKey !== undefined) {
|
|
190
|
+
process.env['SKILLSMITH_API_KEY'] = originalApiKey;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
describe('env source', () => {
|
|
197
|
+
it('returns env source when SKILLSMITH_API_KEY is set and valid', async () => {
|
|
198
|
+
process.env['SKILLSMITH_API_KEY'] = VALID_KEY;
|
|
199
|
+
const status = await getAuthStatus();
|
|
200
|
+
expect(status.authenticated).toBe(true);
|
|
201
|
+
expect(status.source).toBe('env');
|
|
202
|
+
expect(status.keyPrefix).toBe(VALID_KEY.substring(0, 12));
|
|
203
|
+
});
|
|
204
|
+
it('does not return env source for an invalid env key format', async () => {
|
|
205
|
+
process.env['SKILLSMITH_API_KEY'] = 'not-a-valid-key';
|
|
206
|
+
mockKeytar.getPassword.mockResolvedValue(null);
|
|
207
|
+
const tmpDir = makeTempConfigDir();
|
|
208
|
+
process.env.HOME = tmpDir;
|
|
209
|
+
const status = await getAuthStatus();
|
|
210
|
+
expect(status.source).not.toBe('env');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('keyring source', () => {
|
|
214
|
+
it('returns keyring source when keytar has a valid key and env is not set', async () => {
|
|
215
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
216
|
+
mockKeytar.getPassword.mockResolvedValue(VALID_KEY);
|
|
217
|
+
const tmpDir = makeTempConfigDir();
|
|
218
|
+
process.env.HOME = tmpDir;
|
|
219
|
+
const status = await getAuthStatus();
|
|
220
|
+
expect(status.authenticated).toBe(true);
|
|
221
|
+
expect(status.source).toBe('keyring');
|
|
222
|
+
expect(status.keyPrefix).toBe(VALID_KEY.substring(0, 12));
|
|
223
|
+
});
|
|
224
|
+
it('falls through when keyring returns null', async () => {
|
|
225
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
226
|
+
mockKeytar.getPassword.mockResolvedValue(null);
|
|
227
|
+
const tmpDir = makeTempConfigDir();
|
|
228
|
+
process.env.HOME = tmpDir;
|
|
229
|
+
const status = await getAuthStatus();
|
|
230
|
+
expect(status.source).not.toBe('keyring');
|
|
231
|
+
});
|
|
232
|
+
it('falls through when keyring throws', async () => {
|
|
233
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
234
|
+
mockKeytar.getPassword.mockRejectedValue(new Error('keyring unavailable'));
|
|
235
|
+
const tmpDir = makeTempConfigDir();
|
|
236
|
+
process.env.HOME = tmpDir;
|
|
237
|
+
const status = await getAuthStatus();
|
|
238
|
+
// Should not throw; should fall through to config/none
|
|
239
|
+
expect(status.authenticated).toBe(false);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
describe('config source', () => {
|
|
243
|
+
it('returns config source when key is in config file and env/keyring are absent', async () => {
|
|
244
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
245
|
+
mockKeytar.getPassword.mockResolvedValue(null);
|
|
246
|
+
const tmpDir = makeTempConfigDir();
|
|
247
|
+
process.env.HOME = tmpDir;
|
|
248
|
+
saveConfig({ apiKey: VALID_KEY });
|
|
249
|
+
const status = await getAuthStatus();
|
|
250
|
+
expect(status.authenticated).toBe(true);
|
|
251
|
+
expect(status.source).toBe('config');
|
|
252
|
+
expect(status.keyPrefix).toBe(VALID_KEY.substring(0, 12));
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
describe('none source', () => {
|
|
256
|
+
it('returns none when no key is stored anywhere', async () => {
|
|
257
|
+
delete process.env['SKILLSMITH_API_KEY'];
|
|
258
|
+
mockKeytar.getPassword.mockResolvedValue(null);
|
|
259
|
+
const tmpDir = makeTempConfigDir();
|
|
260
|
+
process.env.HOME = tmpDir;
|
|
261
|
+
const status = await getAuthStatus();
|
|
262
|
+
expect(status.authenticated).toBe(false);
|
|
263
|
+
expect(status.keyPrefix).toBeNull();
|
|
264
|
+
expect(status.source).toBe('none');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
// ---------------------------------------------------------------------------
|
|
269
|
+
// Keytar import failure simulation
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
describe('keytar import failure fallback', () => {
|
|
272
|
+
it('falls back to config file when @isaacs/keytar cannot be imported', async () => {
|
|
273
|
+
// We simulate unavailability by mocking setPassword to throw.
|
|
274
|
+
// A full module-import-failure path is covered by the storeApiKey fallback test above.
|
|
275
|
+
mockKeytar.setPassword.mockRejectedValue(new Error('simulated import failure'));
|
|
276
|
+
const originalHome = process.env.HOME;
|
|
277
|
+
const tmpDir = makeTempConfigDir();
|
|
278
|
+
process.env.HOME = tmpDir;
|
|
279
|
+
try {
|
|
280
|
+
await storeApiKey(VALID_KEY);
|
|
281
|
+
// No exception means fallback succeeded
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
process.env.HOME = originalHome;
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
//# sourceMappingURL=index.test.js.map
|