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

microsoft / botbuilder-js / 12258997087

10 Dec 2024 03:08PM UTC coverage: 84.277% (-0.4%) from 84.686%
12258997087

push

github

web-flow
feat: Add support for Node 22 (#4808)

* Use tsup to compile ESM-only filenamify package

* Update yamls to include node 22

* Throw error on crypto.createDecipher for node 22

* Add NODE_NO_WARNINGS flag to bf dialogs:merge exec

* Apply feedback

* Update unit tests

8149 of 10821 branches covered (75.31%)

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.

20431 of 23091 relevant lines covered (88.48%)

3465.04 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';
1✔
8
import * as fs from 'fs';
1✔
9
import * as path from 'path';
1✔
10
import * as process from 'process';
1✔
11
import { v4 as uuidv4 } from 'uuid';
1✔
12
import { BotConfigurationBase } from './botConfigurationBase';
1✔
13
import * as encrypt from './encrypt';
1✔
14
import { ConnectedService } from './models';
15
import { IBotConfiguration, IConnectedService, IDispatchService, ServiceTypes } from './schema';
1✔
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 {
1✔
28
    private internal: InternalBotConfig = {};
24✔
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
20✔
39
            ? source.services.slice().map(BotConfigurationBase.serviceFromJSON)
20!
40
            : [];
41
        const botConfig: BotConfiguration = new BotConfiguration();
20✔
42
        Object.assign(botConfig, source);
20✔
43

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

52
        return botConfig;
20✔
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();
2!
65
        // eslint-disable-next-line security/detect-non-literal-fs-filename
66
        let files: string[] = await fsx.readdir(folder);
2✔
67
        files = files.sort();
2✔
68
        for (const file of files) {
2✔
69
            if (path.extname(<string>file) === '.bot') {
2✔
70
                return await BotConfiguration.load(`${folder}/${<string>file}`, secret);
2✔
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();
1!
88
        // eslint-disable-next-line security/detect-non-literal-fs-filename
89
        let files: string[] = fsx.readdirSync(folder);
1✔
90
        files = files.sort();
1✔
91
        for (const file of files) {
1✔
92
            if (path.extname(<string>file) === '.bot') {
1✔
93
                return BotConfiguration.loadSync(`${folder}/${<string>file}`, secret);
1✔
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');
12✔
111
        const bot: BotConfiguration = BotConfiguration.internalLoad(json, secret);
12✔
112
        bot.internal.location = botpath;
10✔
113

114
        return bot;
10✔
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');
8✔
127
        const bot: BotConfiguration = BotConfiguration.internalLoad(json, secret);
8✔
128
        bot.internal.location = botpath;
8✔
129

130
        return bot;
8✔
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();
4✔
140
    }
141

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

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

153
        return bot;
18✔
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) {
5✔
164
            throw new Error('missing path');
1✔
165
        }
166

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

169
        this.savePrep(secret);
4✔
170

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

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

178
        if (hasSecret) {
4✔
179
            this.decrypt(secret);
3✔
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) {
3✔
191
            throw new Error('missing path');
1✔
192
        }
193
        this.internal.location = botpath;
2✔
194

195
        this.savePrep(secret);
2✔
196

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

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

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

205
        if (hasSecret) {
2✔
206
            this.decrypt(secret);
2✔
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);
1✔
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);
1✔
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);
6✔
244

245
        for (const service of this.services) {
6✔
246
            (<ConnectedService>service).encrypt(secret, encrypt.encryptString);
66✔
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 {
11✔
257
            this.validateSecret(secret);
11✔
258

259
            for (const connected_service of this.services) {
9✔
260
                (<ConnectedService>connected_service).decrypt(secret, encrypt.decryptString);
99✔
261
            }
262
        } catch (err) {
263
            try {
2✔
264
                // legacy decryption
265
                this.padlock = encrypt.legacyDecrypt(this.padlock, secret);
2✔
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')) {
2✔
309
                    throw legacyErr;
2✔
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;
1✔
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) {
22✔
332
            throw new Error(
1✔
333
                'You are attempting to perform an operation which needs access to the secret and --secret is missing'
334
            );
335
        }
336

337
        try {
21✔
338
            if (!this.padlock || this.padlock.length === 0) {
21✔
339
                // if no key, create a guid and enrypt that to use as secret validator
340
                this.padlock = encrypt.encryptString(uuidv4(), secret);
3✔
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);
18✔
344
            }
345
        } catch {
346
            throw new Error(
1✔
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) {
6✔
357
            this.validateSecret(secret);
5✔
358
        }
359

360
        // make sure that all dispatch serviceIds still match services that are in the bot
361
        for (const service of this.services) {
6✔
362
            if (service.type === ServiceTypes.Dispatch) {
66✔
363
                const dispatchService: IDispatchService = <IDispatchService>service;
6✔
364
                const validServices: string[] = [];
6✔
365
                for (const dispatchServiceId of dispatchService.serviceIds) {
6✔
366
                    for (const this_service of this.services) {
12✔
367
                        if (this_service.id === dispatchServiceId) {
132✔
368
                            validServices.push(dispatchServiceId);
12✔
369
                        }
370
                    }
371
                }
372
                dispatchService.serviceIds = validServices;
6✔
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 });
1✔
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