@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.
Files changed (130) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/src/analysis/__tests__/incremental.test.d.ts +1 -1
  3. package/dist/src/analysis/__tests__/incremental.test.js +1 -1
  4. package/dist/src/analysis/__tests__/integration.test.d.ts +1 -1
  5. package/dist/src/analysis/__tests__/integration.test.js +1 -1
  6. package/dist/src/analysis/__tests__/performance.test.d.ts +1 -1
  7. package/dist/src/analysis/__tests__/performance.test.js +1 -1
  8. package/dist/src/analysis/adapters/__tests__/python.test.d.ts +1 -1
  9. package/dist/src/analysis/adapters/__tests__/python.test.js +1 -1
  10. package/dist/src/analysis/adapters/__tests__/typescript.test.d.ts +1 -1
  11. package/dist/src/analysis/adapters/__tests__/typescript.test.js +1 -1
  12. package/dist/src/analysis/adapters/base.d.ts +1 -1
  13. package/dist/src/analysis/adapters/base.js +1 -1
  14. package/dist/src/analysis/adapters/factory.d.ts +1 -1
  15. package/dist/src/analysis/adapters/factory.js +1 -1
  16. package/dist/src/analysis/adapters/go.d.ts +1 -1
  17. package/dist/src/analysis/adapters/go.js +1 -1
  18. package/dist/src/analysis/adapters/index.d.ts +1 -1
  19. package/dist/src/analysis/adapters/index.js +1 -1
  20. package/dist/src/analysis/adapters/java-parsers.d.ts +1 -1
  21. package/dist/src/analysis/adapters/java-parsers.js +1 -1
  22. package/dist/src/analysis/adapters/java.d.ts +1 -1
  23. package/dist/src/analysis/adapters/java.js +1 -1
  24. package/dist/src/analysis/adapters/python-frameworks.d.ts +1 -1
  25. package/dist/src/analysis/adapters/python-frameworks.js +1 -1
  26. package/dist/src/analysis/adapters/python.d.ts +1 -1
  27. package/dist/src/analysis/adapters/python.js +1 -1
  28. package/dist/src/analysis/adapters/rust-parsers.d.ts +1 -1
  29. package/dist/src/analysis/adapters/rust-parsers.js +1 -1
  30. package/dist/src/analysis/adapters/rust.d.ts +1 -1
  31. package/dist/src/analysis/adapters/rust.js +1 -1
  32. package/dist/src/analysis/adapters/typescript.d.ts +1 -1
  33. package/dist/src/analysis/adapters/typescript.js +1 -1
  34. package/dist/src/analysis/aggregator.d.ts +1 -1
  35. package/dist/src/analysis/aggregator.js +1 -1
  36. package/dist/src/analysis/cache.d.ts +1 -1
  37. package/dist/src/analysis/cache.js +1 -1
  38. package/dist/src/analysis/file-streamer.d.ts +1 -1
  39. package/dist/src/analysis/file-streamer.js +1 -1
  40. package/dist/src/analysis/incremental-parser.d.ts +1 -1
  41. package/dist/src/analysis/incremental-parser.js +1 -1
  42. package/dist/src/analysis/incremental.d.ts +1 -1
  43. package/dist/src/analysis/incremental.js +1 -1
  44. package/dist/src/analysis/index.d.ts +1 -1
  45. package/dist/src/analysis/index.js +1 -1
  46. package/dist/src/analysis/language-detector.d.ts +1 -1
  47. package/dist/src/analysis/language-detector.js +1 -1
  48. package/dist/src/analysis/memory-monitor.d.ts +1 -1
  49. package/dist/src/analysis/memory-monitor.js +1 -1
  50. package/dist/src/analysis/metrics.d.ts +1 -1
  51. package/dist/src/analysis/metrics.js +1 -1
  52. package/dist/src/analysis/router.d.ts +1 -1
  53. package/dist/src/analysis/router.js +1 -1
  54. package/dist/src/analysis/tree-cache.d.ts +1 -1
  55. package/dist/src/analysis/tree-cache.js +1 -1
  56. package/dist/src/analysis/tree-sitter/manager.d.ts +1 -1
  57. package/dist/src/analysis/tree-sitter/manager.js +1 -1
  58. package/dist/src/analysis/types.d.ts +1 -1
  59. package/dist/src/analysis/types.js +1 -1
  60. package/dist/src/analysis/worker-pool.d.ts +1 -1
  61. package/dist/src/analysis/worker-pool.js +1 -1
  62. package/dist/src/analysis/worker-types.d.ts +1 -1
  63. package/dist/src/analysis/worker-types.js +1 -1
  64. package/dist/src/analysis/worker-utils.d.ts +1 -1
  65. package/dist/src/analysis/worker-utils.js +1 -1
  66. package/dist/src/config/index.d.ts +49 -1
  67. package/dist/src/config/index.d.ts.map +1 -1
  68. package/dist/src/config/index.js +166 -3
  69. package/dist/src/config/index.js.map +1 -1
  70. package/dist/src/config/index.test.d.ts +11 -0
  71. package/dist/src/config/index.test.d.ts.map +1 -0
  72. package/dist/src/config/index.test.js +288 -0
  73. package/dist/src/config/index.test.js.map +1 -0
  74. package/dist/src/db/quarantine-approvals-schema.d.ts +37 -0
  75. package/dist/src/db/quarantine-approvals-schema.d.ts.map +1 -0
  76. package/dist/src/db/quarantine-approvals-schema.js +71 -0
  77. package/dist/src/db/quarantine-approvals-schema.js.map +1 -0
  78. package/dist/src/embeddings/embedding-utils.d.ts +3 -3
  79. package/dist/src/embeddings/embedding-utils.d.ts.map +1 -1
  80. package/dist/src/embeddings/embedding-utils.js +3 -3
  81. package/dist/src/embeddings/embedding-utils.js.map +1 -1
  82. package/dist/src/embeddings/index.d.ts +1 -1
  83. package/dist/src/embeddings/index.js +3 -3
  84. package/dist/src/embeddings/index.js.map +1 -1
  85. package/dist/src/index.d.ts +1 -1
  86. package/dist/src/index.d.ts.map +1 -1
  87. package/dist/src/index.js +2 -2
  88. package/dist/src/index.js.map +1 -1
  89. package/dist/src/repositories/quarantine/ApprovalRepository.d.ts +148 -0
  90. package/dist/src/repositories/quarantine/ApprovalRepository.d.ts.map +1 -0
  91. package/dist/src/repositories/quarantine/ApprovalRepository.js +212 -0
  92. package/dist/src/repositories/quarantine/ApprovalRepository.js.map +1 -0
  93. package/dist/src/repositories/quarantine/index.d.ts +2 -0
  94. package/dist/src/repositories/quarantine/index.d.ts.map +1 -1
  95. package/dist/src/repositories/quarantine/index.js +1 -0
  96. package/dist/src/repositories/quarantine/index.js.map +1 -1
  97. package/dist/src/security/audit-types.d.ts +1 -1
  98. package/dist/src/security/audit-types.d.ts.map +1 -1
  99. package/dist/src/security/audit-types.js.map +1 -1
  100. package/dist/src/security/scanner/SecurityScanner.formatters.js +1 -1
  101. package/dist/src/security/scanner/SecurityScanner.formatters.js.map +1 -1
  102. package/dist/src/services/quarantine/QuarantineService.d.ts +17 -9
  103. package/dist/src/services/quarantine/QuarantineService.d.ts.map +1 -1
  104. package/dist/src/services/quarantine/QuarantineService.js +114 -56
  105. package/dist/src/services/quarantine/QuarantineService.js.map +1 -1
  106. package/dist/tests/RawUrlSourceAdapter.security.test.js +2 -1
  107. package/dist/tests/RawUrlSourceAdapter.security.test.js.map +1 -1
  108. package/dist/tests/SecurityScanner.test.js +2 -2
  109. package/dist/tests/SecurityScanner.test.js.map +1 -1
  110. package/dist/tests/adapters-factory.test.d.ts +1 -1
  111. package/dist/tests/adapters-factory.test.js +1 -1
  112. package/dist/tests/integration/QuarantineService.test.js +49 -1
  113. package/dist/tests/integration/QuarantineService.test.js.map +1 -1
  114. package/dist/tests/integration/neural/e2e-learning.test.d.ts +1 -1
  115. package/dist/tests/integration/neural/e2e-learning.test.js +1 -1
  116. package/dist/tests/integration/neural/personalization.test.d.ts +1 -1
  117. package/dist/tests/integration/neural/personalization.test.js +1 -1
  118. package/dist/tests/integration/neural/preference-learner.test.d.ts +1 -1
  119. package/dist/tests/integration/neural/preference-learner.test.js +1 -1
  120. package/dist/tests/integration/neural/privacy.test.d.ts +1 -1
  121. package/dist/tests/integration/neural/privacy.test.js +1 -1
  122. package/dist/tests/integration/neural/signal-collection.test.d.ts +1 -1
  123. package/dist/tests/integration/neural/signal-collection.test.js +1 -1
  124. package/dist/tests/language-detector.test.d.ts +1 -1
  125. package/dist/tests/language-detector.test.js +1 -1
  126. package/dist/tests/unit/approval-repository.test.d.ts +9 -0
  127. package/dist/tests/unit/approval-repository.test.d.ts.map +1 -0
  128. package/dist/tests/unit/approval-repository.test.js +509 -0
  129. package/dist/tests/unit/approval-repository.test.js.map +1 -0
  130. 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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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;AAQD;;;;;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,CAqBN;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;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAExD"}
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"}
@@ -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
- const mergedConfig = { ...existingConfig, ...config };
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
- return /^sk_live_[A-Za-z0-9_-]{32,}$/.test(key);
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":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAA;AAC5B,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAC3B,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,IAAI,CAAA;AA0BlF,oCAAoC;AACpC,MAAM,UAAU,GAAG,aAAa,CAAA;AAEhC,+BAA+B;AAC/B,MAAM,WAAW,GAAG,aAAa,CAAA;AAkBjC,wDAAwD;AACxD,IAAI,YAAY,GAAkC,SAAS,CAAA;AAE3D;;;GAGG;AACH,KAAK,UAAU,SAAS;IACtB,IAAI,YAAY,KAAK,SAAS;QAAE,OAAO,YAAY,CAAA;IACnD,IAAI,CAAC;QACH,wFAAwF;QACxF,8EAA8E;QAC9E,8EAA8E;QAC9E,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,gBAAgB,CAAC,CAA0C,CAAA;QACrF,YAAY,GAAG,GAAG,CAAC,OAAO,IAAI,GAAG,CAAA;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;IACD,OAAO,YAAY,CAAA;AACrB,CAAC;AAED,0CAA0C;AAC1C,MAAM,cAAc,GAAG,gBAAgB,CAAA;AAEvC,yCAAyC;AACzC,MAAM,cAAc,GAAG,SAAS,CAAA;AAEhC,+EAA+E;AAC/E,sBAAsB;AACtB,+EAA+E;AAE/E;;;;;GAKG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAA;AACpC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,YAAY,EAAE,EAAE,WAAW,CAAC,CAAA;AAC1C,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe;IAC7B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAA;IAChC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,SAAS,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IACxD,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,UAAU,GAAG,aAAa,EAAE,CAAA;IAElC,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,OAAO,EAAE,CAAA;IACX,CAAC;IAED,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAA;QACpD,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAqB,CAAA;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,oDAAoD;QACpD,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CACxB,MAAiC,EACjC,UAA+B,EAAE,KAAK,EAAE,IAAI,EAAE;IAE9C,eAAe,EAAE,CAAA;IAEjB,MAAM,UAAU,GAAG,aAAa,EAAE,CAAA;IAClC,IAAI,cAAc,GAAqB,EAAE,CAAA;IAEzC,IAAI,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5C,cAAc,GAAG,UAAU,EAAE,CAAA;IAC/B,CAAC;IAED,+DAA+D;IAC/D,MAAM,OAAO,GAAG,MAAM,CAAC,WAAW,CAChC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAC7B,CAAA;IAE9B,6EAA6E;IAC7E,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,CAC1C,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,CAA2B,CAAC,KAAK,SAAS,CACzD,CAAA;IACD,MAAM,OAAO,GAAG,EAAE,GAAG,cAAc,EAAE,CAAA;IACrC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,OAAO,OAAO,CAAC,GAA6B,CAAC,CAAA;IAC/C,CAAC;IAED,MAAM,YAAY,GAAG,EAAE,GAAG,OAAO,EAAE,GAAG,OAAO,EAAE,CAAA;IAC/C,MAAM,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;IAExD,aAAa,CAAC,UAAU,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;IAEzE,8DAA8D;IAC9D,IAAI,CAAC;QACH,SAAS,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,iCAAiC;IACnC,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,SAAS;IACvB,wCAAwC;IACxC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAA;IAC7C,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAA;IACf,CAAC;IAED,2BAA2B;IAC3B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,OAAO,MAAM,CAAC,MAAM,CAAA;AACtB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,UAAU,GAAG,4BAA4B;IACrE,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAA;IAC7C,IAAI,MAAM,EAAE,CAAC;QACX,OAAO,MAAM,CAAA;IACf,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,OAAO,MAAM,CAAC,UAAU,IAAI,UAAU,CAAA;AACxC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc;IAC5B,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,KAAK,GAAG,EAAE,CAAC;QACpF,OAAO,IAAI,CAAA;IACb,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,OAAO,MAAM,CAAC,KAAK,KAAK,IAAI,CAAA;AAC9B,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,OAAO,CAAC,GAAG,CAAC,4BAA4B,KAAK,MAAM,EAAE,CAAC;QACxD,OAAO,IAAI,CAAA;IACb,CAAC;IACD,IAAI,OAAO,CAAC,GAAG,CAAC,4BAA4B,KAAK,OAAO,EAAE,CAAC;QACzD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,OAAO,MAAM,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI,CAAA;AAC3C,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,IAAI,GAAG,CAAC,MAAM,GAAG,GAAG;QAAE,OAAO,KAAK,CAAA,CAAC,cAAc;IACjD,OAAO,iCAAiC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AACpD,CAAC;AAED,+EAA+E;AAC/E,gCAAgC;AAChC,+EAA+E;AAE/E;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAc;IAC9C,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAA;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,cAAc,EAAE,MAAM,CAAC,CAAA;YAChE,OAAM;QACR,CAAC;QAAC,MAAM,CAAC;YACP,+CAA+C;QACjD,CAAC;IACH,CAAC;IACD,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC,CAAA;AACxB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW;IAK/B,MAAM,cAAc,GAAa,EAAE,CAAA;IACnC,IAAI,YAAgC,CAAA;IAEpC,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAA;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,cAAc,EAAE,cAAc,CAAC,CAAA;YAC3E,IAAI,OAAO,EAAE,CAAC;gBACZ,cAAc,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;YAChC,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,YAAY,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACjE,CAAC;IACH,CAAC;IAED,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAA;IACjC,cAAc,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IAElC,IAAI,YAAY,EAAE,CAAC;QACjB,OAAO;YACL,OAAO,EAAE,KAAK;YACd,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;YACpC,KAAK,EAAE,YAAY;SACpB,CAAA;IACH,CAAC;IAED,OAAO;QACL,OAAO,EAAE,IAAI;QACb,MAAM,EAAE,cAAc,CAAC,IAAI,CAAC,OAAO,CAAC;KACrC,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa;IAKjC,+CAA+C;IAC/C,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAA;IAC7C,IAAI,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC;QAC1C,OAAO;YACL,aAAa,EAAE,IAAI;YACnB,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC;YAClC,MAAM,EAAE,KAAK;SACd,CAAA;IACH,CAAC;IAED,gBAAgB;IAChB,MAAM,MAAM,GAAG,MAAM,SAAS,EAAE,CAAA;IAChC,IAAI,MAAM,EAAE,CAAC;QACX,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,cAAc,EAAE,cAAc,CAAC,CAAA;YACxE,IAAI,OAAO,IAAI,mBAAmB,CAAC,OAAO,CAAC,EAAE,CAAC;gBAC5C,OAAO;oBACL,aAAa,EAAE,IAAI;oBACnB,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC;oBACnC,MAAM,EAAE,SAAS;iBAClB,CAAA;YACH,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,8DAA8D;QAChE,CAAC;IACH,CAAC;IAED,iBAAiB;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAA;IAC3B,IAAI,MAAM,CAAC,MAAM,IAAI,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,OAAO;YACL,aAAa,EAAE,IAAI;YACnB,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC;YACzC,MAAM,EAAE,QAAQ;SACjB,CAAA;IACH,CAAC;IAED,OAAO;QACL,aAAa,EAAE,KAAK;QACpB,SAAS,EAAE,IAAI;QACf,MAAM,EAAE,MAAM;KACf,CAAA;AACH,CAAC"}
@@ -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