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

box / boxcli / 22753157565

06 Mar 2026 07:11AM UTC coverage: 85.312% (-0.3%) from 85.585%
22753157565

Pull #632

github

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

1377 of 1828 branches covered (75.33%)

Branch coverage included in aggregate %.

79 of 129 new or added lines in 5 files covered. (61.24%)

2 existing lines in 2 files now uncovered.

4826 of 5443 relevant lines covered (88.66%)

623.32 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(error?.message || '').toLowerCase();
6!
204
                                                const isMissingSecretError =
205
                                                        error?.code === 'ENOENT' ||
6✔
206
                                                        message.includes('not found') ||
207
                                                        message.includes('password not found') ||
208
                                                        message.includes('item not found') ||
209
                                                        message.includes('could not be found');
210

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

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

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

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

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

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

289
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