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

box / boxcli / 22837876813

09 Mar 2026 04:05AM UTC coverage: 85.312% (-0.3%) from 85.585%
22837876813

Pull #632

github

web-flow
Merge 1ff6cfad3 into 11a348b05
Pull Request #632: feat: Unify `keychain` and `keytar` dependencies

1376 of 1828 branches covered (75.27%)

Branch coverage included in aggregate %.

85 of 135 new or added lines in 5 files covered. (62.96%)

2 existing lines in 2 files now uncovered.

4827 of 5443 relevant lines covered (88.68%)

623.33 hits per line

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

79.83
/src/token-cache.js
1
'use strict';
2

3
/* eslint-disable promise/catch-or-return,promise/no-callback-in-promise */
4

5
const fs = require('node:fs');
9✔
6
const os = require('node:os');
9✔
7
const path = require('node:path');
9✔
8
const BoxCLIError = require('./cli-error');
9✔
9
const utilities = require('./util');
9✔
10
const DEBUG = require('./debug');
9✔
11

12
let keytar = null;
9✔
13
try {
9✔
14
        keytar = require('keytar');
9✔
15
} catch {
16
        // keytar cannot be imported because the library is not provided for this operating system / architecture
17
}
18

19
/**
20
 * Cache interface used by the Node SDK to cache tokens to disk in the user's home directory
21
 * Supports secure storage via keytar with fallback to file system
22
 */
