auth0-deploy-cli 9.0.0-beta.2 → 9.0.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 (126) hide show
  1. package/AGENTS.md +2 -357
  2. package/CHANGELOG.md +158 -10
  3. package/CLAUDE.md +138 -2
  4. package/README.md +1 -1
  5. package/lib/context/defaults.d.ts +7 -0
  6. package/lib/context/defaults.js +74 -30
  7. package/lib/context/directory/handlers/actionModules.js +1 -4
  8. package/lib/context/directory/handlers/actions.js +1 -4
  9. package/lib/context/directory/handlers/attackProtection.js +1 -1
  10. package/lib/context/directory/handlers/branding.js +4 -1
  11. package/lib/context/directory/handlers/clientAuthCredentials.d.ts +5 -0
  12. package/lib/context/directory/handlers/clientAuthCredentials.js +13 -0
  13. package/lib/context/directory/handlers/clientGrants.js +78 -24
  14. package/lib/context/directory/handlers/clients.js +20 -3
  15. package/lib/context/directory/handlers/connections.js +19 -5
  16. package/lib/context/directory/handlers/databases.js +1 -4
  17. package/lib/context/directory/handlers/emailTemplates.js +5 -0
  18. package/lib/context/directory/handlers/guardianEmailFactorSettings.d.ts +5 -0
  19. package/lib/context/directory/handlers/guardianEmailFactorSettings.js +38 -0
  20. package/lib/context/directory/handlers/guardianPhoneFactorSettings.d.ts +5 -0
  21. package/lib/context/directory/handlers/guardianPhoneFactorSettings.js +38 -0
  22. package/lib/context/directory/handlers/guardianSettings.d.ts +5 -0
  23. package/lib/context/directory/handlers/guardianSettings.js +38 -0
  24. package/lib/context/directory/handlers/hooks.js +1 -4
  25. package/lib/context/directory/handlers/index.js +12 -0
  26. package/lib/context/directory/handlers/networkACLKeys.d.ts +6 -0
  27. package/lib/context/directory/handlers/networkACLKeys.js +54 -0
  28. package/lib/context/directory/handlers/prompts.js +7 -3
  29. package/lib/context/directory/handlers/rateLimitPolicies.d.ts +6 -0
  30. package/lib/context/directory/handlers/rateLimitPolicies.js +53 -0
  31. package/lib/context/directory/handlers/rules.js +1 -4
  32. package/lib/context/directory/index.js +1 -4
  33. package/lib/context/index.js +31 -31
  34. package/lib/context/yaml/handlers/attackProtection.js +1 -1
  35. package/lib/context/yaml/handlers/branding.js +4 -1
  36. package/lib/context/yaml/handlers/clientAuthCredentials.d.ts +5 -0
  37. package/lib/context/yaml/handlers/clientAuthCredentials.js +13 -0
  38. package/lib/context/yaml/handlers/clients.js +19 -1
  39. package/lib/context/yaml/handlers/connections.js +8 -1
  40. package/lib/context/yaml/handlers/flows.js +3 -0
  41. package/lib/context/yaml/handlers/forms.js +3 -0
  42. package/lib/context/yaml/handlers/guardianEmailFactorSettings.d.ts +5 -0
  43. package/lib/context/yaml/handlers/guardianEmailFactorSettings.js +15 -0
  44. package/lib/context/yaml/handlers/guardianPhoneFactorSettings.d.ts +5 -0
  45. package/lib/context/yaml/handlers/guardianPhoneFactorSettings.js +15 -0
  46. package/lib/context/yaml/handlers/guardianSettings.d.ts +5 -0
  47. package/lib/context/yaml/handlers/guardianSettings.js +15 -0
  48. package/lib/context/yaml/handlers/index.js +12 -0
  49. package/lib/context/yaml/handlers/networkACLKeys.d.ts +6 -0
  50. package/lib/context/yaml/handlers/networkACLKeys.js +42 -0
  51. package/lib/context/yaml/handlers/prompts.js +3 -1
  52. package/lib/context/yaml/handlers/rateLimitPolicies.d.ts +6 -0
  53. package/lib/context/yaml/handlers/rateLimitPolicies.js +27 -0
  54. package/lib/context/yaml/index.js +1 -4
  55. package/lib/keywordPreservation.d.ts +1 -1
  56. package/lib/keywordPreservation.js +2 -1
  57. package/lib/readonly.js +5 -0
  58. package/lib/tools/auth0/handlers/attackProtection.js +19 -6
  59. package/lib/tools/auth0/handlers/clientAuthCredentials.d.ts +27 -0
  60. package/lib/tools/auth0/handlers/clientAuthCredentials.js +234 -0
  61. package/lib/tools/auth0/handlers/clientAuthCredentialsPre.d.ts +38 -0
  62. package/lib/tools/auth0/handlers/clientAuthCredentialsPre.js +123 -0
  63. package/lib/tools/auth0/handlers/clientGrants.d.ts +1 -1
  64. package/lib/tools/auth0/handlers/clients.d.ts +106 -0
  65. package/lib/tools/auth0/handlers/clients.js +206 -2
  66. package/lib/tools/auth0/handlers/connectionProfiles.d.ts +27 -0
  67. package/lib/tools/auth0/handlers/connectionProfiles.js +30 -0
  68. package/lib/tools/auth0/handlers/connections.d.ts +32 -6
  69. package/lib/tools/auth0/handlers/connections.js +49 -7
  70. package/lib/tools/auth0/handlers/databases.d.ts +28 -0
  71. package/lib/tools/auth0/handlers/databases.js +58 -24
  72. package/lib/tools/auth0/handlers/default.d.ts +23 -0
  73. package/lib/tools/auth0/handlers/default.js +47 -29
  74. package/lib/tools/auth0/handlers/eventStreams.js +10 -2
  75. package/lib/tools/auth0/handlers/guardianEmailFactorSettings.d.ts +22 -0
  76. package/lib/tools/auth0/handlers/guardianEmailFactorSettings.js +77 -0
  77. package/lib/tools/auth0/handlers/guardianFactorTemplates.js +13 -4
  78. package/lib/tools/auth0/handlers/guardianPhoneFactorMessageTypes.js +11 -9
  79. package/lib/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.js +11 -9
  80. package/lib/tools/auth0/handlers/guardianPhoneFactorSettings.d.ts +22 -0
  81. package/lib/tools/auth0/handlers/guardianPhoneFactorSettings.js +77 -0
  82. package/lib/tools/auth0/handlers/guardianPolicies.js +11 -1
  83. package/lib/tools/auth0/handlers/guardianSettings.d.ts +30 -0
  84. package/lib/tools/auth0/handlers/guardianSettings.js +85 -0
  85. package/lib/tools/auth0/handlers/hooks.js +1 -1
  86. package/lib/tools/auth0/handlers/index.js +16 -0
  87. package/lib/tools/auth0/handlers/networkACLKeys.d.ts +45 -0
  88. package/lib/tools/auth0/handlers/networkACLKeys.js +177 -0
  89. package/lib/tools/auth0/handlers/networkACLs.d.ts +132 -2
  90. package/lib/tools/auth0/handlers/networkACLs.js +100 -16
  91. package/lib/tools/auth0/handlers/organizations.d.ts +33 -1
  92. package/lib/tools/auth0/handlers/organizations.js +177 -19
  93. package/lib/tools/auth0/handlers/phoneProvider.js +10 -2
  94. package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
  95. package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
  96. package/lib/tools/auth0/handlers/prompts.js +1 -1
  97. package/lib/tools/auth0/handlers/rateLimitPolicies.d.ts +81 -0
  98. package/lib/tools/auth0/handlers/rateLimitPolicies.js +203 -0
  99. package/lib/tools/auth0/handlers/resourceServers.d.ts +44 -0
  100. package/lib/tools/auth0/handlers/resourceServers.js +44 -0
  101. package/lib/tools/auth0/handlers/riskAssessment.d.ts +1 -1
  102. package/lib/tools/auth0/handlers/riskAssessment.js +19 -1
  103. package/lib/tools/auth0/handlers/roles.d.ts +4 -0
  104. package/lib/tools/auth0/handlers/roles.js +10 -0
  105. package/lib/tools/auth0/handlers/rules.js +19 -7
  106. package/lib/tools/auth0/handlers/tenant.d.ts +45 -0
  107. package/lib/tools/auth0/handlers/tenant.js +48 -0
  108. package/lib/tools/auth0/handlers/themes.d.ts +37 -1
  109. package/lib/tools/auth0/handlers/themes.js +64 -0
  110. package/lib/tools/auth0/handlers/tokenExchangeProfiles.js +3 -1
  111. package/lib/tools/auth0/index.js +12 -6
  112. package/lib/tools/calculateDryRunChanges.js +12 -8
  113. package/lib/tools/constants.d.ts +2 -0
  114. package/lib/tools/constants.js +3 -0
  115. package/lib/tools/utils.d.ts +3 -0
  116. package/lib/tools/utils.js +41 -2
  117. package/lib/types.d.ts +9 -1
  118. package/lib/utils.d.ts +6 -0
  119. package/lib/utils.js +17 -2
  120. package/package.json +2 -2
  121. package/references/code-style.md +133 -0
  122. package/references/commands.md +98 -0
  123. package/references/docs-update.md +34 -0
  124. package/references/git-workflow.md +135 -0
  125. package/references/pitfalls.md +62 -0
  126. package/references/testing.md +118 -0
