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

box / boxcli / 26750051203

01 Jun 2026 10:43AM UTC coverage: 84.967% (-0.005%) from 84.972%
26750051203

push

github

web-flow
feat: replace `keytar` with `@github/keytar` (#688)

1577 of 2151 branches covered (73.31%)

Branch coverage included in aggregate %.

18 of 21 new or added lines in 1 file covered. (85.71%)

5392 of 6051 relevant lines covered (89.11%)

643.35 hits per line

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

85.29
/src/secure-storage.js
1
'use strict';
2

3
const { promisify } = require('node:util');
93✔
4
const fs = require('node:fs');
93✔
5
const path = require('node:path');
93✔
6
const DEBUG = require('./debug');
93✔
7
const PLATFORM_DARWIN = 'darwin';
93✔
8
const KEYTAR_PACKAGE = '@github/keytar';
93✔
9
const KEYTAR = 'keytar';
93✔
10
const KEYCHAIN = 'keychain';
93✔
11

12
/**
13
 * Ensure the correct platform-specific keytar.node binary exists in
14
 * build/Release before requiring @github/keytar. This handles the case
15
 * where the CLI is packed on one OS (e.g. macOS) but runs on another
16
 * (e.g. Windows): the prebuilds/ folder ships all platforms, and this
17
 * copies the right one into place on first run.
18
 */
19
function ensureKeytarBinary() {
20
        try {
93✔
21
                const keytarDir = path.dirname(
93✔
22
                        require.resolve('@github/keytar/package.json')
23
                );
24
                const releaseDir = path.join(keytarDir, 'build', 'Release');
93✔
25
                const targetBinary = path.join(releaseDir, 'keytar.node');
93✔
26

27
                if (fs.existsSync(targetBinary)) {
93✔
28
                        DEBUG.init('keytar binary already exists at %s', targetBinary);
78✔
29
                        return;
78✔
30
                }
31

32
                const prebuildDir = path.join(
15✔
33
                        keytarDir,
34
                        'prebuilds',
35
                        `${process.platform}-${process.arch}`
36
                );
37
                const sourceBinary = path.join(prebuildDir, 'keytar.node');
15✔
38

39
                if (!fs.existsSync(sourceBinary)) {
15!
NEW
40
                        DEBUG.init('keytar prebuild not found at %s', sourceBinary);
×
NEW
41
                        return;
×
42
                }
43

44
                fs.mkdirSync(releaseDir, { recursive: true });
15✔
45
                fs.copyFileSync(sourceBinary, targetBinary);
15✔
46
                DEBUG.init('Copied keytar binary from %s to %s', sourceBinary, targetBinary);
15✔
47
        } catch (error) {
NEW
48
                DEBUG.init('Failed to ensure keytar binary, falling back to prebuilds/: %s', error.message);
×
49
        }
50
}
51

52
/**
53
 * Load an optional dependency and capture load errors.
54
 *
55
 * @param {string} packageName Package to load
56
 * @param {boolean} shouldLoad Whether this package should be loaded
57
 * @returns {{ loadedModule: unknown, loadError: unknown }} Result of loading
58
 */
59
function loadOptionalModule(packageName, shouldLoad = true) {
×
60
        if (!shouldLoad) {
186✔
61
                return { loadedModule: null, loadError: null };
93✔
62
        }
63
        try {
93✔
64
                return { loadedModule: require(packageName), loadError: null };
93✔
65
        } catch (error) {
66
                return { loadedModule: null, loadError: error };
27✔
67
        }
68
}
69

70
ensureKeytarBinary();
93✔
71

72
const { loadedModule: keytarModule, loadError: keytarLoadError } =
73
        loadOptionalModule(KEYTAR_PACKAGE, process.platform !== PLATFORM_DARWIN);
93✔
74
const { loadedModule: keychainModule, loadError: keychainLoadError } =
75
        loadOptionalModule(KEYCHAIN, process.platform === PLATFORM_DARWIN);
93✔
76

77
const isDarwin = process.platform === PLATFORM_DARWIN;
93✔
78
const SUPPORTED_SECURE_STORAGE_PLATFORMS = [PLATFORM_DARWIN, 'win32', 'linux'];
93✔
79
const isSecurePlatform = SUPPORTED_SECURE_STORAGE_PLATFORMS.includes(
93✔
80
        process.platform
81
);
82

83
/**
84
 * Returns true when error indicates missing keychain/keytar entry.
85
 *
86
 * @param {unknown} error The caught error
87
 * @returns {boolean} Whether this is a "secret not found" error
88
 */
89
function isSecretNotFoundError(error) {
90
        const message = String(error?.message || '').toLowerCase();
12!
91
        return (
12✔
92
                error?.code === 'ENOENT' ||
51✔
93
                message.includes('not found') ||
94
                message.includes('password not found') ||
95
                message.includes('item not found') ||
96
                message.includes('could not find password')
97
        );
98
}
99

100
/**
101
 * Unified secure storage wrapper.
102
 *
103
 * On macOS uses the `keychain` npm module (which wraps `/usr/bin/security`).
104
 * ACL (Access Control List) in Keychain is a per-secret allowlist of apps
105
 * that can access the item without prompting. Using `keychain` avoids ACL
106
 * prompts because the accessing process is always the stable system
107
 * `security` binary, regardless of CLI binary identity/signature changes.
108
 * If we used `keytar` on macOS, access would come from the current
109
 * `node`/CLI executable identity; after signed-binary upgrades, macOS can
110
 * treat it as a different app and show ACL prompts for existing items.
111
 * That is why this module intentionally does not use `keytar` on macOS.
112
 *
113
 * On Windows/Linux uses `keytar` (native Keychain/Credential Vault/libsecret).
114
 */
115
class SecureStorage {
116
        constructor() {
117
                if (isDarwin && keychainModule) {
93✔
118
                        this.backend = KEYCHAIN;
45✔
119
                        this.available = true;
45✔
120
                } else if (!isDarwin && isSecurePlatform && keytarModule) {
48✔
121
                        this.backend = KEYTAR;
21✔
122
                        this.available = true;
21✔
123
                } else {
124
                        this.backend = null;
27✔
125
                        this.available = false;
27✔
126
                }
127

128
                DEBUG.init('Secure storage initialized %O', {
93✔
129
                        platform: process.platform,
130
                        arch: process.arch,
131
                        backend: this.backend,
132
                        available: this.available,
133
                        keytarLoaded: Boolean(keytarModule),
134
                        darwinKeychainLoaded: Boolean(keychainModule),
135
                });
136

137
                if (!this.available) {
93✔
138
                        if (isDarwin && !keychainModule) {
27!
139
                                DEBUG.init(
×
140
                                        'macOS keychain module not available: %s',
141
                                        keychainLoadError?.message || 'unknown'
×
142
                                );
143
                        }
144
                        if (!isDarwin && !keytarModule) {
27!
145
                                DEBUG.init(
27✔
146
                                        'keytar module not available: %s',
147
                                        keytarLoadError?.message || 'unknown'
27!
148
                                );
149
                        }
150
                }
151
        }
152

153
        /**
154
         * Read a password from secure storage.
155
         *
156
         * @param {string} service The service name
157
         * @param {string} account The account name
158
         * @returns {Promise<string|null>} The stored password, or null
159
         */
160
        async getPassword(service, account) {
161
                if (!this.available) {
16,173✔
162
                        return null;
9✔
163
                }
164

165
                if (this.backend === KEYCHAIN) {
16,164✔
166
                        try {
8,085✔
167
                                const getPasswordAsync = promisify(
8,085✔
168
                                        keychainModule.getPassword.bind(keychainModule)
169
                                );
170
                                const password = await getPasswordAsync({
8,085✔
171
                                        account,
172
                                        service,
173
                                });
174
                                return password || null;
8,076!
175
                        } catch (error) {
176
                                if (isSecretNotFoundError(error)) {
9!
177
                                        return null;
9✔
178
                                }
179
                                throw error;
×
180
                        }
181
                }
182

183
                return keytarModule.getPassword(service, account);
8,079✔
184
        }
185

186
        /**
187
         * Write a password to secure storage.
188
         *
189
         * @param {string} service The service name
190
         * @param {string} account The account name
191
         * @param {string} password The value to store
192
         * @returns {Promise<void>}
193
         */
194
        async setPassword(service, account, password) {
195
                if (!this.available) {
5,400✔
196
                        throw new Error('Secure storage is not available');
9✔
197
                }
198

199
                if (this.backend === KEYCHAIN) {
5,391✔
200
                        const setPasswordAsync = promisify(
2,697✔
201
                                keychainModule.setPassword.bind(keychainModule)
202
                        );
203
                        await setPasswordAsync({ account, service, password });
2,697✔
204
                        return;
2,697✔
205
                }
206

207
                await keytarModule.setPassword(service, account, password);
2,694✔
208
        }
209

210
        /**
211
         * Delete a password from secure storage.
212
         *
213
         * @param {string} service The service name
214
         * @param {string} account The account name
215
         * @returns {Promise<boolean>} true if deleted
216
         */
217
        async deletePassword(service, account) {
218
                if (!this.available) {
15✔
219
                        return false;
9✔
220
                }
221

222
                if (this.backend === KEYCHAIN) {
6!
223
                        try {
6✔
224
                                const deletePasswordAsync = promisify(
6✔
225
                                        keychainModule.deletePassword.bind(keychainModule)
226
                                );
227
                                await deletePasswordAsync({ account, service });
6✔
228
                                return true;
3✔
229
                        } catch (error) {
230
                                if (isSecretNotFoundError(error)) {
3!
231
                                        return false;
3✔
232
                                }
233
                                throw error;
×
234
                        }
235
                }
236

237
                return keytarModule.deletePassword(service, account);
×
238
        }
239
}
240

241
module.exports = new SecureStorage();
93✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc