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

microsoft / botbuilder-js / 12257176797

10 Dec 2024 01:31PM UTC coverage: 84.625% (-0.06%) from 84.686%
12257176797

Pull #4808

github

web-flow
Merge da9796adb into 89ada0e6f
Pull Request #4808: feat: Add support for Node 22

8185 of 10821 branches covered (75.64%)

Branch coverage included in aggregate %.

8 of 12 new or added lines in 3 files covered. (66.67%)

17 existing lines in 1 file now uncovered.

20513 of 23091 relevant lines covered (88.84%)

7401.54 hits per line

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

81.88
/libraries/botframework-config/src/botConfiguration.ts
1
/**
2
 * @module botframework-config
3
 *
4
 * Copyright(c) Microsoft Corporation.All rights reserved.
5
 * Licensed under the MIT License.
6
 */
7
import * as fsx from 'fs-extra';
2✔
8
import * as fs from 'fs';
2✔
9
import * as path from 'path';
2✔
10
import * as process from 'process';
2✔
11
import { v4 as uuidv4 } from 'uuid';
2✔
12
import { BotConfigurationBase } from './botConfigurationBase';
2✔
13
import * as encrypt from './encrypt';
2✔
14
import { ConnectedService } from './models';
15
import { IBotConfiguration, IConnectedService, IDispatchService, ServiceTypes } from './schema';
2✔
16

17
/**
18
 * @private
19
 */
20
interface InternalBotConfig {
21
    location?: string;
22
}
23

24
/**
25
 * @deprecated See https://aka.ms/bot-file-basics for more information.
26
 */