@@ -46,6 +46,7 @@ exports.schema = void 0;
46
46
  const logger_1 = __importDefault(require("../../../logger"));
47
47
  const default_1 = __importStar(require("./default"));
48
48
  const utils_1 = require("../../utils");
49
+ const calculateDryRunChanges_1 = require("../../calculateDryRunChanges");
49
50
  /**
50
51
  * Schema
51
52
  */
@@ -251,6 +252,41 @@ exports.schema = {
251
252
  pattern: '^[^<>]*$',
252
253
  type: 'string',
253
254
  },
255
+ identifiers: {
256
+ additionalProperties: false,
257
+ description: 'Identifier display settings on the theme. Requires the `universal_login_theme_identifiers` feature flag to be enabled on the tenant.',
258
+ properties: {
259
+ login_display: {
260
+ description: 'Login display mode',
261
+ enum: ['separate', 'unified'],
262
+ type: 'string',
263
+ },
264
+ otp_autocomplete: {
265
+ description: 'Whether OTP autocomplete is enabled',
266
+ type: 'boolean',
267
+ },
268
+ phone_display: {
269
+ additionalProperties: false,
270
+ description: 'Phone number display settings',
271
+ properties: {
272
+ formatting: {
273
+ description: 'Phone number formatting style',
274
+ enum: ['international', 'regional'],
275
+ type: 'string',
276
+ },
277
+ masking: {
278
+ description: 'Phone number masking strategy',
279
+ enum: ['hide_country_code', 'mask_digits', 'show_all'],
280
+ type: 'string',
281
+ },
282
+ },
283
+ required: ['formatting', 'masking'],
284
+ type: 'object',
285
+ },
286
+ },
287
+ required: ['login_display', 'otp_autocomplete', 'phone_display'],
288
+ type: 'object',
289
+ },
254
290
  fonts: {
255
291
  additionalProperties: false,
256
292
  properties: {
@@ -470,6 +506,34 @@ class ThemesHandler extends default_1.default {
470
506
  objString(theme) {
471
507
  return theme.displayName || JSON.stringify(theme);
472
508
  }
509
+ async dryRunChanges(assets) {
510
+ const { themes } = assets;
511
+ if (!themes) {
512
+ return { del: [], create: [], conflicts: [], update: [] };
513
+ }
514
+ const existing = await this.getType();
515
+ // getType() returns null when Universal Login customizations are not enabled
516
+ // on the tenant (the 400 "no-code not enabled" path in getThemes).
517
+ if (existing === null) {
518
+ return { del: [], create: [], conflicts: [], update: [] };
519
+ }
520
+ // Themes are singletons — at most one per tenant. If the local theme has no
521
+ // themeId (stripped during export when --export_ids is not set), backfill it
522
+ // from the remote so the matcher can find a match without requiring --export_ids.
523
+ const normalizedThemes = themes.map((theme) => {
524
+ if (!theme.themeId && existing.length > 0 && existing[0].themeId) {
525
+ return { ...theme, themeId: existing[0].themeId };
526
+ }
527
+ return theme;
528
+ });
529
+ return (0, calculateDryRunChanges_1.calculateDryRunChanges)({
530
+ type: this.type,
531
+ assets: normalizedThemes,
532
+ existing,
533
+ identifiers: this.identifiers,
534
+ ignoreDryRunFields: this.getEffectiveIgnoreDryRunFields(),
535
+ });
536
+ }
473
537
  async getType() {
474
538
  if (!this.existing) {
475
539
  this.existing = await this.getThemes();
@@ -138,7 +138,9 @@ class TokenExchangeProfilesHandler extends default_1.default {
138
138
  }
139
139
  catch (err) {
140
140
  if (err.statusCode === 403) {
141
- logger_1.default.warn('Token Exchange Profiles feature is not available on this tenant. Please contact Auth0 support to enable this feature.');
141
+ logger_1.default.warn(`Cannot retrieve tokenExchangeProfiles: ${err.message}. ` +
142
+ `Verify the M2M application has the 'read:token_exchange_profiles' scope and ` +
143
+ `your subscription plan includes Token Exchange Profiles.`);
142
144
  return [];
143
145
  }
144
146
  throw err;
@@ -11,13 +11,10 @@ const promise_pool_executor_1 = require("promise-pool-executor");
11
11
  const client_1 = __importDefault(require("./client"));
12
12
  const schema_1 = __importDefault(require("./schema"));
13
13
  const handlers_1 = __importDefault(require("./handlers"));
14
- // Cache the compiled AJV validator to avoid recompiling the schema on every validate() call.
15
- // Schema compilation is expensive (~5s) due to the complexity of the combined resource schema.
16
- const ajv = new ajv_1.default({ useDefaults: true, nullable: true });
17
- const validateSchema = ajv.compile(schema_1.default);
18
14
  const calculateDryRunChanges_1 = require("../calculateDryRunChanges");
19
15
  const utils_1 = require("../../utils");
20
16
  const utils_2 = require("../utils");
17
+ const logger_1 = __importDefault(require("../../logger"));
21
18
  /**
22
19
  * Sorts handlers by their @order decorator metadata for a given stage.
23
20
  * Handlers are sorted in ascending order (lower values execute first).
@@ -109,12 +106,13 @@ class Auth0 {
109
106
  }
110
107
  }
111
108
  async validate() {
109
+ const ajv = new ajv_1.default({ useDefaults: true, nullable: true });
112
110
  const nonNullAssets = Object.keys(this.assets)
113
111
  .filter((k) => this.assets[k] != null)
114
112
  .reduce((a, k) => ({ ...a, [k]: this.assets[k] }), {});
115
- const valid = validateSchema(nonNullAssets);
113
+ const valid = ajv.validate(schema_1.default, nonNullAssets);
116
114
  if (!valid) {
117
- throw new Error(`Schema validation failed loading ${JSON.stringify(validateSchema.errors, null, 4)}`);
115
+ throw new Error(`Schema validation failed loading ${JSON.stringify(ajv.errors, null, 4)}`);
118
116
  }
119
117
  await this.runStage('validate');
120
118
  }
@@ -148,6 +146,14 @@ class Auth0 {
148
146
  data: this.handlers,
149
147
  generator: (handler) => (async () => {
150
148
  try {
149
+ // Every handler in the dry-run loop is expected to implement dryRunChanges.
150
+ // Guard against non-conforming handlers (e.g. deploy-only handlers that don't
151
+ // extend the base APIHandler) so a missing method skips the handler instead of
152
+ // crashing the whole dry run. Logged loudly so an omitted preview is visible.
153
+ if (typeof handler.dryRunChanges !== 'function') {
154
+ logger_1.default.warn(`Handler "${handler.type}" does not implement dryRunChanges; skipping it in the dry-run preview. Changes for this resource will still be applied during a real import.`);
155
+ return;
156
+ }
151
157
  const detailedChanges = [];
152
158
  let created = 0;
153
159
  let updated = 0;
@@ -108,6 +108,13 @@ function normalizeArrayValues(values) {
108
108
  .map((value) => normalizeValue(value))
109
109
  .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
110
110
  }
111
+ /**
112
+ * Serializes a value to a human-readable string for use in diff messages.
113
+ * Objects and arrays are JSON-stringified to avoid "[object Object]" output.
114
+ */
115
+ function formatDiffValue(value) {
116
+ return typeof value === 'object' && value !== null ? JSON.stringify(value) : String(value);
117
+ }
111
118
  /**
112
119
  * Writes the accumulated diff log to a JSON file on disk.
113
120
  * Useful for CI pipelines that want a machine-readable dry-run report.
@@ -179,7 +186,7 @@ function getObjectDifferences(localObj, remoteObj, keyObjPath = '', resourceType
179
186
  differences.push(...nestedDifferences);
180
187
  }
181
188
  else if (item !== normalizedRemoteValue[index]) {
182
- const message = `Array item difference at [${currentPath}[${index}]]: local:${item} vs remote:${normalizedRemoteValue[index]}`;
189
+ const message = `Array item difference at [${currentPath}[${index}]]: local:${formatDiffValue(item)} vs remote:${formatDiffValue(normalizedRemoteValue[index])}`;
183
190
  differences.push(message);
184
191
  }
185
192
  });
@@ -204,13 +211,10 @@ function getObjectDifferences(localObj, remoteObj, keyObjPath = '', resourceType
204
211
  differences.push(...nestedDifferences);
205
212
  return;
206
213
  }
207
- // Compare primitive values — omit array indices from path to reduce log noise
214
+ // Compare primitive values
208
215
  if (localValue !== remoteValue) {
209
- let arrayPathRegex = new RegExp(/\[\d+\]/g);
210
- if (!arrayPathRegex.test(currentPath)) {
211
- const message = `Value difference for [${currentPath}]: local:${localValue} vs remote:${remoteValue}`;
212
- differences.push(message);
213
- }
216
+ const message = `Value difference for [${currentPath}]: local:${localValue} vs remote:${remoteValue}`;
217
+ differences.push(message);
214
218
  }
215
219
  });
216
220
  return differences;
@@ -242,7 +246,7 @@ function calculateDryRunChanges({ type, assets, existing, identifiers = ['id', '
242
246
  const create = [];
243
247
  const conflicts = [];
244
248
  const localAssets = (Array.isArray(assets) ? [...assets] : [assets]).map((asset) => type === 'tenant' ? normalizeTenantForDryRun(asset) : asset);
245
- const remoteAssets = (Array.isArray(existing) ? [...existing] : [existing]).map((asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset));
249
+ const remoteAssets = (Array.isArray(existing) ? [...existing] : existing ? [existing] : []).map((asset) => (type === 'tenant' ? normalizeTenantForDryRun(asset) : asset));
246
250
  // Helper: returns true if a local and remote asset share at least one identifier value
247
251
  const assetsMatch = (localAsset, remoteAsset) => identifiers.some((id) => {
248
252
  if (Array.isArray(id)) {
@@ -86,10 +86,12 @@ declare const constants: {
86
86
  FLOWS_VAULT_DIRECTORY: string;
87
87
  SELF_SERVICE_PROFILE_DIRECTORY: string;
88
88
  NETWORK_ACLS_DIRECTORY: string;
89
+ NETWORK_ACL_KEYS_DIRECTORY: string;
89
90
  USER_ATTRIBUTE_PROFILES_DIRECTORY: string;
90
91
  CONNECTION_PROFILES_DIRECTORY: string;
91
92
  TOKEN_EXCHANGE_PROFILES_DIRECTORY: string;
92
93
  SUPPLEMENTAL_SIGNALS_DIRECTORY: string;
94
+ RATE_LIMIT_POLICIES_DIRECTORY: string;
93
95
  EVENT_STREAMS_DIRECTORY: string;
94
96
  };
95
97
  export default constants;
@@ -68,6 +68,7 @@ const constants = {
68
68
  'custom-token-exchange',
69
69
  'login-post-identifier',
70
70
  'signup-post-identifier',
71
+ 'post-credential-validation',
71
72
  ],
72
73
  EMAIL_TEMPLATES_DIRECTORY: 'emails',
73
74
  EMAIL_VERIFY,
@@ -216,10 +217,12 @@ const constants = {
216
217
  FLOWS_VAULT_DIRECTORY: 'flow-vault-connections',
217
218
  SELF_SERVICE_PROFILE_DIRECTORY: 'self-service-profiles',
218
219
  NETWORK_ACLS_DIRECTORY: 'network-acls',
220
+ NETWORK_ACL_KEYS_DIRECTORY: 'network-acl-keys',
219
221
  USER_ATTRIBUTE_PROFILES_DIRECTORY: 'user-attribute-profiles',
220
222
  CONNECTION_PROFILES_DIRECTORY: 'connection-profiles',
221
223
  TOKEN_EXCHANGE_PROFILES_DIRECTORY: 'token-exchange-profiles',
222
224
  SUPPLEMENTAL_SIGNALS_DIRECTORY: 'supplemental-signals',
225
+ RATE_LIMIT_POLICIES_DIRECTORY: 'rate-limit-policies',
223
226
  EVENT_STREAMS_DIRECTORY: 'event-streams',
224
227
  };
225
228
  exports.default = constants;
@@ -27,6 +27,7 @@ export declare function areArraysEquals(x: any[], y: any[]): boolean;
27
27
  export declare const obfuscateSensitiveValues: (data: Asset | Asset[] | null, sensitiveFieldsToObfuscate: string[]) => Asset | Asset[] | null;
28
28
  export declare const validateNoUnresolvedPlaceholders: (data: Asset | null, resourceType: string, resourceName: string) => Asset | null;
29
29
  export declare const stripObfuscatedFieldsFromPayload: (data: Asset | Asset[] | null, obfuscatedFields: string[]) => Asset | Asset[] | null;
30
+ export declare const stripUnresolvedPlaceholders: (data: Asset | null, resourceType: string, resourceName: string) => Asset | null;
30
31
  export declare const detectInsufficientScopeError: <T>(fn: Function) => Promise<{
31
32
  hadSufficientScopes: true;
32
33
  data: T;
@@ -41,6 +42,8 @@ export declare const isDeprecatedError: (err: {
41
42
  message: string;
42
43
  statusCode: number;
43
44
  }) => boolean;
45
+ export declare const isFeatureUnavailableError: (err: any) => boolean;
46
+ export declare const isInsufficientEntitlementError: (err: unknown) => boolean;
44
47
  export declare const isForbiddenFeatureError: (err: any, type: any) => boolean;
45
48
  export declare function maskSecretAtPath({ resourceTypeName, maskedKeyName, maskOnObj, keyJsonPath, }: {
46
49
  resourceTypeName: string;
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.shouldExcludeThirdPartyClients = exports.isForbiddenFeatureError = exports.isDeprecatedError = exports.detectInsufficientScopeError = exports.stripObfuscatedFieldsFromPayload = exports.validateNoUnresolvedPlaceholders = exports.obfuscateSensitiveValues = exports.keywordReplaceStringRegExp = exports.keywordReplaceArrayRegExp = void 0;
39
+ exports.shouldExcludeThirdPartyClients = exports.isForbiddenFeatureError = exports.isInsufficientEntitlementError = exports.isFeatureUnavailableError = exports.isDeprecatedError = exports.detectInsufficientScopeError = exports.stripUnresolvedPlaceholders = exports.stripObfuscatedFieldsFromPayload = exports.validateNoUnresolvedPlaceholders = exports.obfuscateSensitiveValues = exports.keywordReplaceStringRegExp = exports.keywordReplaceArrayRegExp = void 0;
40
40
  exports.keywordArrayReplace = keywordArrayReplace;
41
41
  exports.keywordStringReplace = keywordStringReplace;
42
42
  exports.keywordReplace = keywordReplace;
@@ -309,6 +309,24 @@ const stripObfuscatedFieldsFromPayload = (data, obfuscatedFields) => {
309
309
  return newAsset;
310
310
  };
311
311
  exports.stripObfuscatedFieldsFromPayload = stripObfuscatedFieldsFromPayload;
312
+ // Strips unresolved ##...## / @@...@@ placeholders before sending to Auth0.
313
+ // Keyword replacement runs first; any remaining placeholder means no mapping was
314
+ // provided. The field is dropped so Auth0 preserves its existing value.
315
+ const stripUnresolvedPlaceholders = (data, resourceType, resourceName) => {
316
+ if (data === null)
317
+ return data;
318
+ const unresolved = collectUnresolvedPlaceholders(data);
319
+ if (unresolved.length === 0)
320
+ return data;
321
+ const newAsset = { ...data };
322
+ unresolved.forEach(({ path, value }) => {
323
+ logger_1.default.warn(`Stripping unresolved placeholder for ${resourceType} "${resourceName}" field "${path}": ${value}. ` +
324
+ `To use this value, define ${value.replace(/^##|##$|^@@|@@$/g, '')} in AUTH0_KEYWORD_REPLACE_MAPPINGS. The existing value on Auth0 will be preserved.`);
325
+ dot_prop_1.default.delete(newAsset, path);
326
+ });
327
+ return newAsset;
328
+ };
329
+ exports.stripUnresolvedPlaceholders = stripUnresolvedPlaceholders;
312
330
  const detectInsufficientScopeError = async (fn) => {
313
331
  try {
314
332
  const data = await fn();
@@ -340,9 +358,30 @@ const isDeprecatedError = (err) => {
340
358
  return !!(err.statusCode === 403 || err.message?.includes('deprecated feature'));
341
359
  };
342
360
  exports.isDeprecatedError = isDeprecatedError;
361
+ const isFeatureUnavailableError = (err) => {
362
+ // A 404 indicates an older Management API version where the endpoint is not available.
363
+ // 403s (feature explicitly disabled) are handled by isForbiddenFeatureError.
364
+ return err.statusCode === 404;
365
+ };
366
+ exports.isFeatureUnavailableError = isFeatureUnavailableError;
367
+ const isInsufficientEntitlementError = (err) => {
368
+ if (!err || typeof err !== 'object')
369
+ return false;
370
+ const e = err;
371
+ if (e.statusCode !== 403)
372
+ return false;
373
+ const body = e.body;
374
+ return body?.errorCode === 'insufficient_entitlement';
375
+ };
376
+ exports.isInsufficientEntitlementError = isInsufficientEntitlementError;
343
377
  const isForbiddenFeatureError = (err, type) => {
344
378
  if (err.statusCode === 403) {
345
- logger_1.default.warn(`${err.message};${err.errorCode ?? ''} - Skipping ${type}`);
379
+ // The SDK error's top-level `message` is the full serialized response body; the clean,
380
+ // human-readable message and errorCode live on the response body itself.
381
+ const body = err.originalError?.response?.body ?? {};
382
+ const message = body.message ?? err.message;
383
+ const errorCode = body.errorCode ?? err.errorCode ?? '';
384
+ logger_1.default.warn(`${message}${errorCode ? ` (${errorCode})` : ''} - Skipping ${type}`);
346
385
  return true;
347
386
  }
348
387
  return false;
package/lib/types.d.ts CHANGED
@@ -16,11 +16,13 @@ import { SsProfileWithCustomText } from './tools/auth0/handlers/selfServiceProfi
16
16
  import { PhoneProvider } from './tools/auth0/handlers/phoneProvider';
17
17
  import { PhoneTemplate } from './tools/auth0/handlers/phoneTemplates';
18
18
  import { NetworkACL } from './tools/auth0/handlers/networkACLs';
19
+ import { NetworkAclKey } from './tools/auth0/handlers/networkACLKeys';
19
20
  import { UserAttributeProfile } from './tools/auth0/handlers/userAttributeProfiles';
20
21
  import { AttackProtection } from './tools/auth0/handlers/attackProtection';
21
22
  import { TokenExchangeProfile } from './tools/auth0/handlers/tokenExchangeProfiles';
22
23
  import { RiskAssessment } from './tools/auth0/handlers/riskAssessment';
23
24
  import { SupplementalSignals } from './tools/auth0/handlers/supplementalSignals';
25
+ import { RateLimitPolicy } from './tools/auth0/handlers/rateLimitPolicies';
24
26
  import { EventStream } from './tools/auth0/handlers/eventStreams';
25
27
  type SharedPaginationParams = {
26
28
  checkpoint?: boolean;
@@ -30,6 +32,7 @@ type SharedPaginationParams = {
30
32
  include_totals?: boolean;
31
33
  id?: string;
32
34
  strategy?: Management.ConnectionStrategyEnum[];
35
+ type?: Management.RoleTypeEnum;
33
36
  };
34
37
  export type CheckpointPaginationParams = SharedPaginationParams & {
35
38
  from?: string;
@@ -127,6 +130,9 @@ export type Assets = Partial<{
127
130
  message_types: Asset[];
128
131
  } | null;
129
132
  guardianPhoneFactorSelectedProvider: Asset | null;
133
+ guardianPhoneFactorSettings: Asset | null;
134
+ guardianEmailFactorSettings: Asset | null;
135
+ guardianSettings: Asset | null;
130
136
  guardianPolicies: {
131
137
  policies: string[];
132
138
  } | null;
@@ -154,11 +160,13 @@ export type Assets = Partial<{
154
160
  flows: Flow[] | null;
155
161
  flowVaultConnections: FlowVaultConnection[] | null;
156
162
  selfServiceProfiles: SsProfileWithCustomText[] | null;
163
+ networkACLKeys: NetworkAclKey[] | null;
157
164
  networkACLs: NetworkACL[] | null;
158
165
  userAttributeProfiles: UserAttributeProfile[] | null;
159
166
  userAttributeProfilesWithId: UserAttributeProfile[] | null;
160
167
  connectionProfiles: Asset[] | null;
161
168
  tokenExchangeProfiles: TokenExchangeProfile[] | null;
169
+ rateLimitPolicies: RateLimitPolicy[] | null;
162
170
  eventStreams: EventStream[] | null;
163
171
  }>;
164
172
  export type CalculatedChanges = {
@@ -180,7 +188,7 @@ export type DetailedDryRunChanges = {
180
188
  changes: DetailedDryRunChange[];
181
189
  };
182
190
  };
183
- export type AssetTypes = 'rules' | 'rulesConfigs' | 'hooks' | 'pages' | 'databases' | 'clientGrants' | 'resourceServers' | 'clients' | 'connections' | 'tenant' | 'emailProvider' | 'emailTemplates' | 'guardianFactors' | 'guardianFactorProviders' | 'guardianFactorTemplates' | 'guardianPhoneFactorMessageTypes' | 'guardianPhoneFactorSelectedProvider' | 'guardianPolicies' | 'roles' | 'actions' | 'actionModules' | 'organizations' | 'triggers' | 'attackProtection' | 'riskAssessment' | 'branding' | 'phoneProviders' | 'phoneTemplates' | 'logStreams' | 'prompts' | 'customDomains' | 'themes' | 'forms' | 'flows' | 'flowVaultConnections' | 'selfServiceProfiles' | 'networkACLs' | 'userAttributeProfiles' | 'connectionProfiles' | 'tokenExchangeProfiles' | 'supplementalSignals' | 'eventStreams';
191
+ export type AssetTypes = 'rules' | 'rulesConfigs' | 'hooks' | 'pages' | 'databases' | 'clientGrants' | 'resourceServers' | 'clients' | 'connections' | 'tenant' | 'emailProvider' | 'emailTemplates' | 'guardianFactors' | 'guardianFactorProviders' | 'guardianFactorTemplates' | 'guardianPhoneFactorMessageTypes' | 'guardianPhoneFactorSelectedProvider' | 'guardianPhoneFactorSettings' | 'guardianEmailFactorSettings' | 'guardianSettings' | 'guardianPolicies' | 'roles' | 'actions' | 'actionModules' | 'organizations' | 'triggers' | 'attackProtection' | 'riskAssessment' | 'branding' | 'phoneProviders' | 'phoneTemplates' | 'logStreams' | 'prompts' | 'customDomains' | 'themes' | 'forms' | 'flows' | 'flowVaultConnections' | 'selfServiceProfiles' | 'networkACLKeys' | 'networkACLs' | 'userAttributeProfiles' | 'connectionProfiles' | 'tokenExchangeProfiles' | 'supplementalSignals' | 'rateLimitPolicies' | 'eventStreams' | 'clientAuthCredentials';
184
192
  export type KeywordMappings = {
185
193
  [key: string]: (string | number)[] | string | number;
186
194
  };
package/lib/utils.d.ts CHANGED
@@ -52,4 +52,10 @@ export declare const getFormattedOptions: (connection: any, clients: any) => {
52
52
  } | {
53
53
  options?: undefined;
54
54
  };
55
+ /**
56
+ * Asserts that `resolvedPath` is inside `configRoot`.
57
+ * Throws a hard error on path traversal attempts so there is a single,
58
+ * canonical enforcement point across all handlers.
59
+ */
60
+ export declare function assertInsideConfigRoot(input: string, resolvedPath: string, configRoot: string): void;
55
61
  export {};
package/lib/utils.js CHANGED
@@ -23,6 +23,7 @@ exports.convertClientIdToName = convertClientIdToName;
23
23
  exports.hasKeywordMarkers = hasKeywordMarkers;
24
24
  exports.mapClientID2NameSorted = mapClientID2NameSorted;
25
25
  exports.nomalizedYAMLPath = nomalizedYAMLPath;
26
+ exports.assertInsideConfigRoot = assertInsideConfigRoot;
26
27
  const path_1 = __importDefault(require("path"));
27
28
  const fs_extra_1 = __importDefault(require("fs-extra"));
28
29
  const nconf_1 = __importDefault(require("nconf"));
@@ -301,12 +302,15 @@ exports.decodeBase64ToCertString = decodeBase64ToCertString;
301
302
  // Format connection options by converting client IDs to client names for SAML connections
302
303
  const getFormattedOptions = (connection, clients) => {
303
304
  try {
305
+ const { idpinitiated } = connection.options;
304
306
  return {
305
307
  options: {
306
308
  ...connection.options,
307
309
  idpinitiated: {
308
- ...connection.options.idpinitiated,
309
- client_id: convertClientIdToName(connection.options.idpinitiated.client_id, clients),
310
+ ...idpinitiated,
311
+ ...(idpinitiated.client_id && {
312
+ client_id: convertClientIdToName(idpinitiated.client_id, clients),
313
+ }),
310
314
  },
311
315
  },
312
316
  };
@@ -316,3 +320,14 @@ const getFormattedOptions = (connection, clients) => {
316
320
  }
317
321
  };
318
322
  exports.getFormattedOptions = getFormattedOptions;
323
+ /**
324
+ * Asserts that `resolvedPath` is inside `configRoot`.
325
+ * Throws a hard error on path traversal attempts so there is a single,
326
+ * canonical enforcement point across all handlers.
327
+ */
328
+ function assertInsideConfigRoot(input, resolvedPath, configRoot) {
329
+ if (!resolvedPath.startsWith(configRoot + path_1.default.sep)) {
330
+ throw new Error(`Path traversal blocked: "${input}" resolves to "${resolvedPath}" which is outside the config directory "${configRoot}". ` +
331
+ `Move the file inside your config directory.`);
332
+ }
333
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth0-deploy-cli",
3
- "version": "9.0.0-beta.2",
3
+ "version": "9.0.0",
4
4
  "description": "A command line tool for deploying updates to your Auth0 tenant",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "dependencies": {
36
36
  "@clack/prompts": "1.4.0",
37
37
  "ajv": "^6.12.6",
38
- "auth0": "^5.13.0",
38
+ "auth0": "^7.2.0",
39
39
  "chalk": "5.6.2",
40
40
  "dot-prop": "^5.3.0",
41
41
  "fs-extra": "^10.1.0",
@@ -0,0 +1,133 @@
1
+ # Code Style Reference — auth0-deploy-cli
2
+
3
+ ## Naming Conventions
4
+
5
+ | Element | Convention | Example |
6
+ | -------------------- | ---------------- | ----------------------------------- |
7
+ | Source files | camelCase | `clients.ts`, `attackProtection.ts` |
8
+ | Classes | PascalCase | `ClientsHandler`, `YAMLContext` |
9
+ | Methods / variables | camelCase | `processChanges`, `calcChanges` |
10
+ | Constants / env vars | UPPER_SNAKE_CASE | `AUTH0_ALLOW_DELETE` |
11
+ | Test files | `<name>.test.js` | `clients.test.js` |
12
+
13
+ ## Handler Implementation Pattern
14
+
15
+ Every resource handler in `src/tools/auth0/handlers/` must implement four methods:
16
+
17
+ ```typescript
18
+ class ResourceHandler extends DefaultHandler {
19
+ // Fetch current state from Auth0 API
20
+ async getType(): Promise<Asset[]> { ... }
21
+
22
+ // Determine create / update / delete operations
23
+ async calcChanges(assets: Assets): Promise<Changes> { ... }
24
+
25
+ // Execute CRUD operations — must be a no-op in dry run
26
+ async processChanges(assets: Assets, changes: Changes): Promise<void> { ... }
27
+
28
+ // Validate assets before processing; base class provides a default
29
+ async validate(assets: Assets): Promise<void> { ... }
30
+ }
31
+ ```
32
+
33
+ > `dump()` / export formatting is handled by `YAMLContext` and `DirectoryContext`, not by handlers.
34
+
35
+ Register the handler in `src/tools/constants.ts`. Handler schemas are defined inline in the handler file and aggregated by `src/tools/auth0/schema.ts`.
36
+
37
+ ## Identifiers
38
+
39
+ ```typescript
40
+ // ✅ Stable name-based identifier (passed as constructor option to super())
41
+ super({ ...options, identifiers: ['name'] });
42
+
43
+ // ✅ ID alongside name (when the API uses both for matching)
44
+ super({ ...options, identifiers: ['client_id', 'name'] });
45
+
46
+ // 🚫 Never — sole auto-generated UUID breaks cross-tenant portability
47
+ super({ ...options, identifiers: ['id'] });
48
+ ```
49
+
50
+ ## Keyword Replacement
51
+
52
+ ```yaml
53
+ # @@KEY@@ — JSON-stringified (arrays, objects, booleans, numbers)
54
+ allowed_clients: @@ALLOWED_CLIENTS@@
55
+
56
+ # ##KEY## — literal string substitution (plain strings only)
57
+ name: "##APP_NAME##"
58
+ ```
59
+
60
+ Both patterns must be preserved in every new field that flows through export/import. Mixing them up causes silent corruption — a `##KEY##` around a JSON array produces invalid YAML.
61
+
62
+ ## Good vs Bad Handler Example
63
+
64
+ **✅ Good — follows the pattern, respects dry-run and ALLOW_DELETE:**
65
+
66
+ ```typescript
67
+ async processChanges(assets: Assets, changes: Changes): Promise<void> {
68
+ if (this.config('AUTH0_ALLOW_DELETE')) {
69
+ await Promise.all(changes.delete.map((r) => this.client.resource.delete({ id: r.id })));
70
+ }
71
+ await Promise.all(changes.create.map((r) => this.client.resource.create(r)));
72
+ await Promise.all(changes.update.map((r) => this.client.resource.update({ id: r.id }, r)));
73
+ }
74
+ ```
75
+
76
+ **❌ Bad — logs a secret, missing ALLOW_DELETE guard:**
77
+
78
+ ```typescript
79
+ async processChanges(assets: Assets, changes: Changes): Promise<void> {
80
+ console.log('client_secret:', assets.clients[0].client_secret); // never log secrets
81
+ // missing AUTH0_ALLOW_DELETE check — deletes happen unconditionally
82
+ await Promise.all(changes.delete.map((r) => this.client.clients.delete({ client_id: r.client_id })));
83
+ }
84
+ ```
85
+
86
+ ## @order() Decorator
87
+
88
+ Handlers run in dependency order. Set `@order(N)` so that handlers whose resources are depended upon by others run first:
89
+
90
+ ```typescript
91
+ @order(30) // runs after Connections (20) and before Rules (40)
92
+ export default class ClientsHandler extends DefaultHandler { ... }
93
+ ```
94
+
95
+ Wrong order causes 404s or constraint errors mid-deploy.
96
+
97
+ ## Adding a New Resource Handler
98
+
99
+ 1. Create handler in `src/tools/auth0/handlers/<resource>.ts`
100
+ 2. Extend `DefaultHandler` and implement all four methods (`getType`, `calcChanges`, `processChanges`, `validate`)
101
+ 3. Add the resource type constant to `src/tools/constants.ts`
102
+ 4. Define the JSON schema inline in the handler; it is aggregated by `src/tools/auth0/schema.ts`
103
+ 5. Write unit tests in `test/tools/auth0/handlers/<resource>.test.js`
104
+ 6. Add E2E test coverage if applicable (or document why it's not possible)
105
+ 7. Update `docs/resource-specific-documentation.md` and `examples/` to demonstrate the new resource
106
+
107
+ ## Context Parser Details
108
+
109
+ - **DirectoryContext** (`src/context/directory/`): loads each resource type from separate nested JSON files
110
+ - **YAMLContext** (`src/context/yaml/`): parses a single `tenant.yaml` file with all resources inline
111
+ - Both support `@@KEY@@` and `##KEY##` keyword substitution
112
+ - Context tests use temporary directories with fixtures in `test/context/{directory,yaml}/`
113
+
114
+ ## Configuration and Error Handling
115
+
116
+ **Config priority order:** CLI args → env vars → config files → defaults
117
+
118
+ **Error handling conventions:**
119
+
120
+ - Use `ValidationError` from `src/tools/validationError.ts` for all validation failures
121
+ - Validate early, before making API calls
122
+ - Preserve context when bubbling errors up the call stack
123
+ - Provide clear, actionable error messages
124
+
125
+ ## Architecture: Request Flow
126
+
127
+ ```
128
+ src/index.ts CLI entry — routes export / import commands
129
+ src/commands/ import.ts, export.ts — load config and context
130
+ src/tools/deploy.ts Orchestrate deployment across all handlers
131
+ src/tools/auth0/handlers/* Resource-specific getType / calcChanges / processChanges
132
+ src/tools/auth0/client.ts Management API calls via the auth0 npm SDK
133
+ ```