auth0-deploy-cli 8.42.0 → 8.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/lib/context/defaults.d.ts +7 -0
  3. package/lib/context/defaults.js +74 -30
  4. package/lib/context/directory/handlers/actionModules.js +11 -12
  5. package/lib/context/directory/handlers/actions.js +11 -13
  6. package/lib/context/directory/handlers/attackProtection.js +1 -1
  7. package/lib/context/directory/handlers/branding.js +9 -1
  8. package/lib/context/directory/handlers/clients.js +9 -3
  9. package/lib/context/directory/handlers/connections.js +10 -4
  10. package/lib/context/directory/handlers/databases.js +4 -4
  11. package/lib/context/directory/handlers/emailTemplates.js +9 -0
  12. package/lib/context/directory/handlers/hooks.js +12 -1
  13. package/lib/context/directory/handlers/prompts.js +11 -3
  14. package/lib/context/directory/handlers/rules.js +12 -1
  15. package/lib/context/directory/index.js +7 -8
  16. package/lib/context/yaml/handlers/attackProtection.js +1 -1
  17. package/lib/context/yaml/handlers/branding.js +8 -1
  18. package/lib/context/yaml/handlers/clients.js +4 -1
  19. package/lib/context/yaml/handlers/connections.js +3 -1
  20. package/lib/context/yaml/handlers/flows.js +7 -0
  21. package/lib/context/yaml/handlers/forms.js +7 -0
  22. package/lib/context/yaml/handlers/prompts.js +7 -1
  23. package/lib/context/yaml/index.js +7 -8
  24. package/lib/readonly.js +5 -0
  25. package/lib/tools/auth0/handlers/attackProtection.js +3 -1
  26. package/lib/tools/auth0/handlers/clients.d.ts +21 -0
  27. package/lib/tools/auth0/handlers/clients.js +21 -0
  28. package/lib/tools/auth0/handlers/connectionProfiles.d.ts +27 -0
  29. package/lib/tools/auth0/handlers/connectionProfiles.js +30 -0
  30. package/lib/tools/auth0/handlers/connections.d.ts +11 -6
  31. package/lib/tools/auth0/handlers/connections.js +33 -7
  32. package/lib/tools/auth0/handlers/databases.js +12 -3
  33. package/lib/tools/auth0/handlers/default.d.ts +23 -0
  34. package/lib/tools/auth0/handlers/default.js +21 -10
  35. package/lib/tools/auth0/handlers/organizations.d.ts +29 -1
  36. package/lib/tools/auth0/handlers/organizations.js +165 -17
  37. package/lib/tools/auth0/handlers/phoneProvider.js +10 -2
  38. package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
  39. package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
  40. package/lib/tools/auth0/handlers/roles.d.ts +4 -0
  41. package/lib/tools/auth0/handlers/roles.js +10 -0
  42. package/lib/tools/calculateDryRunChanges.js +12 -8
  43. package/lib/types.d.ts +1 -0
  44. package/lib/utils.js +5 -2
  45. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [8.44.0] - 2026-08-26