27
export class BotConfiguration extends BotConfigurationBase {
2✔
28
    private internal: InternalBotConfig = {};
48✔
29

30
    /**
31
     * Load the bot configuration from a JSON.
32
     *
33
     * @param source JSON based configuration.
34
     * @returns A new BotConfiguration instance.
35
     */
36
    static fromJSON(source: Partial<IBotConfiguration> = {}): BotConfiguration {
×
37
        // tslint:disable-next-line:prefer-const
38
        const services: IConnectedService[] = source.services
40✔
39
            ? source.services.slice().map(BotConfigurationBase.serviceFromJSON)
40!
40
            : [];
41
        const botConfig: BotConfiguration = new BotConfiguration();
40✔
42
        Object.assign(botConfig, source);
40✔
43

44
        // back compat for secretKey rename
45
        if (!botConfig.padlock && (<any>botConfig).secretKey) {
40✔
46
            botConfig.padlock = (<any>botConfig).secretKey;
2✔
47
            delete (<any>botConfig).secretKey;
2✔
48
        }
49
        botConfig.services = services;
40✔
50
        botConfig.migrateData();
40✔
51

52
        return botConfig;
40✔
53
    }
54

55
    /**
56
     * Load the bot configuration by looking in a folder and loading the first .bot file in the
57
     * folder.
58
     *
59
     * @param folder (Optional) folder to look for bot files. If not specified the current working directory is used.
60
     * @param secret (Optional) secret used to decrypt the bot file.
61
     * @returns A Promise with the new BotConfiguration instance.
62
     */
63
    static async loadBotFromFolder(folder?: string, secret?: string): Promise<BotConfiguration> {
64
        folder = folder || process.cwd();
4!
65
        // eslint-disable-next-line security/detect-non-literal-fs-filename
66
        let files: string[] = await fsx.readdir(folder);
4✔
67
        files = files.sort();
4✔
68
        for (const file of files) {
4✔
69
            if (path.extname(<string>file) === '.bot') {
4✔
70
                return await BotConfiguration.load(`${folder}/${<string>file}`, secret);
4✔
71
            }
72
        }
73
        throw new Error(
×
74
            `Error: no bot file found in ${folder}. Choose a different location or use msbot init to create a .bot file."`
75
        );
76
    }
77

78
    /**
79
     * Load the bot configuration by looking in a folder and loading the first .bot file in the
80
     * folder. (blocking)
81
     *
82
     * @param folder (Optional) folder to look for bot files. If not specified the current working directory is used.
83
     * @param secret (Optional) secret used to decrypt the bot file.
84
     * @returns A new BotConfiguration instance.
85
     */
86
    static loadBotFromFolderSync(folder?: string, secret?: string): BotConfiguration {
87
        folder = folder || process.cwd();
2!
88
        // eslint-disable-next-line security/detect-non-literal-fs-filename
89
        let files: string[] = fsx.readdirSync(folder);
2✔
90
        files = files.sort();
2✔
91
        for (const file of files) {
2✔
92
            if (path.extname(<string>file) === '.bot') {
2✔
93
                return BotConfiguration.loadSync(`${folder}/${<string>file}`, secret);
2✔
94
            }
95
        }
96
        throw new Error(
×
97
            `Error: no bot file found in ${folder}. Choose a different location or use msbot init to create a .bot file."`
98
        );
99
    }
100

101
    /**
102
     * Load the configuration from a .bot file.
103
     *
104
     * @param botpath Path to bot file.
105
     * @param secret (Optional) secret used to decrypt the bot file.
106
     * @returns A Promise with the new BotConfiguration instance.
107
     */
108
    static async load(botpath: string, secret?: string): Promise<BotConfiguration> {
109
        // eslint-disable-next-line security/detect-non-literal-fs-filename
110
        const json: string = await fs.promises.readFile(botpath, 'utf8');
24✔
111
        const bot: BotConfiguration = BotConfiguration.internalLoad(json, secret);
24✔
112
        bot.internal.location = botpath;
20✔
113

114
        return bot;
20✔
115
    }
116

117
    /**
118
     * Load the configuration from a .bot file. (blocking)
119
     *
120
     * @param botpath Path to bot file.
121
     * @param secret (Optional) secret used to decrypt the bot file.
122
     * @returns A new BotConfiguration instance.
123
     */
124
    static loadSync(botpath: string, secret?: string): BotConfiguration {
125
        // eslint-disable-next-line security/detect-non-literal-fs-filename
126
        const json: string = fs.readFileSync(botpath, 'utf8');
16✔
127
        const bot: BotConfiguration = BotConfiguration.internalLoad(json, secret);
16✔
128
        bot.internal.location = botpath;
16✔
129

130
        return bot;
16✔
131
    }
132

133
    /**
134
     * Generate a new key suitable for encrypting.
135
     *
136
     * @returns Key to use with the [encrypt](xref:botframework-config.BotConfiguration.encrypt) method.
137
     */
138
    static generateKey(): string {
139
        return encrypt.generateKey();
8✔
140
    }
141

142
    /**
143
     * @private
144
     */
145
    private static internalLoad(json: string, secret?: string): BotConfiguration {
146
        const bot: BotConfiguration = BotConfiguration.fromJSON(JSON.parse(json));
40✔
147

148
        const hasSecret = !!bot.padlock;
40✔
149
        if (hasSecret) {
40✔
150
            bot.decrypt(secret);
12✔
151
        }
152

153
        return bot;
36✔
154
    }
155

156
    /**
157
     * Save the configuration to a .bot file.
158
     *
159
     * @param botpath Path to bot file.
160
     * @param secret (Optional) secret used to encrypt the bot file.
161
     */
162
    async saveAs(botpath: string, secret?: string): Promise<void> {
163
        if (!botpath) {
10✔
164
            throw new Error('missing path');
2✔
165
        }
166

167
        this.internal.location = botpath;
8✔
168

169
        this.savePrep(secret);
8✔
170

171
        const hasSecret = !!this.padlock;
8✔
172

173
        if (hasSecret) {
8✔
174
            this.encrypt(secret);
6✔
175
        }
176
        await fsx.writeJson(botpath, this.toJSON(), { spaces: 4 });
8✔
177

178
        if (hasSecret) {
8✔
179
            this.decrypt(secret);
6✔
180
        }
181
    }
182

183
    /**
184
     * Save the configuration to a .bot file. (blocking)
185
     *
186
     * @param botpath Path to bot file.
187
     * @param secret (Optional) secret used to encrypt the bot file.
188
     */
189
    saveAsSync(botpath: string, secret?: string): void {
190
        if (!botpath) {
6✔
191
            throw new Error('missing path');
2✔
192
        }
193
        this.internal.location = botpath;
4✔
194

195
        this.savePrep(secret);
4✔
196

197
        const hasSecret = !!this.padlock;
4✔
198

199
        if (hasSecret) {
4✔
200
            this.encrypt(secret);
4✔
201
        }
202

203
        fsx.writeJsonSync(botpath, this.toJSON(), { spaces: 4 });
4✔
204

205
        if (hasSecret) {
4✔
206
            this.decrypt(secret);
4✔
207
        }
208
    }
209

210
    /**
211
     * Save the file with secret.
212
     *
213
     * @param secret (Optional) secret used to encrypt the bot file.
214
     * @returns A promise representing the asynchronous operation.
215
     */
216
    async save(secret?: string): Promise<void> {
217
        return this.saveAs(this.internal.location, secret);
2✔
218
    }
219

220
    // eslint-disable-next-line jsdoc/require-returns
221
    /**
222
     * Save the file with secret. (blocking)
223
     *
224
     * @param secret (Optional) secret used to encrypt the bot file.
225
     */
226
    saveSync(secret?: string): void {
227
        return this.saveAsSync(this.internal.location, secret);
2✔
228
    }
229

230
    /**
231
     * Clear secret.
232
     */
233
    clearSecret(): void {
UNCOV
234
        this.padlock = '';
×
235
    }
236

237
    /**
238
     * Encrypt all values in the in memory config.
239
     *
240
     * @param secret Secret to encrypt.
241
     */
242
    encrypt(secret: string): void {
243
        this.validateSecret(secret);
12✔
244

245
        for (const service of this.services) {
12✔
246
            (<ConnectedService>service).encrypt(secret, encrypt.encryptString);
132✔
247
        }
248
    }
249

250
    /**
251
     * Decrypt all values in the in memory config.
252
     *
253
     * @param secret Secret to decrypt.
254
     */
255
    decrypt(secret?: string): void {
256
        try {
22✔
257
            this.validateSecret(secret);
22✔
258

259
            for (const connected_service of this.services) {
18✔
260
                (<ConnectedService>connected_service).decrypt(secret, encrypt.decryptString);
198✔
261
            }
262
        } catch (err) {
263
            try {
4✔
264
                // legacy decryption
265
                this.padlock = encrypt.legacyDecrypt(this.padlock, secret);
4✔
UNCOV
266
                this.clearSecret();
×
UNCOV
267
                this.version = '2.0';
×
268

UNCOV
269
                const encryptedProperties: { [key: string]: string[] } = {
×
270
                    abs: [],
271
                    endpoint: ['appPassword'],
272
                    luis: ['authoringKey', 'subscriptionKey'],
273
                    dispatch: ['authoringKey', 'subscriptionKey'],
274
                    file: [],
275
                    qna: ['subscriptionKey'],
276
                };
277

UNCOV
278
                for (const service of this.services) {
×
UNCOV
279
                    for (const prop of encryptedProperties[service.type]) {
×
UNCOV
280
                        const val: string = <string>(<any>service)[prop];
×
UNCOV
281
                        (<any>service)[prop] = encrypt.legacyDecrypt(val, secret);
×
282
                    }
283
                }
284

285
                // assign new ids
286

287
                // map old ids -> new Ids
UNCOV
288
                const map: any = {};
×
289

UNCOV
290
                const oldServices: IConnectedService[] = this.services;
×
UNCOV
291
                this.services = [];
×
UNCOV
292
                for (const oldService of oldServices) {
×
293
                    // connecting causes new ids to be created
UNCOV
294
                    const newServiceId: string = this.connectService(oldService);
×
UNCOV
295
                    map[oldService.id] = newServiceId;
×
296
                }
297

298
                // fix up dispatch serviceIds to new ids
UNCOV
299
                for (const service of this.services) {
×
UNCOV
300
                    if (service.type === ServiceTypes.Dispatch) {
×
301
                        const dispatch: IDispatchService = <IDispatchService>service;
×
302
                        for (let i = 0; i < dispatch.serviceIds.length; i++) {
×
303
                            dispatch.serviceIds[i] = map[dispatch.serviceIds[i]];
×
304
                        }
305
                    }
306
                }
307
            } catch (legacyErr) {
308
                if (legacyErr.message.includes('Node.js versions')) {
4✔
309
                    throw legacyErr;
4✔
310
                }
UNCOV
311
                throw err;
×
312
            }
313
        }
314
    }
315

316
    /**
317
     * Gets the path that this config was loaded from.  .save() will save to this path.
318
     *
319
     * @returns The path.
320
     */
321
    getPath(): string {
322
        return this.internal.location;
2✔
323
    }
324

325
    /**
326
     * Make sure secret is correct by decrypting the secretKey with it.
327
     *
328
     * @param secret Secret to use.
329
     */
330
    validateSecret(secret: string): void {
331
        if (!secret) {
44✔
332
            throw new Error(
2✔
333
                'You are attempting to perform an operation which needs access to the secret and --secret is missing'
334
            );
335
        }
336

337
        try {
42✔
338
            if (!this.padlock || this.padlock.length === 0) {
42✔
339
                // if no key, create a guid and enrypt that to use as secret validator
340
                this.padlock = encrypt.encryptString(uuidv4(), secret);
6✔
341
            } else {
342
                // validate we can decrypt the padlock, this tells us we have the correct secret for the rest of the file.
343
                encrypt.decryptString(this.padlock, secret);
36✔
344
            }
345
        } catch {
346
            throw new Error(
2✔
347
                'You are attempting to perform an operation which needs access to the secret and --secret is incorrect.'
348
            );
349
        }
350
    }
351

352
    /**
353
     * @private
354
     */
355
    private savePrep(secret?: string): void {
356
        if (secret) {
12✔
357
            this.validateSecret(secret);
10✔
358
        }
359

360
        // make sure that all dispatch serviceIds still match services that are in the bot
361
        for (const service of this.services) {
12✔
362
            if (service.type === ServiceTypes.Dispatch) {
132✔
363
                const dispatchService: IDispatchService = <IDispatchService>service;
12✔
364
                const validServices: string[] = [];
12✔
365
                for (const dispatchServiceId of dispatchService.serviceIds) {
12✔
366
                    for (const this_service of this.services) {
24✔
367
                        if (this_service.id === dispatchServiceId) {
264✔
368
                            validServices.push(dispatchServiceId);
24✔
369
                        }
370
                    }
371
                }
372
                dispatchService.serviceIds = validServices;
12✔
373
            }
374
        }
375
    }
376
}
377

378
// Make sure the internal field is not included in JSON representation.
379
Object.defineProperty(BotConfiguration.prototype, 'internal', { enumerable: false, writable: true });
2✔
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