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.
- package/AGENTS.md +2 -357
- package/CHANGELOG.md +158 -10
- package/CLAUDE.md +138 -2
- package/README.md +1 -1
- package/lib/context/defaults.d.ts +7 -0
- package/lib/context/defaults.js +74 -30
- package/lib/context/directory/handlers/actionModules.js +1 -4
- package/lib/context/directory/handlers/actions.js +1 -4
- package/lib/context/directory/handlers/attackProtection.js +1 -1
- package/lib/context/directory/handlers/branding.js +4 -1
- package/lib/context/directory/handlers/clientAuthCredentials.d.ts +5 -0
- package/lib/context/directory/handlers/clientAuthCredentials.js +13 -0
- package/lib/context/directory/handlers/clientGrants.js +78 -24
- package/lib/context/directory/handlers/clients.js +20 -3
- package/lib/context/directory/handlers/connections.js +19 -5
- package/lib/context/directory/handlers/databases.js +1 -4
- package/lib/context/directory/handlers/emailTemplates.js +5 -0
- package/lib/context/directory/handlers/guardianEmailFactorSettings.d.ts +5 -0
- package/lib/context/directory/handlers/guardianEmailFactorSettings.js +38 -0
- package/lib/context/directory/handlers/guardianPhoneFactorSettings.d.ts +5 -0
- package/lib/context/directory/handlers/guardianPhoneFactorSettings.js +38 -0
- package/lib/context/directory/handlers/guardianSettings.d.ts +5 -0
- package/lib/context/directory/handlers/guardianSettings.js +38 -0
- package/lib/context/directory/handlers/hooks.js +1 -4
- package/lib/context/directory/handlers/index.js +12 -0
- package/lib/context/directory/handlers/networkACLKeys.d.ts +6 -0
- package/lib/context/directory/handlers/networkACLKeys.js +54 -0
- package/lib/context/directory/handlers/prompts.js +7 -3
- package/lib/context/directory/handlers/rateLimitPolicies.d.ts +6 -0
- package/lib/context/directory/handlers/rateLimitPolicies.js +53 -0
- package/lib/context/directory/handlers/rules.js +1 -4
- package/lib/context/directory/index.js +1 -4
- package/lib/context/index.js +31 -31
- package/lib/context/yaml/handlers/attackProtection.js +1 -1
- package/lib/context/yaml/handlers/branding.js +4 -1
- package/lib/context/yaml/handlers/clientAuthCredentials.d.ts +5 -0
- package/lib/context/yaml/handlers/clientAuthCredentials.js +13 -0
- package/lib/context/yaml/handlers/clients.js +19 -1
- package/lib/context/yaml/handlers/connections.js +8 -1
- package/lib/context/yaml/handlers/flows.js +3 -0
- package/lib/context/yaml/handlers/forms.js +3 -0
- package/lib/context/yaml/handlers/guardianEmailFactorSettings.d.ts +5 -0
- package/lib/context/yaml/handlers/guardianEmailFactorSettings.js +15 -0
- package/lib/context/yaml/handlers/guardianPhoneFactorSettings.d.ts +5 -0
- package/lib/context/yaml/handlers/guardianPhoneFactorSettings.js +15 -0
- package/lib/context/yaml/handlers/guardianSettings.d.ts +5 -0
- package/lib/context/yaml/handlers/guardianSettings.js +15 -0
- package/lib/context/yaml/handlers/index.js +12 -0
- package/lib/context/yaml/handlers/networkACLKeys.d.ts +6 -0
- package/lib/context/yaml/handlers/networkACLKeys.js +42 -0
- package/lib/context/yaml/handlers/prompts.js +3 -1
- package/lib/context/yaml/handlers/rateLimitPolicies.d.ts +6 -0
- package/lib/context/yaml/handlers/rateLimitPolicies.js +27 -0
- package/lib/context/yaml/index.js +1 -4
- package/lib/keywordPreservation.d.ts +1 -1
- package/lib/keywordPreservation.js +2 -1
- package/lib/readonly.js +5 -0
- package/lib/tools/auth0/handlers/attackProtection.js +19 -6
- package/lib/tools/auth0/handlers/clientAuthCredentials.d.ts +27 -0
- package/lib/tools/auth0/handlers/clientAuthCredentials.js +234 -0
- package/lib/tools/auth0/handlers/clientAuthCredentialsPre.d.ts +38 -0
- package/lib/tools/auth0/handlers/clientAuthCredentialsPre.js +123 -0
- package/lib/tools/auth0/handlers/clientGrants.d.ts +1 -1
- package/lib/tools/auth0/handlers/clients.d.ts +106 -0
- package/lib/tools/auth0/handlers/clients.js +206 -2
- package/lib/tools/auth0/handlers/connectionProfiles.d.ts +27 -0
- package/lib/tools/auth0/handlers/connectionProfiles.js +30 -0
- package/lib/tools/auth0/handlers/connections.d.ts +32 -6
- package/lib/tools/auth0/handlers/connections.js +49 -7
- package/lib/tools/auth0/handlers/databases.d.ts +28 -0
- package/lib/tools/auth0/handlers/databases.js +58 -24
- package/lib/tools/auth0/handlers/default.d.ts +23 -0
- package/lib/tools/auth0/handlers/default.js +47 -29
- package/lib/tools/auth0/handlers/eventStreams.js +10 -2
- package/lib/tools/auth0/handlers/guardianEmailFactorSettings.d.ts +22 -0
- package/lib/tools/auth0/handlers/guardianEmailFactorSettings.js +77 -0
- package/lib/tools/auth0/handlers/guardianFactorTemplates.js +13 -4
- package/lib/tools/auth0/handlers/guardianPhoneFactorMessageTypes.js +11 -9
- package/lib/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.js +11 -9
- package/lib/tools/auth0/handlers/guardianPhoneFactorSettings.d.ts +22 -0
- package/lib/tools/auth0/handlers/guardianPhoneFactorSettings.js +77 -0
- package/lib/tools/auth0/handlers/guardianPolicies.js +11 -1
- package/lib/tools/auth0/handlers/guardianSettings.d.ts +30 -0
- package/lib/tools/auth0/handlers/guardianSettings.js +85 -0
- package/lib/tools/auth0/handlers/hooks.js +1 -1
- package/lib/tools/auth0/handlers/index.js +16 -0
- package/lib/tools/auth0/handlers/networkACLKeys.d.ts +45 -0
- package/lib/tools/auth0/handlers/networkACLKeys.js +177 -0
- package/lib/tools/auth0/handlers/networkACLs.d.ts +132 -2
- package/lib/tools/auth0/handlers/networkACLs.js +100 -16
- package/lib/tools/auth0/handlers/organizations.d.ts +33 -1
- package/lib/tools/auth0/handlers/organizations.js +177 -19
- package/lib/tools/auth0/handlers/phoneProvider.js +10 -2
- package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
- package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
- package/lib/tools/auth0/handlers/prompts.js +1 -1
- package/lib/tools/auth0/handlers/rateLimitPolicies.d.ts +81 -0
- package/lib/tools/auth0/handlers/rateLimitPolicies.js +203 -0
- package/lib/tools/auth0/handlers/resourceServers.d.ts +44 -0
- package/lib/tools/auth0/handlers/resourceServers.js +44 -0
- package/lib/tools/auth0/handlers/riskAssessment.d.ts +1 -1
- package/lib/tools/auth0/handlers/riskAssessment.js +19 -1
- package/lib/tools/auth0/handlers/roles.d.ts +4 -0
- package/lib/tools/auth0/handlers/roles.js +10 -0
- package/lib/tools/auth0/handlers/rules.js +19 -7
- package/lib/tools/auth0/handlers/tenant.d.ts +45 -0
- package/lib/tools/auth0/handlers/tenant.js +48 -0
- package/lib/tools/auth0/handlers/themes.d.ts +37 -1
- package/lib/tools/auth0/handlers/themes.js +64 -0
- package/lib/tools/auth0/handlers/tokenExchangeProfiles.js +3 -1
- package/lib/tools/auth0/index.js +12 -6
- package/lib/tools/calculateDryRunChanges.js +12 -8
- package/lib/tools/constants.d.ts +2 -0
- package/lib/tools/constants.js +3 -0
- package/lib/tools/utils.d.ts +3 -0
- package/lib/tools/utils.js +41 -2
- package/lib/types.d.ts +9 -1
- package/lib/utils.d.ts +6 -0
- package/lib/utils.js +17 -2
- package/package.json +2 -2
- package/references/code-style.md +133 -0
- package/references/commands.md +98 -0
- package/references/docs-update.md +34 -0
- package/references/git-workflow.md +135 -0
- package/references/pitfalls.md +62 -0
- 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(
|
|
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;
|
package/lib/tools/auth0/index.js
CHANGED
|
@@ -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 =
|
|
113
|
+
const valid = ajv.validate(schema_1.default, nonNullAssets);
|
|
116
114
|
if (!valid) {
|
|
117
|
-
throw new Error(`Schema validation failed loading ${JSON.stringify(
|
|
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
|
|
214
|
+
// Compare primitive values
|
|
208
215
|
if (localValue !== remoteValue) {
|
|
209
|
-
|
|
210
|
-
|
|
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)) {
|
package/lib/tools/constants.d.ts
CHANGED
|
@@ -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;
|
package/lib/tools/constants.js
CHANGED
|
@@ -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;
|
package/lib/tools/utils.d.ts
CHANGED
|
@@ -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;
|
package/lib/tools/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
...
|
|
309
|
-
|
|
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
|
|
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": "^
|
|
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
|
+
```
|