11
+
12
+ ### Added
13
+
14
+ - Support `third_party_client_access` and `cross_app_access_resource_app` on clients and connections. [#1471]
15
+
16
+ ### Fixed
17
+
18
+ - Warn on path traversal in file references and fix double-path resolution in YAML connections handler. [#1477]
19
+ - Strip `form_submission_mode` and `form_submission_behavior` from `bruteForceProtection` on export and import. [#1476]
20
+ - Include empty credentials for custom phone provider on import and export to prevent 400 errors. [#1474]
21
+ - Preserve keyword placeholder strings for all email provider credentials. [#1470]
22
+ - Preserve `idpinitiated.enabled:false` on SAML connection export instead of emitting `undefined_clientId`. [#1465]
23
+ - Apply 429 backoff to organization connection writes to prevent rate-limit failures. [#1464]
24
+ - Resolve dry-run reporting bugs across multiple asset types. [#1462]
25
+ - Skip organization PATCH when no top-level properties have changed. [#1461]
26
+ - Sort `attackProtection` arrays on export to prevent spurious diffs. [#1460]
27
+
28
+ ## [8.43.0] - 2026-08-12
29
+
30
+ ### Added
31
+
32
+ - Add org-to-app entitlement (EA) support for organizations. [#1454]
33
+ - Support full group metadata for Google Workspace directory sync (GA). [#1456]
34
+
35
+ ### Fixed
36
+
37
+ - Create phone templates on import instead of skipping when they have no ID. [#1457]
38
+ - Address path traversal vulnerability and false warnings in config file handlers. [#1449]
39
+ - Exclude org-scoped roles from tenant export by filtering `type='tenant'`. [#1453]
40
+ - Strip read-only field from roles to fix export/import round-trip. [#1445]
41
+
10
42
  ## [8.42.0] - 2026-07-30
11
43
 
12
44
  ### Added
@@ -1907,7 +1939,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1907
1939
  [#1436]: https://github.com/auth0/auth0-deploy-cli/issues/1436
1908
1940
  [#1437]: https://github.com/auth0/auth0-deploy-cli/issues/1437
1909
1941
  [#1438]: https://github.com/auth0/auth0-deploy-cli/issues/1438
1910
- [Unreleased]: https://github.com/auth0/auth0-deploy-cli/compare/v8.42.0...HEAD
1942
+ [#1445]: https://github.com/auth0/auth0-deploy-cli/issues/1445
1943
+ [#1449]: https://github.com/auth0/auth0-deploy-cli/issues/1449
1944
+ [#1453]: https://github.com/auth0/auth0-deploy-cli/issues/1453
1945
+ [#1454]: https://github.com/auth0/auth0-deploy-cli/issues/1454
1946
+ [#1456]: https://github.com/auth0/auth0-deploy-cli/issues/1456
1947
+ [#1457]: https://github.com/auth0/auth0-deploy-cli/issues/1457
1948
+ [#1460]: https://github.com/auth0/auth0-deploy-cli/issues/1460
1949
+ [#1461]: https://github.com/auth0/auth0-deploy-cli/issues/1461
1950
+ [#1462]: https://github.com/auth0/auth0-deploy-cli/issues/1462
1951
+ [#1464]: https://github.com/auth0/auth0-deploy-cli/issues/1464
1952
+ [#1465]: https://github.com/auth0/auth0-deploy-cli/issues/1465
1953
+ [#1470]: https://github.com/auth0/auth0-deploy-cli/issues/1470
1954
+ [#1471]: https://github.com/auth0/auth0-deploy-cli/issues/1471
1955
+ [#1474]: https://github.com/auth0/auth0-deploy-cli/issues/1474
1956
+ [#1476]: https://github.com/auth0/auth0-deploy-cli/issues/1476
1957
+ [#1477]: https://github.com/auth0/auth0-deploy-cli/issues/1477
1958
+ [Unreleased]: https://github.com/auth0/auth0-deploy-cli/compare/v8.44.0...HEAD
1959
+ [8.44.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.43.0...v8.44.0
1960
+ [8.43.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.42.0...v8.43.0
1911
1961
  [8.42.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.41.0...v8.42.0
1912
1962
  [8.41.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.40.0...v8.41.0
1913
1963
  [8.40.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.39.0...v8.40.0
@@ -6,4 +6,11 @@ export declare function phoneTemplatesDefaults(phoneTemplate: any): any;
6
6
  export declare function connectionDefaults(connection: any, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any;
7
7
  export declare function logStreamDefaults(logStreams: any, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any;
8
8
  export declare function eventStreamDefaults(eventStreams: any[], config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): any[];
9
+ /**
10
+ * Sorts primitive arrays (e.g. `shields`, `admin_notification_frequency`) within the
11
+ * attackProtection subtree in place, so repeated exports produce deterministic output.
12
+ * These are unordered, string-typed enum sets — ordering is cosmetic. Object arrays are
13
+ * left as-is.
14
+ */
15
+ export declare function sortAttackProtectionArrays(attackProtection: AttackProtection): AttackProtection;
9
16
  export declare function attackProtectionDefaults(attackProtection: AttackProtection, config?: Pick<Config, 'AUTH0_EXPORT_SECRETS'>): AttackProtection;
@@ -6,6 +6,7 @@ exports.phoneTemplatesDefaults = phoneTemplatesDefaults;
6
6
  exports.connectionDefaults = connectionDefaults;
7
7
  exports.logStreamDefaults = logStreamDefaults;
8
8
  exports.eventStreamDefaults = eventStreamDefaults;
9
+ exports.sortAttackProtectionArrays = sortAttackProtectionArrays;
9
10
  exports.attackProtectionDefaults = attackProtectionDefaults;
10
11
  const utils_1 = require("../tools/utils");
11
12
  // env vars arrive as strings, so check both forms
@@ -22,45 +23,57 @@ function emailProviderDefaults(emailProvider, config) {
22
23
  // Add placeholder for credentials as they cannot be exported
23
24
  const { name } = updated;
24
25
  if (apiKeyProviders.includes(name)) {
25
- updated.credentials = {
26
- api_key: `##${name.toUpperCase()}_API_KEY##`,
27
- ...(updated.credentials || {}),
28
- };
26
+ if (typeof updated.credentials !== 'string') {
27
+ updated.credentials = {
28
+ api_key: `##${name.toUpperCase()}_API_KEY##`,
29
+ ...(updated.credentials || {}),
30
+ };
31
+ }
29
32
  }
30
33
  if (name === 'smtp') {
31
- // This is to mask smtp_user to '##SMTP_USER##'
32
- if (updated.credentials && 'smtp_user' in updated.credentials) {
33
- delete updated.credentials.smtp_user;
34
+ // If credentials is a keyword placeholder string (e.g. @@SMTP_CREDENTIALS@@), preserve it as-is.
35
+ // The `in` operator requires an object and would throw a TypeError on a string.
36
+ if (typeof updated.credentials !== 'string') {
37
+ // This is to mask smtp_user to '##SMTP_USER##'
38
+ if (updated.credentials && 'smtp_user' in updated.credentials) {
39
+ delete updated.credentials.smtp_user;
40
+ }
41
+ updated.credentials = {
42
+ smtp_host: '##SMTP_HOSTNAME##',
43
+ smtp_port: '##SMTP_PORT##',
44
+ smtp_user: '##SMTP_USER##',
45
+ smtp_pass: '##SMTP_PASS##',
46
+ ...(updated.credentials || {}),
47
+ };
34
48
  }
35
- updated.credentials = {
36
- smtp_host: '##SMTP_HOSTNAME##',
37
- smtp_port: '##SMTP_PORT##',
38
- smtp_user: '##SMTP_USER##',
39
- smtp_pass: '##SMTP_PASS##',
40
- ...(updated.credentials || {}),
41
- };
42
49
  }
43
50
  if (name === 'ses') {
44
- updated.credentials = {
45
- accessKeyId: '##SES_ACCESS_KEY_ID##',
46
- secretAccessKey: '##SES_ACCESS_SECRET_KEY##',
47
- region: '##SES_AWS_REGION##',
48
- ...(updated.credentials || {}),
49
- };
51
+ if (typeof updated.credentials !== 'string') {
52
+ updated.credentials = {
53
+ accessKeyId: '##SES_ACCESS_KEY_ID##',
54
+ secretAccessKey: '##SES_ACCESS_SECRET_KEY##',
55
+ region: '##SES_AWS_REGION##',
56
+ ...(updated.credentials || {}),
57
+ };
58
+ }
50
59
  }
51
60
  if (name === 'azure_cs') {
52
- updated.credentials = {
53
- connectionString: '##AZURE_CS_CONNECTION_KEY##',
54
- ...(updated.credentials || {}),
55
- };
61
+ if (typeof updated.credentials !== 'string') {
62
+ updated.credentials = {
63
+ connectionString: '##AZURE_CS_CONNECTION_KEY##',
64
+ ...(updated.credentials || {}),
65
+ };
66
+ }
56
67
  }
57
68
  if (name === 'ms365') {
58
- updated.credentials = {
59
- tenantId: '##MS365_TENANT_ID##',
60
- clientId: '##MS365_CLIENT_ID##',
61
- clientSecret: '##MS365_CLIENT_SECRET##',
62
- ...(updated.credentials || {}),
63
- };
69
+ if (typeof updated.credentials !== 'string') {
70
+ updated.credentials = {
71
+ tenantId: '##MS365_TENANT_ID##',
72
+ clientId: '##MS365_CLIENT_ID##',
73
+ clientSecret: '##MS365_CLIENT_SECRET##',
74
+ ...(updated.credentials || {}),
75
+ };
76
+ }
64
77
  }
65
78
  return updated;
66
79
  }
@@ -87,6 +100,12 @@ function phoneProviderDefaults(phoneProvider) {
87
100
  auth_token: `##${name.toUpperCase()}_AUTH_TOKEN##`,
88
101
  };
89
102
  }
103
+ else if (name === 'custom') {
104
+ // The `custom` provider has no exportable secrets, but the API requires a
105
+ // `credentials` object on import even when empty. Emit an empty object so
106
+ // exported configs round-trip cleanly.
107
+ updated.credentials = {};
108
+ }
90
109
  return updated;
91
110
  }
92
111
  function phoneTemplatesDefaults(phoneTemplate) {
@@ -193,6 +212,31 @@ function eventStreamDefaults(eventStreams, config) {
193
212
  };
194
213
  });
195
214
  }
215
+ /**
216
+ * Sorts primitive arrays (e.g. `shields`, `admin_notification_frequency`) within the
217
+ * attackProtection subtree in place, so repeated exports produce deterministic output.
218
+ * These are unordered, string-typed enum sets — ordering is cosmetic. Object arrays are
219
+ * left as-is.
220
+ */
221
+ function sortAttackProtectionArrays(attackProtection) {
222
+ const sortArraysDeep = (value) => {
223
+ if (Array.isArray(value)) {
224
+ const isPrimitiveArray = value.every((item) => item === null || typeof item !== 'object');
225
+ if (isPrimitiveArray) {
226
+ value.sort();
227
+ }
228
+ else {
229
+ value.forEach(sortArraysDeep);
230
+ }
231
+ return;
232
+ }
233
+ if (value && typeof value === 'object') {
234
+ Object.values(value).forEach(sortArraysDeep);
235
+ }
236
+ };
237
+ sortArraysDeep(attackProtection);
238
+ return attackProtection;
239
+ }
196
240
  function attackProtectionDefaults(attackProtection, config) {
197
241
  if (isExportSecrets(config))
198
242
  return attackProtection;
@@ -20,20 +20,19 @@ function parse(context) {
20
20
  disableKeywordReplacement: context.disableKeywordReplacement,
21
21
  }),
22
22
  };
23
- const moduleFolder = path_1.default.join(tools_1.constants.ACTION_MODULES_DIRECTORY, `${module.name}`);
24
23
  if (module.code) {
25
- // The `module.code` can be a file path. It needs to be loaded.
26
- // It can be a relative path, so we need to handle both cases.
27
- const unixPath = module.code.replace(/[\\/]+/g, '/').replace(/^([a-zA-Z]+:|\.\/)/, '');
28
- if (fs_extra_1.default.existsSync(unixPath)) {
29
- logger_1.default.warn(`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
30
- `Please update your configuration to use paths relative to the config directory. ` +
31
- `Current absolute path used: ["${module.code}"]`);
32
- module.code = context.loadFile(unixPath, moduleFolder);
33
- }
34
- else {
35
- module.code = context.loadFile(path_1.default.join(context.filePath, module.code), moduleFolder);
24
+ const normalizedCode = module.code.replace(/\\/g, '/');
25
+ const configRoot = path_1.default.resolve(context.filePath);
26
+ const resolvedPath = path_1.default.resolve(context.filePath, normalizedCode);
27
+ if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
28
+ logger_1.default.warn(`Path "${module.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
29
+ `This will be blocked as an error in the next major release. ` +
30
+ `Move the file inside your config directory.`);
36
31
  }
32
+ module.code = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
33
+ mappings: context.mappings,
34
+ disableKeywordReplacement: context.disableKeywordReplacement,
35
+ });
37
36
  }
38
37
  return module;
39
38
  });
@@ -22,21 +22,19 @@ function parse(context) {
22
22
  disableKeywordReplacement: context.disableKeywordReplacement,
23
23
  }),
24
24
  };
25
- const actionFolder = path_1.default.join(tools_1.constants.ACTIONS_DIRECTORY, `${action.name}`);
26
25
  if (action.code) {
27
- // Convert `action.code` path to Unix-style path by replacing backslashes and multiple slashes with a single forward slash, and remove leading drive letters or './'.
28
- const unixPath = action.code.replace(/[\\/]+/g, '/').replace(/^([a-zA-Z]+:|\.\/)/, '');
29
- if (fs_extra_1.default.existsSync(unixPath)) {
30
- // If the Unix-style path exists, load the file from that path
31
- logger_1.default.warn(`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
32
- `Please update your configuration to use paths relative to the config directory. ` +
33
- `Current absolute path used: ["${action.code}"]`);
34
- action.code = context.loadFile(unixPath, actionFolder);
35
- }
36
- else {
37
- // Otherwise, load the file from the context's file path
38
- action.code = context.loadFile(path_1.default.join(context.filePath, action.code), actionFolder);
26
+ const normalizedCode = action.code.replace(/\\/g, '/');
27
+ const configRoot = path_1.default.resolve(context.filePath);
28
+ const resolvedPath = path_1.default.resolve(context.filePath, normalizedCode);
29
+ if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
30
+ logger_1.default.warn(`Path "${action.code}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
31
+ `This will be blocked as an error in the next major release. ` +
32
+ `Move the file inside your config directory.`);
39
33
  }
34
+ action.code = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
35
+ mappings: context.mappings,
36
+ disableKeywordReplacement: context.disableKeywordReplacement,
37
+ });
40
38
  }
41
39
  return action;
42
40
  });
@@ -65,7 +65,7 @@ async function dump(context) {
65
65
  return;
66
66
  const files = attackProtectionFiles(context.filePath);
67
67
  fs_extra_1.default.ensureDirSync(files.directory);
68
- const maskedAttackProtection = (0, defaults_1.attackProtectionDefaults)(attackProtection, context.config);
68
+ const maskedAttackProtection = (0, defaults_1.sortAttackProtectionArrays)((0, defaults_1.attackProtectionDefaults)(attackProtection, context.config));
69
69
  if (maskedAttackProtection.botDetection) {
70
70
  (0, utils_1.dumpJSON)(files.botDetection, maskedAttackProtection.botDetection);
71
71
  }
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const path_1 = __importDefault(require("path"));
7
7
  const fs_extra_1 = __importDefault(require("fs-extra"));
8
8
  const tools_1 = require("../../../tools");
9
+ const logger_1 = __importDefault(require("../../../logger"));
9
10
  const utils_1 = require("../../../utils");
10
11
  function parse(context) {
11
12
  const brandingDirectory = path_1.default.join(context.filePath, tools_1.constants.BRANDING_DIRECTORY);
@@ -31,7 +32,14 @@ function parse(context) {
31
32
  disableKeywordReplacement: context.disableKeywordReplacement,
32
33
  });
33
34
  const normalizedPathArray = (0, utils_1.nomalizedYAMLPath)(definition.body);
34
- definition.body = (0, tools_1.loadFileAndReplaceKeywords)(path_1.default.join(brandingTemplatesFolder, ...normalizedPathArray), {
35
+ const resolvedBodyFile = path_1.default.resolve(brandingTemplatesFolder, ...normalizedPathArray);
36
+ const configRoot = path_1.default.resolve(context.filePath);
37
+ if (!resolvedBodyFile.startsWith(configRoot + path_1.default.sep)) {
38
+ logger_1.default.warn(`Branding template body path "${definition.body}" resolves to "${resolvedBodyFile}" which is outside the config directory "${configRoot}". ` +
39
+ `This will be blocked as an error in the next major release. ` +
40
+ `Move the file inside your config directory.`);
41
+ }
42
+ definition.body = (0, tools_1.loadFileAndReplaceKeywords)(resolvedBodyFile, {
35
43
  mappings: context.mappings,
36
44
  disableKeywordReplacement: context.disableKeywordReplacement,
37
45
  });
@@ -20,9 +20,15 @@ function parse(context) {
20
20
  disableKeywordReplacement: context.disableKeywordReplacement,
21
21
  });
22
22
  if (client.custom_login_page) {
23
- const htmlFileName = path_1.default.join(clientsFolder, client.custom_login_page);
24
- if ((0, utils_1.isFile)(htmlFileName)) {
25
- client.custom_login_page = (0, tools_1.loadFileAndReplaceKeywords)(htmlFileName, {
23
+ const configRoot = path_1.default.resolve(context.filePath);
24
+ const resolvedLoginPage = path_1.default.resolve(clientsFolder, client.custom_login_page);
25
+ if ((0, utils_1.isFile)(resolvedLoginPage)) {
26
+ if (!resolvedLoginPage.startsWith(configRoot + path_1.default.sep)) {
27
+ logger_1.default.warn(`Path "${client.custom_login_page}" resolves to "${resolvedLoginPage}" which is outside the config directory "${configRoot}". ` +
28
+ `This will be blocked as an error in the next major release. ` +
29
+ `Move the file inside your config directory.`);
30
+ }
31
+ client.custom_login_page = (0, tools_1.loadFileAndReplaceKeywords)(resolvedLoginPage, {
26
32
  mappings: context.mappings,
27
33
  disableKeywordReplacement: context.disableKeywordReplacement,
28
34
  });
@@ -23,11 +23,17 @@ function parse(context) {
23
23
  });
24
24
  if (connection.strategy === 'email') {
25
25
  (0, utils_1.ensureProp)(connection, 'options.email.body');
26
- const htmlFileName = path_1.default.join(connectionsFolder, connection.options.email.body);
27
- if (!(0, utils_1.isFile)(htmlFileName)) {
28
- throw new Error(`Passwordless email template purportedly located at ${htmlFileName} does not exist for connection. Ensure the existence of this file to proceed with deployment.`);
26
+ const configRoot = path_1.default.resolve(context.filePath);
27
+ const resolvedHtmlFile = path_1.default.resolve(connectionsFolder, connection.options.email.body);
28
+ if (!(0, utils_1.isFile)(resolvedHtmlFile)) {
29
+ throw new Error(`Passwordless email template purportedly located at ${resolvedHtmlFile} does not exist for connection. Ensure the existence of this file to proceed with deployment.`);
29
30
  }
30
- connection.options.email.body = (0, tools_1.loadFileAndReplaceKeywords)(htmlFileName, {
31
+ if (!resolvedHtmlFile.startsWith(configRoot + path_1.default.sep)) {
32
+ logger_1.default.warn(`Path "${connection.options.email.body}" resolves to "${resolvedHtmlFile}" which is outside the config directory "${configRoot}". ` +
33
+ `This will be blocked as an error in the next major release. ` +
34
+ `Move the file inside your config directory.`);
35
+ }
36
+ connection.options.email.body = (0, tools_1.loadFileAndReplaceKeywords)(resolvedHtmlFile, {
31
37
  mappings: context.mappings,
32
38
  disableKeywordReplacement: context.disableKeywordReplacement,
33
39
  });
@@ -41,11 +41,11 @@ function getDatabase(folder, configRoot, mappingOpts) {
41
41
  }
42
42
  else {
43
43
  const resolvedBase = path_1.default.resolve(configRoot);
44
- const toLoad = path_1.default.resolve(folder, script);
44
+ const toLoad = path_1.default.resolve(folder, script.replace(/\\/g, '/'));
45
45
  if (!toLoad.startsWith(resolvedBase + path_1.default.sep)) {
46
- logger_1.default.warn(`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
47
- `Please update your configuration to use paths relative to the config directory. ` +
48
- `Current absolute path used: ["${script}"]`);
46
+ logger_1.default.warn(`Path "${script}" resolves to "${toLoad}" which is outside the config directory "${resolvedBase}". ` +
47
+ `This will be blocked as an error in the next major release. ` +
48
+ `Move the file inside your config directory.`);
49
49
  }
50
50
  database.options.customScripts[name] = (0, tools_1.loadFileAndReplaceKeywords)(toLoad, mappingOpts);
51
51
  }
@@ -34,6 +34,15 @@ function parse(context) {
34
34
  logger_1.default.warn(`Skipping email template file ${meta.body} as missing the corresponding '.json' file`);
35
35
  return [];
36
36
  }
37
+ if (meta.body !== undefined) {
38
+ const configRoot = path_1.default.resolve(context.filePath);
39
+ const resolvedTemplatePath = path_1.default.resolve(templateFilePath);
40
+ if (!resolvedTemplatePath.startsWith(configRoot + path_1.default.sep)) {
41
+ logger_1.default.warn(`Email template body path "${meta.body}" resolves to "${resolvedTemplatePath}" which is outside the config directory "${configRoot}". ` +
42
+ `This will be blocked as an error in the next major release. ` +
43
+ `Move the file inside your config directory.`);
44
+ }
45
+ }
37
46
  return {
38
47
  ...meta,
39
48
  body: (0, tools_1.loadFileAndReplaceKeywords)(templateFilePath, {
@@ -21,7 +21,18 @@ function parse(context) {
21
21
  }),
22
22
  };
23
23
  if (hook.script) {
24
- hook.script = context.loadFile(hook.script, tools_1.constants.HOOKS_DIRECTORY);
24
+ const normalizedScript = hook.script.replace(/\\/g, '/');
25
+ const configRoot = path_1.default.resolve(context.filePath);
26
+ const resolvedPath = path_1.default.resolve(context.filePath, tools_1.constants.HOOKS_DIRECTORY, normalizedScript);
27
+ if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
28
+ logger_1.default.warn(`Path "${hook.script}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
29
+ `This will be blocked as an error in the next major release. ` +
30
+ `Move the file inside your config directory.`);
31
+ }
32
+ hook.script = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
33
+ mappings: context.mappings,
34
+ disableKeywordReplacement: context.disableKeywordReplacement,
35
+ });
25
36
  }
26
37
  hook.name = hook.name.toLowerCase().replace(/\s/g, '-');
27
38
  return hook;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const path_1 = __importDefault(require("path"));
7
7
  const fs_extra_1 = require("fs-extra");
8
8
  const tools_1 = require("../../../tools");
9
+ const logger_1 = __importDefault(require("../../../logger"));
9
10
  const utils_1 = require("../../../utils");
10
11
  const getPromptsDirectory = (filePath) => path_1.default.join(filePath, tools_1.constants.PROMPTS_DIRECTORY);
11
12
  const getPromptsSettingsFile = (promptsDirectory) => path_1.default.join(promptsDirectory, 'prompts.json');
@@ -46,9 +47,16 @@ function parse(context) {
46
47
  const screensObject = screensArray[0];
47
48
  acc[promptName] = Object.entries(screensObject).reduce((screenAcc, [screenName, items]) => {
48
49
  screenAcc[screenName] = items.reduce((insertionAcc, { name, template }) => {
49
- const templateFilePath = path_1.default.join(promptsDirectory, template);
50
- insertionAcc[name] = (0, utils_1.isFile)(templateFilePath)
51
- ? (0, tools_1.loadFileAndReplaceKeywords)(templateFilePath, {
50
+ const configRoot = path_1.default.resolve(context.filePath);
51
+ const resolvedTemplatePath = path_1.default.resolve(promptsDirectory, template);
52
+ if ((0, utils_1.isFile)(resolvedTemplatePath) &&
53
+ !resolvedTemplatePath.startsWith(configRoot + path_1.default.sep)) {
54
+ logger_1.default.warn(`Prompt partial template "${template}" resolves to "${resolvedTemplatePath}" which is outside the config directory "${configRoot}". ` +
55
+ `This will be blocked as an error in the next major release. ` +
56
+ `Move the file inside your config directory.`);
57
+ }
58
+ insertionAcc[name] = (0, utils_1.isFile)(resolvedTemplatePath)
59
+ ? (0, tools_1.loadFileAndReplaceKeywords)(resolvedTemplatePath, {
52
60
  mappings: context.mappings,
53
61
  disableKeywordReplacement: context.disableKeywordReplacement,
54
62
  }).trim()
@@ -21,7 +21,18 @@ function parse(context) {
21
21
  }),
22
22
  };
23
23
  if (rule.script) {
24
- rule.script = context.loadFile(rule.script, tools_1.constants.RULES_DIRECTORY);
24
+ const normalizedScript = rule.script.replace(/\\/g, '/');
25
+ const configRoot = path_1.default.resolve(context.filePath);
26
+ const resolvedPath = path_1.default.resolve(context.filePath, tools_1.constants.RULES_DIRECTORY, normalizedScript);
27
+ if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
28
+ logger_1.default.warn(`Path "${rule.script}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
29
+ `This will be blocked as an error in the next major release. ` +
30
+ `Move the file inside your config directory.`);
31
+ }
32
+ rule.script = (0, tools_1.loadFileAndReplaceKeywords)(resolvedPath, {
33
+ mappings: context.mappings,
34
+ disableKeywordReplacement: context.disableKeywordReplacement,
35
+ });
25
36
  }
26
37
  return rule;
27
38
  });
@@ -68,14 +68,13 @@ class DirectoryContext {
68
68
  };
69
69
  }
70
70
  loadFile(f, folder) {
71
- const basePath = path.join(this.filePath, folder);
72
- let toLoad = path.join(basePath, f);
73
- if (!(0, utils_1.isFile)(toLoad)) {
74
- // try load not relative to yaml file
75
- toLoad = f;
76
- logger_1.default.warn(`Support for absolute paths and paths outside the config root will be deprecated in a future version to improve the security of the tool. ` +
77
- `Please update your configuration to use paths relative to the config directory. ` +
78
- `Current absolute path used: ["${f}"]`);
71
+ const configRoot = path.resolve(this.filePath);
72
+ const basePath = path.resolve(this.filePath, folder);
73
+ const toLoad = path.resolve(basePath, f.replace(/\\/g, '/'));
74
+ if (!toLoad.startsWith(configRoot + path.sep)) {
75
+ logger_1.default.warn(`Path "${f}" resolves to "${toLoad}" which is outside the config directory "${configRoot}". ` +
76
+ `This will be blocked as an error in the next major release. ` +
77
+ `Move the file inside your config directory.`);
79
78
  }
80
79
  return (0, tools_1.loadFileAndReplaceKeywords)(toLoad, {
81
80
  mappings: this.mappings,
@@ -27,7 +27,7 @@ async function dump(context) {
27
27
  }
28
28
  const maskedAttackProtection = (0, defaults_1.attackProtectionDefaults)(attackProtectionConfig, context.config);
29
29
  return {
30
- attackProtection: maskedAttackProtection,
30
+ attackProtection: (0, defaults_1.sortAttackProtectionArrays)(maskedAttackProtection),
31
31
  };
32
32
  }
33
33
  const attackProtectionHandler = {
@@ -18,9 +18,16 @@ async function parse(context) {
18
18
  const parsedTemplates = templates.map((templateDefinition) => {
19
19
  const normalizedPathArray = (0, utils_1.nomalizedYAMLPath)(templateDefinition.body);
20
20
  const markupFile = path_1.default.join(context.basePath, ...normalizedPathArray);
21
+ const configRoot = path_1.default.resolve(context.basePath);
22
+ const resolvedMarkupFile = path_1.default.resolve(markupFile);
23
+ if (!resolvedMarkupFile.startsWith(configRoot + path_1.default.sep)) {
24
+ logger_1.default.warn(`Branding template body path "${templateDefinition.body}" resolves to "${resolvedMarkupFile}" which is outside the config directory "${configRoot}". ` +
25
+ `This will be blocked as an error in the next major release. ` +
26
+ `Move the file inside your config directory.`);
27
+ }
21
28
  return {
22
29
  template: templateDefinition.template,
23
- body: (0, tools_1.loadFileAndReplaceKeywords)(markupFile, {
30
+ body: (0, tools_1.loadFileAndReplaceKeywords)(resolvedMarkupFile, {
24
31
  mappings: context.mappings,
25
32
  disableKeywordReplacement: context.disableKeywordReplacement,
26
33
  }),
@@ -21,7 +21,10 @@ async function parse(context) {
21
21
  if (client.custom_login_page) {
22
22
  const htmlFileName = path_1.default.join(clientsFolder, client.custom_login_page);
23
23
  if ((0, utils_1.isFile)(htmlFileName)) {
24
- client.custom_login_page = context.loadFile(htmlFileName);
24
+ // Pass a path relative to basePath so context.loadFile() resolves it correctly.
25
+ // Passing htmlFileName (which already includes basePath) would cause double resolution
26
+ // when basePath is a relative path (e.g. when AUTH0_INPUT_FILE is a relative path).
27
+ client.custom_login_page = context.loadFile(path_1.default.join(tools_1.constants.CLIENTS_DIRECTORY, client.custom_login_page));
25
28
  }
26
29
  }
27
30
  return client;
@@ -26,7 +26,9 @@ async function parse(context) {
26
26
  logger_1.default.error(missingTemplateErrorMessage);
27
27
  throw new Error(missingTemplateErrorMessage);
28
28
  }
29
- connection.options.email.body = context.loadFile(htmlFileName);
29
+ // Pass a path relative to basePath so context.loadFile() resolves it correctly.
30
+ // Passing htmlFileName (which already includes basePath) would cause double resolution.
31
+ connection.options.email.body = context.loadFile(path_1.default.join(tools_1.constants.CONNECTIONS_DIRECTORY, connection.options.email.body));
30
32
  }
31
33
  return connection;
32
34
  }),
@@ -14,6 +14,13 @@ async function parse(context) {
14
14
  return { flows: null };
15
15
  const parsedFlows = flows.map((flow) => {
16
16
  const flowFile = path_1.default.join(context.basePath, flow.body);
17
+ const configRoot = path_1.default.resolve(context.basePath);
18
+ const resolvedFlowFile = path_1.default.resolve(flowFile);
19
+ if (!resolvedFlowFile.startsWith(configRoot + path_1.default.sep)) {
20
+ logger_1.default.warn(`Flow body file "${flow.body}" resolves to "${resolvedFlowFile}" which is outside the config directory "${configRoot}". ` +
21
+ `This will be blocked as an error in the next major release. ` +
22
+ `Move the file inside your config directory.`);
23
+ }
17
24
  const parsedFlowBody = (0, utils_1.loadJSON)(flowFile, {
18
25
  mappings: context.mappings,
19
26
  disableKeywordReplacement: context.disableKeywordReplacement,
@@ -14,6 +14,13 @@ async function parse(context) {
14
14
  return { forms: null };
15
15
  const parsedForms = forms.map((form) => {
16
16
  const formFile = path_1.default.join(context.basePath, form.body);
17
+ const configRoot = path_1.default.resolve(context.basePath);
18
+ const resolvedFormFile = path_1.default.resolve(formFile);
19
+ if (!resolvedFormFile.startsWith(configRoot + path_1.default.sep)) {
20
+ logger_1.default.warn(`Form body file "${form.body}" resolves to "${resolvedFormFile}" which is outside the config directory "${configRoot}". ` +
21
+ `This will be blocked as an error in the next major release. ` +
22
+ `Move the file inside your config directory.`);
23
+ }
17
24
  const parsedFormBody = (0, utils_1.loadJSON)(formFile, {
18
25
  mappings: context.mappings,
19
26
  disableKeywordReplacement: context.disableKeywordReplacement,
@@ -19,7 +19,13 @@ const loadScreenRenderers = (context, screenRenderArray) => {
19
19
  Object.entries(screens).forEach(([, fileName]) => {
20
20
  const filePath = fileName;
21
21
  try {
22
- const rendererFile = path_1.default.join(context.basePath, filePath);
22
+ const configRoot = path_1.default.resolve(context.basePath);
23
+ const rendererFile = path_1.default.resolve(context.basePath, filePath);
24
+ if (!rendererFile.startsWith(configRoot + path_1.default.sep)) {
25
+ logger_1.default.warn(`Screen renderer file "${filePath}" resolves to "${rendererFile}" which is outside the config directory "${configRoot}". ` +
26
+ `This will be blocked as an error in the next major release. ` +
27
+ `Move the file inside your config directory.`);
28
+ }
23
29
  const rendererData = (0, utils_1.loadJSON)(rendererFile, {
24
30
  mappings: context.mappings,
25
31
  disableKeywordReplacement: context.disableKeywordReplacement,