23
class CLITokenCache {
24
        /**
25
         * @constructor
26
         * @param {string} environmentName The name of the active CLI environment
27
         */
28
        constructor(environmentName) {
29
                this.environmentName = environmentName;
360✔
30
                this.filePath = path.join(
360✔
31
                        os.homedir(),
32
                        '.box',
33
                        `${environmentName}_token_cache.json`
34
                );
35
                // Service and account for keytar - includes environment name for multiple environments
36
                this.keytarService = `boxcli-token-${environmentName}`;
360✔
37
                this.keytarAccount = 'Box';
360✔
38
                this.supportsSecureStorage =
360✔
39
                        keytar && ['darwin', 'win32', 'linux'].includes(process.platform);
600✔
40
        }
41

42
        /**
43
         * Read tokens from secure storage with fallback to file system
44
         * @param {Function} callback The callback to pass resulting token info to
45
         * @returns {void}
46
         */
47
        read(callback) {
48
                // Try secure storage first if available
49
                if (this.supportsSecureStorage) {
75✔
50
                        keytar
12✔
51
                                .getPassword(this.keytarService, this.keytarAccount)
52
                                .then((tokenJson) => {
53
                                        if (tokenJson) {
6!
NEW
54
                                                try {
×
NEW
55
                                                        const tokenInfo = JSON.parse(tokenJson);
×
NEW
56
                                                        DEBUG.init(
×
57
                                                                'Loaded token from secure storage for environment: %s',
58
                                                                this.environmentName
59
                                                        );
NEW
60
                                                        return callback(null, tokenInfo);
×
61
                                                } catch (parseError) {
NEW
62
                                                        DEBUG.init(
×
63
                                                                'Failed to parse token from secure storage, falling back to file: %s',
64
                                                                parseError.message
65
                                                        );
66
                                                        // Fall through to file-based storage
67
                                                }
68
                                        }
69
                                        // Token not in secure storage, try file
70
                                        return this._readFromFile(callback);
6✔
71
                                })
72
                                .catch((error) => {
73
                                        DEBUG.init(
6✔
74
                                                'Failed to read from secure storage, falling back to file: %s',
75
                                                error.message
76
                                        );
77
                                        // Fall back to file-based storage
78
                                        this._readFromFile(callback);
6✔
79
                                });
80
                } else {
81
                        // Secure storage not available, use file
82
                        this._readFromFile(callback);
63✔
83
                }
84
        }
85

86
        /**
87
         * Read tokens from file system
88
         * @param {Function} callback The callback to pass resulting token info to
89
         * @returns {void}
90
         * @private
91
         */
92
        _readFromFile(callback) {
93
                utilities
75✔
94
                        .readFileAsync(this.filePath, 'utf8')
95
                        .then((json) => {
96
                                const tokenInfo = JSON.parse(json);
57✔
97
                                if (tokenInfo.accessToken) {
48!
98
                                        DEBUG.init(
48✔
99
                                                'Loaded token from file system for environment: %s (will be migrated to secure storage on next write)',
100
                                                this.environmentName
101
                                        );
102
                                }
103
                                return tokenInfo;
48✔
104
                        })
105
                        // If file is not present or not valid JSON, treat that as empty (but available) cache
106
                        .catch(() => ({}))
27✔
107
                        .then((tokenInfo) => callback(null, tokenInfo));
75✔
108
        }
109

110
        /**
111
         * Write tokens to secure storage with fallback to file system
112
         * @param {Object} tokenInfo The token object to write
113
         * @param {Function} callback The callback to pass results to
114
         * @returns {void}
115
         */
116
        write(tokenInfo, callback) {
117
                const output = JSON.stringify(tokenInfo, null, 4);
75✔
118

119
                // Try secure storage first if available
120
                if (this.supportsSecureStorage) {
75✔
121
                        keytar
12✔
122
                                .setPassword(this.keytarService, this.keytarAccount, output)
123
                                .then(() => {
124
                                        DEBUG.init(
6✔
125
                                                'Stored token in secure storage for environment: %s',
126
                                                this.environmentName
127
                                        );
128
                                        // Clear the file-based cache if it exists (migration scenario)
129
                                        if (fs.existsSync(this.filePath)) {
6!
130
                                                fs.unlinkSync(this.filePath);
6✔
131
                                        }
132
                                        return;
6✔
133
                                })
134
                                .then(() => {
135
                                        DEBUG.init(
6✔
136
                                                'Migrated token from file to secure storage for environment: %s',
137
                                                this.environmentName
138
                                        );
139
                                        return callback();
6✔
140
                                })
141
                                .catch((error) => {
142
                                        DEBUG.init(
6✔
143
                                                'Failed to write to secure storage for environment %s, falling back to file: %s',
144
                                                this.environmentName,
145
                                                error.message
146
                                        );
147
                                        if (process.platform === 'linux') {
6!
NEW
148
                                                DEBUG.init(
×
149
                                                        'To enable secure storage on Linux, install libsecret-1-dev package'
150
                                                );
151
                                        }
152
                                        // Fall back to file-based storage
153
                                        this._writeToFile(output, callback);
6✔
154
                                });
155
                } else {
156
                        // Secure storage not available, use file
157
                        this._writeToFile(output, callback);
63✔
158
                }
159
        }
160

161
        /**
162
         * Write tokens to file system
163
         * @param {string} output The JSON string to write
164
         * @param {Function} callback The callback to pass results to
165
         * @returns {void}
166
         * @private
167
         */
168
        _writeToFile(output, callback) {
169
                utilities
69✔
170
                        .writeFileAsync(this.filePath, output, 'utf8')
171
                        // Pass success or error to the callback
172
                        .then(callback)
173
                        .catch((error) =>
174
                                callback(
×
175
                                        new BoxCLIError('Failed to write to token cache', error)
176
                                )
177
                        );
178
        }
179

180
        /**
181
         * Delete the token from both secure storage and file system
182
         * @param {Function} callback The callback to pass results to
183
         * @returns {void}
184
         */
185
        clear(callback) {
186
                const promises = [];
60✔
187

188
                // Try to delete from secure storage
189
                if (this.supportsSecureStorage) {
60✔
190
                        promises.push(
6✔
191
                                keytar
192
                                        .deletePassword(this.keytarService, this.keytarAccount)
193
                                        .then((deleted) => {
NEW
194
                                                if (!deleted) {
×
NEW
195
                                                        DEBUG.init(
×
196
                                                                'No token found in secure storage for environment: %s',
197
                                                                this.environmentName
198
                                                        );
199
                                                }
NEW
200
                                                return deleted;
×
201
                                        })
202
                                        .catch((error) => {
203
                                                const message = String(
6✔
204
                                                        error?.message || ''
6!
205
                                                ).toLowerCase();
206
                                                const isMissingSecretError =
207
                                                        error?.code === 'ENOENT' ||
6✔
208
                                                        message.includes('not found') ||
209
                                                        message.includes('password not found') ||
210
                                                        message.includes('item not found') ||
211
                                                        message.includes('could not be found');
212

213
                                                if (isMissingSecretError) {
6!
NEW
214
                                                        DEBUG.init(
×
215
                                                                'No token found in secure storage for environment: %s',
216
                                                                this.environmentName
217
                                                        );
NEW
218
                                                        return null;
×
219
                                                }
220

221
                                                throw new BoxCLIError(
6✔
222
                                                        'Failed to delete token from secure storage',
223
                                                        error
224
                                                );
225
                                        })
226
                        );
227
                }
228

229
                // Try to delete from file
230
                promises.push(
60✔
231
                        utilities.unlinkAsync(this.filePath).catch((error) => {
232
                                if (error && error.code === 'ENOENT') {
27✔
233
                                        DEBUG.init(
18✔
234
                                                'No token file found on disk for environment: %s',
235
                                                this.environmentName
236
                                        );
237
                                        return;
18✔
238
                                }
239
                                throw new BoxCLIError('Failed to delete token file', error);
9✔
240
                        })
241
                );
242

243
                Promise.all(promises)
60✔
244
                        .then(() => callback())
45✔
245
                        .catch((error) =>
246
                                callback(new BoxCLIError('Failed to delete token cache', error))
15✔
247
                        );
248
        }
249

250
        /**
251
         * Write the token to storage, compatible with TS SDK
252
         * @param {AccessToken} token The token to write
253
         * @returns {Promise<undefined>} A promise resolving to undefined
254
         */
255
        store(token) {
256
                return new Promise((resolve, reject) => {
18✔
257
                        const accquiredAtMS = Date.now();
18✔
258
                        const tokenInfo = {
18✔
259
                                accessToken: token.accessToken,
260
                                accessTokenTTLMS: token.expiresIn * 1000,
261
                                refreshToken: token.refreshToken,
262
                                acquiredAtMS: accquiredAtMS,
263
                        };
264
                        this.write(tokenInfo, (error) => {
18✔
265
                                if (error) {
18!
266
                                        reject(error);
×
267
                                } else {
268
                                        resolve();
18✔
269
                                }
270
                        });
271
                });
272
        }
273

274
        /**
275
         * Read the token from storage, compatible with TS SDK
276
         * @returns {Promise<undefined | AccessToken>} A promise resolving to the token
277
         */
278
        get() {
279
                return new Promise((resolve, reject) => {
18✔
280
                        this.read((error, tokenInfo) => {
18✔
281
                                if (error) {
18!
282
                                        reject(error);
×
283
                                } else {
284
                                        resolve(tokenInfo.accessToken ? tokenInfo : undefined);
18✔
285
                                }
286
                        });
287
                });
288
        }
289
}
290

291
module.exports = CLITokenCache;
9✔
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