• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

agentic-dev-library / thumbcode / 22048848504

16 Feb 2026 03:12AM UTC coverage: 55.51% (+1.1%) from 54.449%
22048848504

push

github

web-flow
fix: resolve all test failures, lint errors, and formatting (#145)

Fix biometric auth error propagation, extract parseDiff for testability, fix lint errors, apply formatting. 893/893 tests, typecheck clean, lint 0 errors, build clean.

1543 of 3156 branches covered (48.89%)

Branch coverage included in aggregate %.

34 of 40 new or added lines in 2 files covered. (85.0%)

196 existing lines in 10 files now uncovered.

2497 of 4122 relevant lines covered (60.58%)

40.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.05
/packages/core/src/credentials/KeyStorage.ts
1
/**
2
 * Key Storage
3
 *
4
 * Handles secure credential storage and retrieval using Capacitor Secure Storage
5
 * with hardware-backed encryption. Includes biometric authentication support.
6
 *
7
 * Falls back to encrypted sessionStorage for web environments with short TTL.
8
 */
9

10
import { BiometricAuth, type BiometryType } from '@aparajita/capacitor-biometric-auth';
11
import { SecureStoragePlugin } from 'capacitor-secure-storage-plugin';
12
import type {
13
  BiometricResult,
14
  CredentialType,
15
  RetrieveOptions,
16
  RetrieveResult,
17
  SecureCredential,
18
  StoreOptions,
19
  ValidationResult,
20
} from './types';
21
import { validate } from './validation';
22
import type { KeyValidator } from './KeyValidator';
23

24
// SecureStore key prefixes for different credential types
25
const SECURE_STORE_KEYS: Record<CredentialType, string> = {
8✔
26
  github: 'thumbcode_cred_github',
27
  anthropic: 'thumbcode_cred_anthropic',
28
  openai: 'thumbcode_cred_openai',
29
  mcp_server: 'thumbcode_cred_mcp',
30
  gitlab: 'thumbcode_cred_gitlab',
31
  bitbucket: 'thumbcode_cred_bitbucket',
32
  mcp_signing_secret: 'thumbcode_cred_mcp_signing_secret',
33
};
34

35
declare global {
36
  interface Window {
37
    Capacitor?: {
38
      isNativePlatform: () => boolean;
39
    };
40
  }
41
}
42

43
// Helper to check for native platform
44
function isNativePlatform(): boolean {
45
  if (typeof window !== 'undefined' && window.Capacitor?.isNativePlatform) {
77!
46
    return window.Capacitor.isNativePlatform();
77✔
47
  }
48
  return false;
×
49
}
50

51
/**
52
 * Web Encryption Implementation (AES-GCM)
53
 *
54
 * Provides a minimal layer of obfuscation/encryption for web storage.
55
 * Note: This is NOT fully secure against a determined attacker with local access,
56
 * as the key is also stored in the browser. However, it prevents casual inspection.
57
 */
58
class WebEncryption {
59
  private static readonly KEY_STORAGE_KEY = 'thumbcode_web_ek';
8✔
60
  private static readonly ALGORITHM = 'AES-GCM';
8✔
61
  private static readonly KEY_LENGTH = 256;
8✔
62

63
  private static async getKey(): Promise<CryptoKey> {
64
    const storedKey = sessionStorage.getItem(this.KEY_STORAGE_KEY);
2✔
65

66
    if (storedKey) {
2!
67
      // Import existing key
UNCOV
68
      const keyData = JSON.parse(storedKey);
×
UNCOV
69
      return crypto.subtle.importKey(
×
70
        'jwk',
71
        keyData,
72
        { name: this.ALGORITHM, length: this.KEY_LENGTH },
73
        true,
74
        ['encrypt', 'decrypt']
75
      );
76
    }
77

78
    // Generate new key
79
    const key = await crypto.subtle.generateKey(
2✔
80
      { name: this.ALGORITHM, length: this.KEY_LENGTH },
81
      true,
82
      ['encrypt', 'decrypt']
83
    );
84

85
    // Export and store key
86
    const exportedKey = await crypto.subtle.exportKey('jwk', key);
2✔
87
    sessionStorage.setItem(this.KEY_STORAGE_KEY, JSON.stringify(exportedKey));
2✔
88

89
    return key;
2✔
90
  }
91

92
  static async encrypt(data: string): Promise<string> {
93
    const key = await this.getKey();
1✔
94
    const iv = crypto.getRandomValues(new Uint8Array(12));
1✔
95
    const encoder = new TextEncoder();
1✔
96
    const encodedData = encoder.encode(data);
1✔
97

98
    const encryptedContent = await crypto.subtle.encrypt(
1✔
99
      { name: this.ALGORITHM, iv },
100
      key,
101
      encodedData
102
    );
103

104
    const encryptedArray = Array.from(new Uint8Array(encryptedContent));
1✔
105
    const ivArray = Array.from(iv);
1✔
106

107
    return JSON.stringify({
1✔
108
      iv: ivArray,
109
      data: encryptedArray,
110
    });
111
  }
112

113
  static async decrypt(encryptedString: string): Promise<string | null> {
114
    try {
1✔
115
      const { iv, data } = JSON.parse(encryptedString);
1✔
116
      const key = await this.getKey();
1✔
117

118
      const decryptedContent = await crypto.subtle.decrypt(
1✔
119
        { name: this.ALGORITHM, iv: new Uint8Array(iv) },
120
        key,
121
        new Uint8Array(data)
122
      );
123

124
      const decoder = new TextDecoder();
1✔
125
      return decoder.decode(decryptedContent);
1✔
126
    } catch (error) {
UNCOV
127
      console.error('Decryption failed:', error);
×
UNCOV
128
      return null;
×
129
    }
130
  }
131
}
132

133
export class KeyStorage {
134
  constructor(private validator: KeyValidator) {}
31✔
135

136
  /**
137
   * Check if biometric authentication is available on the device
138
   */
139
  async isBiometricAvailable(): Promise<boolean> {
140
    if (!isNativePlatform()) {
5✔
141
      return false;
1✔
142
    }
143
    const result = await BiometricAuth.checkBiometry();
4✔
144
    return result.isAvailable;
4✔
145
  }
146

147
  /**
148
   * Get the available biometric authentication types
149
   */
150
  async getBiometricTypes(): Promise<BiometryType[]> {
UNCOV
151
    if (!isNativePlatform()) {
×
UNCOV
152
      return [];
×
153
    }
UNCOV
154
    const result = await BiometricAuth.checkBiometry();
×
155
    // Return array with the detected biometry type, or empty if none
UNCOV
156
    return result.isAvailable ? [result.biometryType] : [];
×
157
  }
158

159
  /**
160
   * Perform biometric authentication
161
   */
162
  async authenticateWithBiometrics(
163
    promptMessage = 'Authenticate to access your credentials'
9✔
164
  ): Promise<BiometricResult> {
165
    if (!isNativePlatform()) {
9✔
166
      return { success: false, error: 'Biometric authentication is not supported on web' };
2✔
167
    }
168

169
    try {
7✔
170
      await BiometricAuth.authenticate({
7✔
171
        reason: promptMessage,
172
        cancelTitle: 'Cancel',
173
        allowDeviceCredential: true,
174
      });
175

176
      // If authenticate resolves without throwing, auth succeeded
177
      return { success: true };
3✔
178
    } catch {
179
      return {
4✔
180
        success: false,
181
        error: 'Biometric authentication failed',
182
      };
183
    }
184
  }
185

186
  /**
187
   * Store a credential securely
188
   */
189
  async store(
190
    type: CredentialType,
191
    secret: string,
192
    options: StoreOptions = {}
25✔
193
  ): Promise<ValidationResult> {
194
    const { requireBiometric = false, skipValidation = false } = options;
25✔
195

196
    if (!validate(type, secret)) {
25✔
197
      return { isValid: false, message: 'Invalid credential format' };
7✔
198
    }
199

200
    // Biometric check if required
201
    // On web, if requireBiometric is true, this will fail because authenticateWithBiometrics returns false
202
    if (requireBiometric) {
18✔
203
      const biometricResult = await this.authenticateWithBiometrics();
5✔
204
      if (!biometricResult.success) {
5✔
205
        return {
3✔
206
          isValid: false,
207
          message: biometricResult.error || 'Biometric authentication failed'
3!
208
        };
209
      }
210
    }
211

212
    // Validate the credential before storing (unless skipped)
213
    if (!skipValidation) {
15✔
214
      const validation = await this.validator.validateCredential(type, secret);
5✔
215
      if (!validation.isValid) {
5✔
216
        return validation;
1✔
217
      }
218
    }
219

220
    const key = SECURE_STORE_KEYS[type];
14✔
221
    const payload: SecureCredential = {
14✔
222
      secret,
223
      storedAt: new Date().toISOString(),
224
      type,
225
    };
226
    const value = JSON.stringify(payload);
14✔
227

228
    try {
14✔
229
      if (isNativePlatform()) {
14✔
230
        await SecureStoragePlugin.set({ key, value });
13✔
231
      } else {
232
        // Encrypt before storing in sessionStorage
233
        try {
1✔
234
          const encryptedValue = await WebEncryption.encrypt(value);
1✔
235
          sessionStorage.setItem(key, encryptedValue);
1✔
236
        } catch (e) {
237
          // Handle quota exceeded or private mode restrictions
UNCOV
238
          return {
×
239
            isValid: false,
240
            message: 'Failed to store in session storage (Storage full or restricted)',
241
          };
242
        }
243
      }
244

245
      return { isValid: true, message: 'Credential stored successfully' };
13✔
246
    } catch (error) {
247
      return {
1✔
248
        isValid: false,
249
        message: error instanceof Error ? error.message : 'Failed to store credential',
1!
250
      };
251
    }
252
  }
253

254
  /**
255
   * Retrieve a credential securely
256
   */
257
  async retrieve(type: CredentialType, options: RetrieveOptions = {}): Promise<RetrieveResult> {
16✔
258
    const { requireBiometric = false } = options;
16✔
259

260
    // Biometric check if required
261
    if (requireBiometric) {
16✔
262
      const biometricResult = await this.authenticateWithBiometrics();
3✔
263
      if (!biometricResult.success) {
3✔
264
        return { secret: null };
2✔
265
      }
266
    }
267

268
    try {
14✔
269
      const key = SECURE_STORE_KEYS[type];
14✔
270
      let payload: string | null = null;
14✔
271

272
      if (isNativePlatform()) {
14✔
273
        const result = await SecureStoragePlugin.get({ key });
12✔
274
        payload = result.value;
8✔
275
      } else {
276
        try {
2✔
277
          const encryptedValue = sessionStorage.getItem(key);
2✔
278
          if (encryptedValue) {
2✔
279
            payload = await WebEncryption.decrypt(encryptedValue);
1✔
280
          }
281
        } catch (e) {
UNCOV
282
          console.error('Failed to access session storage:', e);
×
UNCOV
283
          return { secret: null };
×
284
        }
285
      }
286

287
      if (!payload) {
10✔
288
        return { secret: null };
1✔
289
      }
290

291
      const data: SecureCredential = JSON.parse(payload);
9✔
292

293
      return {
9✔
294
        secret: data.secret,
295
        metadata: {
296
          storedAt: data.storedAt,
297
          type: data.type,
298
        },
299
      };
300
    } catch (error) {
301
      // SecureStoragePlugin.get throws when key is not found
302
      console.error('Failed to retrieve credential:', error);
4✔
303
      return { secret: null };
4✔
304
    }
305
  }
306

307
  /**
308
   * Delete a credential securely
309
   */
310
  async delete(type: CredentialType): Promise<boolean> {
311
    try {
3✔
312
      const key = SECURE_STORE_KEYS[type];
3✔
313
      if (isNativePlatform()) {
3!
314
        await SecureStoragePlugin.remove({ key });
3✔
315
      } else {
UNCOV
316
        try {
×
UNCOV
317
          sessionStorage.removeItem(key);
×
318
        } catch (e) {
UNCOV
319
          console.error('Failed to remove from session storage:', e);
×
UNCOV
320
          return false;
×
321
        }
322
      }
323
      return true;
2✔
324
    } catch (error) {
325
      console.error('Failed to delete credential:', error);
1✔
326
      return false;
1✔
327
    }
328
  }
329

330
  /**
331
   * Check if a credential exists
332
   */
333
  async exists(type: CredentialType): Promise<boolean> {
334
    try {
32✔
335
      const key = SECURE_STORE_KEYS[type];
32✔
336
      if (isNativePlatform()) {
32!
337
        const result = await SecureStoragePlugin.get({ key });
32✔
338
        return result.value !== null && result.value !== undefined;
9✔
339
      } else {
UNCOV
340
        try {
×
UNCOV
341
          return sessionStorage.getItem(key) !== null;
×
342
        } catch {
UNCOV
343
          return false;
×
344
        }
345
      }
346
    } catch {
347
      // SecureStoragePlugin.get throws when key is not found
348
      return false;
23✔
349
    }
350
  }
351

352
  /**
353
   * Get all stored credential types
354
   */
355
  async getStoredCredentialTypes(): Promise<CredentialType[]> {
356
    const types = Object.keys(SECURE_STORE_KEYS) as CredentialType[];
4✔
357
    const results = await Promise.all(types.map((type) => this.exists(type)));
28✔
358
    return types.filter((_, index) => results[index]);
28✔
359
  }
360

361
  /**
362
   * Validate all stored credentials and return results
363
   */
364
  async validateAllStored(): Promise<Map<CredentialType, ValidationResult>> {
365
    const results = new Map<CredentialType, ValidationResult>();
1✔
366
    const storedTypes = await this.getStoredCredentialTypes();
1✔
367

368
    await Promise.all(
1✔
369
      storedTypes.map(async (type) => {
370
        const { secret } = await this.retrieve(type);
3✔
371
        if (secret) {
3!
372
          const result = await this.validator.validateCredential(type, secret);
3✔
373
          results.set(type, result);
3✔
374
        }
375
      })
376
    );
377

378
    return results;
1✔
379
  }
380
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc