auth0-deploy-cli 8.29.3 → 8.31.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 (34) hide show
  1. package/.circleci/config.yml +15 -84
  2. package/.github/workflows/npm-publish.yml +52 -0
  3. package/AGENTS.md +21 -1
  4. package/CHANGELOG.md +30 -1
  5. package/CLAUDE.md +3 -0
  6. package/lib/args.d.ts +1 -0
  7. package/lib/args.js +5 -0
  8. package/lib/commands/export.js +5 -1
  9. package/lib/context/directory/handlers/actions.js +1 -2
  10. package/lib/context/directory/handlers/clientGrants.js +9 -1
  11. package/lib/context/directory/handlers/clients.js +6 -1
  12. package/lib/context/directory/handlers/connections.js +7 -1
  13. package/lib/context/directory/handlers/databases.js +6 -1
  14. package/lib/context/directory/handlers/resourceServers.js +6 -1
  15. package/lib/context/directory/handlers/rules.js +6 -1
  16. package/lib/context/directory/handlers/triggers.js +1 -3
  17. package/lib/context/yaml/handlers/clientGrants.js +9 -1
  18. package/lib/context/yaml/handlers/clients.js +5 -0
  19. package/lib/context/yaml/handlers/connections.js +7 -1
  20. package/lib/context/yaml/handlers/databases.js +7 -1
  21. package/lib/context/yaml/handlers/resourceServers.js +30 -3
  22. package/lib/context/yaml/handlers/rules.js +5 -0
  23. package/lib/context/yaml/index.js +2 -1
  24. package/lib/tools/auth0/handlers/actions.js +13 -15
  25. package/lib/tools/auth0/handlers/clientGrants.js +31 -4
  26. package/lib/tools/auth0/handlers/connections.d.ts +5 -0
  27. package/lib/tools/auth0/handlers/connections.js +9 -1
  28. package/lib/tools/auth0/handlers/customDomains.d.ts +5 -0
  29. package/lib/tools/auth0/handlers/customDomains.js +26 -0
  30. package/lib/tools/auth0/handlers/prompts.js +18 -0
  31. package/lib/tools/auth0/handlers/tenant.js +5 -0
  32. package/lib/types.d.ts +1 -0
  33. package/lib/utils.js +13 -1
  34. package/package.json +8 -7
@@ -6,7 +6,7 @@ orbs:
6
6
  jobs:
7
7
  e2e_test_as_node_module:
8
8
  docker:
9
- - image: cimg/node:22.12.0
9
+ - image: cimg/node:22.19.0
10
10
  working_directory: ~/repo
11
11
  steps:
12
12
  - checkout
@@ -15,7 +15,7 @@ jobs:
15
15
 
16
16
  e2e_test_as_cli:
17
17
  docker:
18
- - image: cimg/node:22.12.0
18
+ - image: cimg/node:22.19.0
19
19
  working_directory: ~/repo
20
20
  steps:
21
21
  - checkout
@@ -45,57 +45,31 @@ jobs:
45
45
  - run: npm run test:coverage
46
46
  - persist_to_workspace:
47
47
  root: ~/repo
48
- paths: .
48
+ paths:
49
+ - .
49
50
  - codecov/upload
50
51
 
51
- deploy:
52
- parameters:
53
- v:
54
- type: string
55
- default: "lts"
56
- docker:
57
- - image: cimg/node:<< parameters.v >>
58
- working_directory: ~/repo
59
- steps:
60
- - attach_workspace:
61
- at: ~/repo
62
- - run:
63
- name: Authenticate with registry
64
- command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
65
- - run:
66
- name: Publish package
67
- command: npm publish
68
-
69
- deploy_beta:
70
- parameters:
71
- v:
72
- type: string
73
- default: "lts"
52
+ does_typescript_compile:
74
53
  docker:
75
- - image: cimg/node:<< parameters.v >>
54
+ - image: cimg/node:22.19.0
76
55
  working_directory: ~/repo
77
56
  steps:
78
- - attach_workspace:
79
- at: ~/repo
80
- - run:
81
- name: Authenticate with registry
82
- command: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
83
- - run:
84
- name: Publish beta package
85
- command: npm publish --tag beta
57
+ - checkout
58
+ - run: npm i
59
+ - run: npx tsc --noEmit
86
60
 
87
- does_typescript_compile:
61
+ does_format_pass:
88
62
  docker:
89
- - image: cimg/node:22.12.0
63
+ - image: cimg/node:22.19.0
90
64
  working_directory: ~/repo
91
65
  steps:
92
66
  - checkout
93
67
  - run: npm i
94
- - run: npx tsc --noEmit
68
+ - run: npm run format:check
95
69
 
96
70
  does_lint_pass:
97
71
  docker:
98
- - image: cimg/node:22.12.0
72
+ - image: cimg/node:22.19.0
99
73
  working_directory: ~/repo
100
74
  steps:
101
75
  - checkout
@@ -119,6 +93,8 @@ workflows:
119
93
  jobs:
120
94
  - does_typescript_compile:
121
95
  name: Does Typescript compile?
96
+ - does_format_pass:
97
+ name: Does format pass?
122
98
  - does_lint_pass:
123
99
  name: Does lint pass?
124
100
  - unit_test:
@@ -128,48 +104,3 @@ workflows:
128
104
  name: Unit tests with Node current
129
105
  v: "22.12.0"
130
106
 
131
- test_and_deploy:
132
- jobs:
133
- - unit_test:
134
- name: Unit tests with Node LTS
135
- v: "lts"
136
- filters:
137
- branches:
138
- only: master
139
- tags:
140
- only: /^v.*/
141
- - deploy:
142
- name: Publish to NPM
143
- v: "lts"
144
- requires:
145
- - Unit tests with Node LTS
146
- filters:
147
- branches:
148
- ignore: /.*/
149
- tags:
150
- only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
151
- context:
152
- - publish-npm
153
-
154
- test_and_deploy_beta:
155
- jobs:
156
- - unit_test:
157
- name: Unit tests with Node LTS (Beta)
158
- v: "lts"
159
- filters:
160
- branches:
161
- only: beta
162
- tags:
163
- only: /^v[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$/
164
- - deploy_beta:
165
- name: Publish Beta to NPM
166
- v: "lts"
167
- requires:
168
- - Unit tests with Node LTS (Beta)
169
- filters:
170
- branches:
171
- ignore: /.*/
172
- tags:
173
- only: /^v[0-9]+\.[0-9]+\.[0-9]+-beta\.[0-9]+$/
174
- context:
175
- - publish-npm
@@ -0,0 +1,52 @@
1
+ name: Publish package to npm
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+
19
+ - uses: actions/setup-node@v6
20
+ with:
21
+ node-version: 22.14.0
22
+ registry-url: https://registry.npmjs.org
23
+ package-manager-cache: false
24
+
25
+ - name: Update npm for trusted publishing
26
+ run: npm install --global npm@11
27
+
28
+ - name: Install dependencies
29
+ run: npm install
30
+
31
+ - name: Build package
32
+ run: npm run build
33
+
34
+ - name: Determine npm dist-tag
35
+ id: npm_dist_tag
36
+ shell: bash
37
+ run: |
38
+ VERSION="${GITHUB_REF_NAME#v}"
39
+
40
+ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-beta\.[0-9]+)?$ ]]; then
41
+ echo "Unsupported release tag: ${GITHUB_REF_NAME}" >&2
42
+ exit 1
43
+ fi
44
+
45
+ if [[ "$VERSION" == *"-beta."* ]]; then
46
+ echo "value=beta" >> "$GITHUB_OUTPUT"
47
+ else
48
+ echo "value=latest" >> "$GITHUB_OUTPUT"
49
+ fi
50
+
51
+ - name: Publish package
52
+ run: npm publish --tag "${{ steps.npm_dist_tag.outputs.value }}"
package/AGENTS.md CHANGED
@@ -63,7 +63,7 @@ npm run build && node lib/index.js import -c config-dev.json -i ./local-export/t
63
63
 
64
64
  ### File Structure
65
65
 
66
- ```
66
+ ```text
67
67
  src/ # TypeScript source
68
68
  ├── index.ts # CLI entry point
69
69
  ├── commands/ # import.ts, export.ts
@@ -225,6 +225,26 @@ npm run lint
225
225
  - [ ] Tested with both YAML and directory formats if applicable
226
226
  - [ ] Checked backward compatibility
227
227
 
228
+ ### Checklist for Every Feature or Fix
229
+
230
+ #### Completion
231
+
232
+ - [ ] Change is complete and matches the intended feature or fix
233
+ - [ ] TypeScript compiles and tests pass locally
234
+
235
+ #### Format and behavior
236
+
237
+ - [ ] Works for both YAML and directory flows when the resource supports both
238
+ - [ ] Keyword preservation still works as expected
239
+ - [ ] Sorted-key output stays stable; no unintended order changes were introduced
240
+ - [ ] Exported configs do not rely on IDs by default; resources should link by names where possible
241
+
242
+ #### Safety and compatibility
243
+
244
+ - [ ] Dry-run output is correct and non-destructive
245
+ - [ ] Unit tests cover the changed path and any important edge cases
246
+ - [ ] Backward compatibility was checked and no existing behavior was unintentionally broken
247
+
228
248
  ### Commit Message Format
229
249
 
230
250
  Follow conventional commits style:
package/CHANGELOG.md CHANGED
@@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [8.31.0] - 2026-04-10
11
+
12
+ ### Added
13
+
14
+ - Add support for `is_default` on `customDomains`. [#1330]
15
+ - Add `AUTH0_EXPORT_ORDERED` config property and `--export_ordered` flag for stable exports. [#1335]
16
+
17
+ ### Fixed
18
+
19
+ - Fix `clientGrants` matching when multiple grants share the same `client_id` and `audience`. [#1341]
20
+
21
+ ## [8.30.0] - 2026-04-02
22
+
23
+ ### Added
24
+
25
+ - Add support for passkey enrollment in `prompts` partials. [#1328]
26
+ - Add support for `dpop_signing_alg` validation in OIDC and Okta `connections`. [#1343]
27
+
28
+ ### Fixed
29
+
30
+ - Fix action module fetching logic to resolve correct module IDs. [#1340]
31
+ - Fix `AUTH0_EXCLUDED_*` options not respected during export. [#1342]
32
+
10
33
  ## [8.29.3] - 2026-03-25
11
34
 
12
35
  ### Fixed
@@ -1692,9 +1715,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1692
1715
  [#1320]: https://github.com/auth0/auth0-deploy-cli/issues/1320
1693
1716
  [#1321]: https://github.com/auth0/auth0-deploy-cli/issues/1321
1694
1717
  [#1322]: https://github.com/auth0/auth0-deploy-cli/issues/1322
1718
+ [#1328]: https://github.com/auth0/auth0-deploy-cli/issues/1328
1695
1719
  [#1333]: https://github.com/auth0/auth0-deploy-cli/issues/1333
1696
1720
  [#1334]: https://github.com/auth0/auth0-deploy-cli/issues/1334
1697
- [Unreleased]: https://github.com/auth0/auth0-deploy-cli/compare/v8.29.3...HEAD
1721
+ [#1340]: https://github.com/auth0/auth0-deploy-cli/issues/1340
1722
+ [#1342]: https://github.com/auth0/auth0-deploy-cli/issues/1342
1723
+ [#1343]: https://github.com/auth0/auth0-deploy-cli/issues/1343
1724
+ [Unreleased]: https://github.com/auth0/auth0-deploy-cli/compare/v8.31.0...HEAD
1725
+ [8.31.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.30.0...v8.31.0
1726
+ [8.30.0]: https://github.com/auth0/auth0-deploy-cli/compare/v8.29.3...v8.30.0
1698
1727
  [8.29.3]: https://github.com/auth0/auth0-deploy-cli/compare/v8.29.2...v8.29.3
1699
1728
  [8.29.2]: https://github.com/auth0/auth0-deploy-cli/compare/v8.29.1...v8.29.2
1700
1729
  [8.29.1]: https://github.com/auth0/auth0-deploy-cli/compare/v8.29.0...v8.29.1
package/CLAUDE.md ADDED
@@ -0,0 +1,3 @@
1
+ # For project context see
2
+
3
+ - [AGENTS.md](./AGENTS.md)
package/lib/args.d.ts CHANGED
@@ -16,6 +16,7 @@ type ExportSpecificParams = {
16
16
  format: 'yaml' | 'directory';
17
17
  output_folder: string;
18
18
  export_ids?: boolean;
19
+ export_ordered?: boolean;
19
20
  };
20
21
  export type ExportParams = ExportSpecificParams & SharedParams;
21
22
  export type ImportParams = ImportSpecificParams & SharedParams;
package/lib/args.js CHANGED
@@ -83,6 +83,11 @@ function getParams() {
83
83
  type: 'boolean',
84
84
  default: false,
85
85
  },
86
+ export_ordered: {
87
+ alias: 's',
88
+ describe: 'Order keys in exported JSON files for consistent diffs.',
89
+ type: 'boolean',
90
+ },
86
91
  })
87
92
  .example('$0 export -c config.json -f yaml -o path/to/export', 'Dump Auth0 config to folder in YAML format')
88
93
  .example('$0 export -c config.json -f directory -o path/to/export', 'Dump Auth0 config to folder in directory format')
@@ -11,7 +11,7 @@ const logger_1 = __importDefault(require("../logger"));
11
11
  const utils_1 = require("../utils");
12
12
  const index_1 = require("../context/index");
13
13
  async function exportCMD(params) {
14
- const { output_folder: outputFolder, base_path: basePath, config_file: configFile, config: configObj, export_ids: exportIds, secret: clientSecret, env: shouldInheritEnv = false, experimental_ea: experimentalEA, } = params;
14
+ const { output_folder: outputFolder, base_path: basePath, config_file: configFile, config: configObj, export_ids: exportIds, export_ordered: exportOrdered, secret: clientSecret, env: shouldInheritEnv = false, experimental_ea: experimentalEA, } = params;
15
15
  if (shouldInheritEnv) {
16
16
  nconf_1.default.env().use('memory');
17
17
  }
@@ -32,6 +32,10 @@ async function exportCMD(params) {
32
32
  if (exportIds) {
33
33
  overrides.AUTH0_EXPORT_IDENTIFIERS = exportIds;
34
34
  }
35
+ // Allow passed in export_ordered to override the configured one
36
+ if (exportOrdered) {
37
+ overrides.AUTH0_EXPORT_ORDERED = exportOrdered;
38
+ }
35
39
  // Overrides AUTH0_INCLUDE_EXPERIMENTAL_EA is experimental_ea passed in command line
36
40
  if (experimentalEA) {
37
41
  overrides.AUTH0_EXPERIMENTAL_EA = experimentalEA;
@@ -99,8 +99,7 @@ async function dump(context) {
99
99
  // Dump template metadata
100
100
  const name = (0, utils_1.sanitize)(action.name);
101
101
  const actionFile = path_1.default.join(actionsFolder, `${name}.json`);
102
- logger_1.default.info(`Writing ${actionFile}`);
103
- fs_extra_1.default.writeFileSync(actionFile, JSON.stringify(mapToAction(context.filePath, action, includeIdentifiers), null, 2));
102
+ (0, utils_1.dumpJSON)(actionFile, mapToAction(context.filePath, action, includeIdentifiers));
104
103
  });
105
104
  }
106
105
  const actionsHandler = {
@@ -25,13 +25,14 @@ function parse(context) {
25
25
  };
26
26
  }
27
27
  async function dump(context) {
28
- const { clientGrants } = context.assets;
28
+ let { clientGrants } = context.assets;
29
29
  if (!clientGrants)
30
30
  return; // Skip, nothing to dump
31
31
  const grantsFolder = path_1.default.join(context.filePath, tools_1.constants.CLIENTS_GRANTS_DIRECTORY);
32
32
  fs_extra_1.default.ensureDirSync(grantsFolder);
33
33
  if (clientGrants.length === 0)
34
34
  return;
35
+ const excludedClientsByNames = (context.assets.exclude && context.assets.exclude.clients) || [];
35
36
  const allResourceServers = await (0, client_1.paginate)(context.mgmtClient.resourceServers.list, {
36
37
  paginate: true,
37
38
  include_totals: true,
@@ -40,6 +41,13 @@ async function dump(context) {
40
41
  paginate: true,
41
42
  include_totals: true,
42
43
  });
44
+ // Filter out grants for excluded clients
45
+ if (excludedClientsByNames.length) {
46
+ const excludedClientIds = new Set(allClients
47
+ .filter((c) => c.name !== undefined && excludedClientsByNames.includes(c.name))
48
+ .map((c) => c.client_id));
49
+ clientGrants = clientGrants.filter((grant) => !excludedClientIds.has(grant.client_id));
50
+ }
43
51
  // Convert client_id to the client name for readability
44
52
  clientGrants.forEach((grant) => {
45
53
  const dumpGrant = { ...grant };
@@ -36,10 +36,15 @@ function parse(context) {
36
36
  };
37
37
  }
38
38
  async function dump(context) {
39
- const { clients } = context.assets;
39
+ let { clients } = context.assets;
40
40
  const { userAttributeProfiles, connectionProfiles } = context.assets;
41
41
  if (!clients)
42
42
  return; // Skip, nothing to dump
43
+ // Filter excluded clients
44
+ const excludedClients = (context.assets.exclude && context.assets.exclude.clients) || [];
45
+ if (excludedClients.length) {
46
+ clients = clients.filter((client) => !excludedClients.includes(client.name ?? ''));
47
+ }
43
48
  const clientsFolder = path_1.default.join(context.filePath, tools_1.constants.CLIENTS_DIRECTORY);
44
49
  fs_extra_1.default.ensureDirSync(clientsFolder);
45
50
  clients.forEach((client) => {
@@ -40,9 +40,15 @@ function parse(context) {
40
40
  };
41
41
  }
42
42
  async function dump(context) {
43
- const { connections, clientsOrig } = context.assets;
43
+ let { connections } = context.assets;
44
+ const { clientsOrig } = context.assets;
44
45
  if (!connections)
45
46
  return; // Skip, nothing to dump
47
+ // Filter excluded connections
48
+ const excludedConnections = (context.assets.exclude && context.assets.exclude.connections) || [];
49
+ if (excludedConnections.length) {
50
+ connections = connections.filter((connection) => !excludedConnections.includes(connection.name));
51
+ }
46
52
  const connectionsFolder = path_1.default.join(context.filePath, tools_1.constants.CONNECTIONS_DIRECTORY);
47
53
  fs_extra_1.default.ensureDirSync(connectionsFolder);
48
54
  // Convert enabled_clients from id to name
@@ -65,9 +65,14 @@ function parse(context) {
65
65
  };
66
66
  }
67
67
  async function dump(context) {
68
- const { databases } = context.assets;
68
+ let { databases } = context.assets;
69
69
  if (!databases)
70
70
  return; // Skip, nothing to dump
71
+ // Filter excluded databases
72
+ const excludedDatabases = (context.assets.exclude && context.assets.exclude.databases) || [];
73
+ if (excludedDatabases.length) {
74
+ databases = databases.filter((database) => !excludedDatabases.includes(database.name));
75
+ }
71
76
  const databasesFolder = path_1.default.join(context.filePath, tools_1.constants.DATABASE_CONNECTIONS_DIRECTORY);
72
77
  fs_extra_1.default.ensureDirSync(databasesFolder);
73
78
  databases.forEach((database) => {
@@ -24,10 +24,15 @@ function parse(context) {
24
24
  };
25
25
  }
26
26
  async function dump(context) {
27
- const { resourceServers } = context.assets;
27
+ let { resourceServers } = context.assets;
28
28
  let { clients } = context.assets;
29
29
  if (!resourceServers)
30
30
  return; // Skip, nothing to dump
31
+ // Filter excluded resource servers
32
+ const excludedResourceServers = (context.assets.exclude && context.assets.exclude.resourceServers) || [];
33
+ if (excludedResourceServers.length) {
34
+ resourceServers = resourceServers.filter((resourceServer) => !excludedResourceServers.includes(resourceServer.name ?? ''));
35
+ }
31
36
  const resourceServersFolder = path_1.default.join(context.filePath, tools_1.constants.RESOURCE_SERVERS_DIRECTORY);
32
37
  fs_extra_1.default.ensureDirSync(resourceServersFolder);
33
38
  if (clients === undefined) {
@@ -30,9 +30,14 @@ function parse(context) {
30
30
  };
31
31
  }
32
32
  async function dump(context) {
33
- const { rules } = context.assets;
33
+ let { rules } = context.assets;
34
34
  if (!rules)
35
35
  return; // Skip, nothing to dump
36
+ // Filter excluded rules
37
+ const excludedRules = (context.assets.exclude && context.assets.exclude.rules) || [];
38
+ if (excludedRules.length) {
39
+ rules = rules.filter((rule) => !excludedRules.includes(rule.name));
40
+ }
36
41
  // Create Rules folder
37
42
  const rulesFolder = path_1.default.join(context.filePath, tools_1.constants.RULES_DIRECTORY);
38
43
  fs_extra_1.default.ensureDirSync(rulesFolder);
@@ -7,7 +7,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
9
  const utils_1 = require("../../../utils");
10
- const logger_1 = __importDefault(require("../../../logger"));
11
10
  function parse(context) {
12
11
  const triggersFolder = path_1.default.join(context.filePath, tools_1.constants.TRIGGERS_DIRECTORY);
13
12
  if (!(0, utils_1.existsMustBeDir)(triggersFolder))
@@ -29,8 +28,7 @@ async function dump(context) {
29
28
  const triggersFolder = path_1.default.join(context.filePath, tools_1.constants.TRIGGERS_DIRECTORY);
30
29
  fs_extra_1.default.ensureDirSync(triggersFolder);
31
30
  const triggerFile = path_1.default.join(triggersFolder, 'triggers.json');
32
- logger_1.default.info(`Writing ${triggerFile}`);
33
- fs_extra_1.default.writeFileSync(triggerFile, JSON.stringify(triggers, null, 2));
31
+ (0, utils_1.dumpJSON)(triggerFile, triggers);
34
32
  }
35
33
  const triggersHandler = {
36
34
  parse,
@@ -12,7 +12,7 @@ async function parse(context) {
12
12
  }
13
13
  async function dump(context) {
14
14
  let { clients } = context.assets;
15
- const { clientGrants } = context.assets;
15
+ let { clientGrants } = context.assets;
16
16
  if (!clientGrants)
17
17
  return { clientGrants: null };
18
18
  if (clients === undefined) {
@@ -21,6 +21,14 @@ async function dump(context) {
21
21
  include_totals: true,
22
22
  });
23
23
  }
24
+ // Filter out grants for excluded clients
25
+ const excludedClientsByNames = (context.assets.exclude && context.assets.exclude.clients) || [];
26
+ if (excludedClientsByNames.length) {
27
+ const excludedClientIds = new Set((clients || [])
28
+ .filter((c) => c.name !== undefined && excludedClientsByNames.includes(c.name))
29
+ .map((c) => c.client_id));
30
+ clientGrants = clientGrants.filter((grant) => !excludedClientIds.has(grant.client_id));
31
+ }
24
32
  // Convert client_id to the client name for readability
25
33
  return {
26
34
  clientGrants: clientGrants.map((grant) => {
@@ -36,6 +36,11 @@ async function dump(context) {
36
36
  const { userAttributeProfiles, connectionProfiles } = context.assets;
37
37
  if (!clients)
38
38
  return { clients: null };
39
+ // Filter excluded clients
40
+ const excludedClients = (context.assets.exclude && context.assets.exclude.clients) || [];
41
+ if (excludedClients.length) {
42
+ clients = clients.filter((client) => !excludedClients.includes(client.name ?? ''));
43
+ }
39
44
  // map ids to names for user attribute profiles and connection profiles
40
45
  clients = clients.map((client) => {
41
46
  const userAttributeProfileId = client?.express_configuration?.user_attribute_profile_id;
@@ -50,9 +50,15 @@ const getFormattedOptions = (connection, clients) => {
50
50
  }
51
51
  };
52
52
  async function dump(context) {
53
- const { connections, clients } = context.assets;
53
+ let { connections } = context.assets;
54
+ const { clients } = context.assets;
54
55
  if (!connections)
55
56
  return { connections: null };
57
+ // Filter excluded connections
58
+ const excludedConnections = (context.assets.exclude && context.assets.exclude.connections) || [];
59
+ if (excludedConnections.length) {
60
+ connections = connections.filter((connection) => !excludedConnections.includes(connection.name));
61
+ }
56
62
  return {
57
63
  connections: connections.map((connection) => {
58
64
  let dumpedConnection = {
@@ -31,9 +31,15 @@ async function parse(context) {
31
31
  };
32
32
  }
33
33
  async function dump(context) {
34
- const { databases, clients } = context.assets;
34
+ let { databases } = context.assets;
35
+ const { clients } = context.assets;
35
36
  if (!databases)
36
37
  return { databases: null };
38
+ // Filter excluded databases
39
+ const excludedDatabases = (context.assets.exclude && context.assets.exclude.databases) || [];
40
+ if (excludedDatabases.length) {
41
+ databases = databases.filter((database) => !excludedDatabases.includes(database.name));
42
+ }
37
43
  const sortCustomScripts = ([name1], [name2]) => {
38
44
  if (name1 === name2)
39
45
  return 0;
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const client_1 = require("../../../tools/auth0/client");
4
4
  const utils_1 = require("../../../utils");
5
- async function dumpAndParse(context) {
5
+ async function parse(context) {
6
6
  const { resourceServers } = context.assets;
7
7
  let { clients } = context.assets;
8
8
  if (!resourceServers) {
@@ -24,8 +24,35 @@ async function dumpAndParse(context) {
24
24
  }),
25
25
  };
26
26
  }
27
+ async function dump(context) {
28
+ let { resourceServers } = context.assets;
29
+ let { clients } = context.assets;
30
+ if (!resourceServers) {
31
+ return { resourceServers: null };
32
+ }
33
+ // Filter excluded resource servers
34
+ const excludedResourceServers = (context.assets.exclude && context.assets.exclude.resourceServers) || [];
35
+ if (excludedResourceServers.length) {
36
+ resourceServers = resourceServers.filter((rs) => !excludedResourceServers.includes(rs.name ?? ''));
37
+ }
38
+ if (clients === undefined) {
39
+ clients = await (0, client_1.paginate)(context.mgmtClient.clients.list, {
40
+ paginate: true,
41
+ include_totals: true,
42
+ });
43
+ }
44
+ return {
45
+ resourceServers: resourceServers.map((rs) => {
46
+ const dumpResourceServer = { ...rs };
47
+ if (dumpResourceServer.client_id) {
48
+ dumpResourceServer.client_id = (0, utils_1.convertClientIdToName)(dumpResourceServer.client_id, clients || []);
49
+ }
50
+ return dumpResourceServer;
51
+ }),
52
+ };
53
+ }
27
54
  const resourceServersHandler = {
28
- parse: dumpAndParse,
29
- dump: dumpAndParse,
55
+ parse,
56
+ dump,
30
57
  };
31
58
  exports.default = resourceServersHandler;
@@ -25,6 +25,11 @@ async function dump(context) {
25
25
  if (!rules) {
26
26
  return { rules: null };
27
27
  }
28
+ // Filter excluded rules
29
+ const excludedRules = (context.assets.exclude && context.assets.exclude.rules) || [];
30
+ if (excludedRules.length) {
31
+ rules = rules.filter((rule) => !excludedRules.includes(rule.name));
32
+ }
28
33
  // Create Rules folder
29
34
  const rulesFolder = path_1.default.join(context.basePath, 'rules');
30
35
  fs_extra_1.default.ensureDirSync(rulesFolder);
@@ -183,7 +183,8 @@ class YAMLContext {
183
183
  cleaned = (0, utils_1.stripIdentifiers)(auth0, cleaned);
184
184
  }
185
185
  // Write YAML File
186
- const raw = js_yaml_1.default.dump(cleaned);
186
+ const exportOrdered = Boolean(this.config.AUTH0_EXPORT_ORDERED);
187
+ const raw = js_yaml_1.default.dump(cleaned, { sortKeys: exportOrdered });
187
188
  logger_1.default.info(`Writing ${this.configFile}`);
188
189
  fs_extra_1.default.writeFileSync(this.configFile, raw);
189
190
  }
@@ -249,7 +249,7 @@ class ActionHandler extends default_1.default {
249
249
  }
250
250
  }
251
251
  async calcChanges(assets) {
252
- let { actions, actionModules } = assets;
252
+ let { actions } = assets;
253
253
  // Do nothing if not set
254
254
  if (!actions)
255
255
  return {
@@ -259,19 +259,14 @@ class ActionHandler extends default_1.default {
259
259
  conflicts: [],
260
260
  };
261
261
  let modules = null;
262
- if (actionModules && actionModules.length > 0) {
263
- modules = actionModules;
262
+ try {
263
+ modules = await (0, client_1.paginate)(this.client.actions.modules.list, {
264
+ paginate: true,
265
+ });
264
266
  }
265
- else {
266
- try {
267
- modules = await (0, client_1.paginate)(this.client.actions.modules.list, {
268
- paginate: true,
269
- });
270
- }
271
- catch {
272
- logger_1.default.debug('Skipping actions modules enrichment because action modules could not be retrieved.');
273
- modules = null;
274
- }
267
+ catch {
268
+ logger_1.default.debug('Skipping actions modules enrichment because action modules could not be retrieved.');
269
+ modules = null;
275
270
  }
276
271
  if (modules != null) {
277
272
  // Use task queue to process actions in parallel
@@ -305,12 +300,15 @@ class ActionHandler extends default_1.default {
305
300
  moduleVersions = await moduleVersions.getNextPage();
306
301
  allModuleVersions.push(...moduleVersions.data);
307
302
  }
303
+ const moduleVersionId = allModuleVersions?.find((v) => v.version_number === module.module_version_number)?.id;
304
+ if (!moduleVersionId) {
305
+ throw new Error(`Could not find action module version id for module '${module.module_name}' version '${module.module_version_number}'`);
306
+ }
308
307
  return {
309
308
  module_name: module.module_name,
310
309
  module_id: foundModule.id,
311
310
  module_version_number: module.module_version_number,
312
- module_version_id: allModuleVersions?.find((v) => v.version_number === module.module_version_number)
313
- ?.id || '',
311
+ module_version_id: moduleVersionId,
314
312
  };
315
313
  }
316
314
  return module;
@@ -84,7 +84,9 @@ class ClientGrantsHandler extends default_1.default {
84
84
  type: 'clientGrants',
85
85
  id: 'id',
86
86
  // @ts-ignore because not sure why two-dimensional array passed in
87
- identifiers: ['id', ['client_id', 'audience']],
87
+ // Try ['client_id', 'audience', 'subject_type'] first; falls through to
88
+ // ['client_id', 'audience'] when subject_type is null (falsy).
89
+ identifiers: ['id', ['client_id', 'audience', 'subject_type'], ['client_id', 'audience']],
88
90
  stripUpdateFields: ['audience', 'client_id', 'subject_type', 'is_system'],
89
91
  });
90
92
  }
@@ -155,6 +157,31 @@ class ClientGrantsHandler extends default_1.default {
155
157
  ...assets,
156
158
  clientGrants: formatted,
157
159
  });
160
+ // subject_type is immutable (in stripUpdateFields). Grants matched via the
161
+ // ['client_id', 'audience'] fallback with a mismatched subject_type must become
162
+ // DELETE + CREATE, not UPDATE, so the tenant converges to the desired state.
163
+ const subjectTypeMismatches = update.filter((localGrant) => {
164
+ // Only flag when local explicitly specifies subject_type (backward compat).
165
+ if (localGrant.subject_type === undefined)
166
+ return false;
167
+ const remoteGrant = (this.existing || []).find((e) => e.id === localGrant.id);
168
+ return (remoteGrant && (remoteGrant.subject_type ?? null) !== (localGrant.subject_type ?? null));
169
+ });
170
+ const adjustedUpdate = update.filter((u) => !subjectTypeMismatches.includes(u));
171
+ const adjustedDel = [
172
+ ...del,
173
+ ...subjectTypeMismatches
174
+ .map((u) => (this.existing || []).find((e) => e.id === u.id))
175
+ .filter((e) => e !== undefined),
176
+ ];
177
+ const adjustedCreate = [
178
+ ...create,
179
+ ...subjectTypeMismatches
180
+ .map((u) => formatted.find((f) => f.client_id === u.client_id &&
181
+ f.audience === u.audience &&
182
+ (f.subject_type ?? null) === (u.subject_type ?? null)))
183
+ .filter((g) => g !== undefined),
184
+ ];
158
185
  const filterGrants = (list) => {
159
186
  let filtered = list;
160
187
  // Filter out the current client (Auth0 Management API client)
@@ -174,11 +201,11 @@ class ClientGrantsHandler extends default_1.default {
174
201
  };
175
202
  const changes = {
176
203
  // @ts-ignore because this expects `client_id` and that's not yet typed on Asset
177
- del: filterGrants(del),
204
+ del: filterGrants(adjustedDel),
178
205
  // @ts-ignore because this expects `client_id` and that's not yet typed on Asset
179
- update: filterGrants(update),
206
+ update: filterGrants(adjustedUpdate),
180
207
  // @ts-ignore because this expects `client_id` and that's not yet typed on Asset
181
- create: filterGrants(create),
208
+ create: filterGrants(adjustedCreate),
182
209
  // @ts-ignore because this expects `client_id` and that's not yet typed on Asset
183
210
  conflicts: filterGrants(conflicts),
184
211
  };
@@ -16,6 +16,11 @@ export declare const schema: {
16
16
  };
17
17
  options: {
18
18
  type: string;
19
+ properties: {
20
+ dpop_signing_alg: {
21
+ type: string;
22
+ };
23
+ };
19
24
  };
20
25
  enabled_clients: {
21
26
  type: string;
@@ -51,6 +51,14 @@ const utils_1 = require("../../utils");
51
51
  const client_1 = require("../client");
52
52
  const scimHandler_1 = __importDefault(require("./scimHandler"));
53
53
  const logger_1 = __importDefault(require("../../../logger"));
54
+ const connectionOptionsSchema = {
55
+ type: 'object',
56
+ properties: {
57
+ dpop_signing_alg: {
58
+ type: 'string',
59
+ },
60
+ },
61
+ };
54
62
  exports.schema = {
55
63
  type: 'array',
56
64
  items: {
@@ -58,7 +66,7 @@ exports.schema = {
58
66
  properties: {
59
67
  name: { type: 'string' },
60
68
  strategy: { type: 'string' },
61
- options: { type: 'object' },
69
+ options: connectionOptionsSchema,
62
70
  enabled_clients: { type: 'array', items: { type: 'string' } },
63
71
  realms: { type: 'array', items: { type: 'string' } },
64
72
  metadata: { type: 'object' },
@@ -50,6 +50,10 @@ export declare const schema: {
50
50
  type: string[];
51
51
  description: string;
52
52
  };
53
+ is_default: {
54
+ type: string;
55
+ description: string;
56
+ };
53
57
  };
54
58
  required: string[];
55
59
  };
@@ -60,6 +64,7 @@ export default class CustomDomainsHadnler extends DefaultAPIHandler {
60
64
  constructor(config: DefaultAPIHandler);
61
65
  objString(item: Asset): string;
62
66
  getType(): Promise<Asset | null>;
67
+ validate(assets: Assets): Promise<void>;
63
68
  processChanges(assets: Assets): Promise<void>;
64
69
  }
65
70
  export {};
@@ -44,6 +44,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.schema = void 0;
46
46
  const default_1 = __importStar(require("./default"));
47
+ const validationError_1 = __importDefault(require("../../validationError"));
47
48
  const logger_1 = __importDefault(require("../../../logger"));
48
49
  exports.schema = {
49
50
  type: 'array',
@@ -81,6 +82,10 @@ exports.schema = {
81
82
  type: ['string'],
82
83
  description: 'Relying Party ID (rpId) to be used for Passkeys on this custom domain. If not provided or set to null, the full domain will be used.',
83
84
  },
85
+ is_default: {
86
+ type: 'boolean',
87
+ description: 'Whether this custom domain is the default domain used for email notifications.',
88
+ },
84
89
  },
85
90
  required: ['domain', 'type'],
86
91
  },
@@ -136,6 +141,16 @@ class CustomDomainsHadnler extends default_1.default {
136
141
  throw err;
137
142
  }
138
143
  }
144
+ async validate(assets) {
145
+ await super.validate(assets);
146
+ const { customDomains } = assets;
147
+ if (!customDomains)
148
+ return;
149
+ const defaultDomains = customDomains.filter((d) => d.is_default === true);
150
+ if (defaultDomains.length > 1) {
151
+ throw new validationError_1.default(`Only one custom domain can be set as default (is_default: true), but found ${defaultDomains.length}: ${defaultDomains.map((d) => d.domain).join(', ')}`);
152
+ }
153
+ }
139
154
  async processChanges(assets) {
140
155
  const { customDomains } = assets;
141
156
  if (!customDomains)
@@ -149,6 +164,17 @@ class CustomDomainsHadnler extends default_1.default {
149
164
  }
150
165
  const changes = await this.calcChanges(assets);
151
166
  await super.processChanges(assets, changes);
167
+ // If a domain is marked as is_default, set it as the tenant's default custom domain.
168
+ const defaultDomain = customDomains.find((d) => d.is_default === true);
169
+ if (defaultDomain) {
170
+ try {
171
+ await this.client.customDomains.setDefault({ domain: defaultDomain.domain });
172
+ logger_1.default.info(`Set default custom domain: ${defaultDomain.domain}`);
173
+ }
174
+ catch (err) {
175
+ throw new Error(`Problem setting default custom domain ${defaultDomain.domain}\n${err}`);
176
+ }
177
+ }
152
178
  }
153
179
  }
154
180
  exports.default = CustomDomainsHadnler;
@@ -161,7 +161,10 @@ const customPartialsPromptTypes = [
161
161
  'signup',
162
162
  'signup-id',
163
163
  'signup-password',
164
+ 'passkeys',
164
165
  ];
166
+ // Prompts that may not be available on all tenants (early access features)
167
+ const optionalPartialsPromptTypes = ['passkeys'];
165
168
  const customPartialsScreenTypes = [
166
169
  'login',
167
170
  'login-id',
@@ -171,6 +174,8 @@ const customPartialsScreenTypes = [
171
174
  'signup-password',
172
175
  'login-passwordless-sms-otp',
173
176
  'login-passwordless-email-code',
177
+ 'passkeys-enrollment',
178
+ 'passkeys-enrollment-local',
174
179
  ];
175
180
  const customPartialsInsertionPoints = [
176
181
  'form-content-start',
@@ -358,6 +363,19 @@ class PromptsHandler extends default_1.default {
358
363
  this.IsFeatureSupported = false;
359
364
  return null;
360
365
  }
366
+ // Handle 400 errors for prompt types not available on all tenants (early access features)
367
+ // Error format: "Path validation error: 'Invalid value \"passkeys\"' on property prompt (Name of the prompt)."
368
+ if (error &&
369
+ error?.statusCode === 400 &&
370
+ error.message?.includes('Path validation error') &&
371
+ error.message?.includes('on property prompt')) {
372
+ // Check if the error message contains any of the optional prompt types
373
+ const unavailablePrompt = optionalPartialsPromptTypes.find((promptType) => error.message?.includes(promptType));
374
+ if (unavailablePrompt) {
375
+ logger_1.default.warn(`Skipping partials for prompt type '${unavailablePrompt}' because it is not available on this tenant.`);
376
+ return null;
377
+ }
378
+ }
361
379
  if (error && error.statusCode === 429) {
362
380
  logger_1.default.error(`The global rate limit has been exceeded, resulting in a ${error.statusCode} error. ${error.message}. Although this is an error, it is not blocking the pipeline.`);
363
381
  return null;
@@ -198,6 +198,11 @@ class TenantHandler extends default_1.default {
198
198
  delete updatedTenant.flags;
199
199
  }
200
200
  }
201
+ if (tenant.flags?.enable_custom_domain_in_emails !== undefined) {
202
+ logger_1.default.warn('The "enable_custom_domain_in_emails" tenant flag is deprecated. ' +
203
+ 'Use the "is_default" field on customDomains to configure the default domain instead. ' +
204
+ 'The flag will still be applied for now but will be removed in a future release.');
205
+ }
201
206
  if (updatedTenant && Object.keys(updatedTenant).length > 0) {
202
207
  await this.client.tenants.settings.update(updatedTenant);
203
208
  this.updated += 1;
package/lib/types.d.ts CHANGED
@@ -71,6 +71,7 @@ export type Config = {
71
71
  AUTH0_RETRY_MAX_DELAY_MS?: number;
72
72
  AUTH0_KEYWORD_REPLACE_MAPPINGS?: KeywordMappings;
73
73
  AUTH0_EXPORT_IDENTIFIERS?: boolean;
74
+ AUTH0_EXPORT_ORDERED?: boolean;
74
75
  AUTH0_CONNECTIONS_DIRECTORY?: string;
75
76
  EXCLUDED_PROPS?: {
76
77
  [key: string]: string[];
package/lib/utils.js CHANGED
@@ -24,6 +24,7 @@ exports.mapClientID2NameSorted = mapClientID2NameSorted;
24
24
  exports.nomalizedYAMLPath = nomalizedYAMLPath;
25
25
  const path_1 = __importDefault(require("path"));
26
26
  const fs_extra_1 = __importDefault(require("fs-extra"));
27
+ const nconf_1 = __importDefault(require("nconf"));
27
28
  const sanitize_filename_1 = __importDefault(require("sanitize-filename"));
28
29
  const dot_prop_1 = __importDefault(require("dot-prop"));
29
30
  const lodash_1 = require("lodash");
@@ -69,10 +70,21 @@ function loadJSON(file, opts = {
69
70
  throw new Error(`Error parsing JSON from metadata file: ${file}, because: ${e.message}`);
70
71
  }
71
72
  }
73
+ function orderedKeysReplacer(_key, value) {
74
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
75
+ const obj = value;
76
+ return Object.fromEntries(Object.keys(obj)
77
+ .sort()
78
+ .map((k) => [k, obj[k]]));
79
+ }
80
+ return value;
81
+ }
72
82
  function dumpJSON(file, mappings) {
73
83
  try {
74
84
  logger_1.default.info(`Writing ${file}`);
75
- const jsonBody = JSON.stringify(mappings, null, 2);
85
+ const exportOrdered = Boolean(nconf_1.default.get('AUTH0_EXPORT_ORDERED'));
86
+ const replacer = exportOrdered ? orderedKeysReplacer : undefined;
87
+ const jsonBody = JSON.stringify(mappings, replacer, 2);
76
88
  fs_extra_1.default.writeFileSync(file, jsonBody.endsWith('\n') ? jsonBody : `${jsonBody}\n`);
77
89
  }
78
90
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth0-deploy-cli",
3
- "version": "8.29.3",
3
+ "version": "8.31.0",
4
4
  "description": "A command line tool for deploying updates to your Auth0 tenant",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -9,9 +9,10 @@
9
9
  "scripts": {
10
10
  "lint:fix": "eslint --fix . && kacl lint",
11
11
  "lint": "eslint . && kacl lint",
12
+ "format:check": "npx prettier --check .",
12
13
  "format": "npx prettier --write .",
13
14
  "test": "ts-mocha -p tsconfig.json --recursive 'test/**/*.test*' --exclude 'test/e2e/*' --timeout 20000",
14
- "test:e2e:node-module": "ts-mocha -p tsconfig.json --recursive 'test/e2e/*.test*' --timeout 120000",
15
+ "test:e2e:node-module": "ts-mocha -p tsconfig.json --recursive 'test/e2e/*.test*' --timeout 150000",
15
16
  "test:e2e:cli": "sh ./test/e2e/e2e-cli.sh",
16
17
  "test:coverage": "nyc npm run test && nyc report --reporter=lcov",
17
18
  "build": "rimraf ./lib && npx tsc",
@@ -33,15 +34,15 @@
33
34
  "homepage": "https://github.com/auth0/auth0-deploy-cli#readme",
34
35
  "dependencies": {
35
36
  "ajv": "^6.12.6",
36
- "auth0": "^5.4.0",
37
+ "auth0": "^5.6.0",
37
38
  "dot-prop": "^5.3.0",
38
39
  "fs-extra": "^10.1.0",
39
40
  "js-yaml": "^4.1.1",
40
- "lodash": "^4.17.23",
41
+ "lodash": "^4.18.1",
41
42
  "mkdirp": "^1.0.4",
42
43
  "nconf": "^0.13.0",
43
44
  "promise-pool-executor": "^1.1.1",
44
- "sanitize-filename": "^1.6.3",
45
+ "sanitize-filename": "^1.6.4",
45
46
  "undici": "^7.24.3",
46
47
  "winston": "^3.19.0",
47
48
  "yargs": "^15.4.1"
@@ -52,8 +53,8 @@
52
53
  "@types/lodash": "^4.17.24",
53
54
  "@types/mocha": "^10.0.10",
54
55
  "@types/nconf": "^0.10.7",
55
- "@typescript-eslint/eslint-plugin": "^8.57.0",
56
- "@typescript-eslint/parser": "^8.57.0",
56
+ "@typescript-eslint/eslint-plugin": "^8.58.0",
57
+ "@typescript-eslint/parser": "^8.58.0",
57
58
  "chai": "^4.5.0",
58
59
  "chai-as-promised": "^7.1.2",
59
60
  "eslint": "^9.39.2",