auth0-deploy-cli 8.30.0 → 8.32.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 (82) hide show
  1. package/.circleci/config.yml +17 -5
  2. package/AGENTS.md +21 -1
  3. package/CHANGELOG.md +48 -415
  4. package/CLAUDE.md +3 -0
  5. package/lib/args.d.ts +5 -0
  6. package/lib/args.js +23 -0
  7. package/lib/commands/export.js +5 -1
  8. package/lib/commands/import.js +40 -3
  9. package/lib/context/directory/handlers/actions.js +1 -2
  10. package/lib/context/directory/handlers/clients.js +14 -0
  11. package/lib/context/directory/handlers/connections.js +1 -0
  12. package/lib/context/directory/handlers/guardianFactors.js +8 -6
  13. package/lib/context/directory/handlers/tenant.d.ts +1 -6
  14. package/lib/context/directory/handlers/tenant.js +0 -1
  15. package/lib/context/directory/handlers/triggers.js +1 -3
  16. package/lib/context/index.js +5 -0
  17. package/lib/context/yaml/handlers/clients.js +12 -0
  18. package/lib/context/yaml/handlers/connections.js +1 -17
  19. package/lib/context/yaml/handlers/guardianFactors.js +3 -1
  20. package/lib/context/yaml/handlers/tenant.js +1 -1
  21. package/lib/context/yaml/index.js +2 -1
  22. package/lib/sessionDurationsToMinutes.d.ts +9 -7
  23. package/lib/sessionDurationsToMinutes.js +11 -10
  24. package/lib/tools/auth0/handlers/actions.d.ts +1 -0
  25. package/lib/tools/auth0/handlers/actions.js +36 -0
  26. package/lib/tools/auth0/handlers/attackProtection.js +15 -0
  27. package/lib/tools/auth0/handlers/branding.js +7 -0
  28. package/lib/tools/auth0/handlers/clientGrants.js +32 -4
  29. package/lib/tools/auth0/handlers/clients.d.ts +52 -2
  30. package/lib/tools/auth0/handlers/clients.js +175 -22
  31. package/lib/tools/auth0/handlers/connections.d.ts +1 -1
  32. package/lib/tools/auth0/handlers/connections.js +42 -6
  33. package/lib/tools/auth0/handlers/customDomains.d.ts +5 -0
  34. package/lib/tools/auth0/handlers/customDomains.js +26 -0
  35. package/lib/tools/auth0/handlers/databases.d.ts +91 -0
  36. package/lib/tools/auth0/handlers/databases.js +162 -2
  37. package/lib/tools/auth0/handlers/default.d.ts +4 -0
  38. package/lib/tools/auth0/handlers/default.js +82 -4
  39. package/lib/tools/auth0/handlers/emailProvider.js +8 -0
  40. package/lib/tools/auth0/handlers/emailTemplates.js +7 -0
  41. package/lib/tools/auth0/handlers/flowVaultConnections.js +6 -1
  42. package/lib/tools/auth0/handlers/flows.js +13 -4
  43. package/lib/tools/auth0/handlers/forms.js +10 -1
  44. package/lib/tools/auth0/handlers/guardianFactors.js +7 -1
  45. package/lib/tools/auth0/handlers/guardianPhoneFactorMessageTypes.js +8 -1
  46. package/lib/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.js +8 -1
  47. package/lib/tools/auth0/handlers/guardianPolicies.js +7 -0
  48. package/lib/tools/auth0/handlers/hooks.d.ts +1 -0
  49. package/lib/tools/auth0/handlers/hooks.js +10 -0
  50. package/lib/tools/auth0/handlers/organizations.d.ts +11 -1
  51. package/lib/tools/auth0/handlers/organizations.js +48 -52
  52. package/lib/tools/auth0/handlers/pages.js +7 -0
  53. package/lib/tools/auth0/handlers/phoneProvider.js +7 -0
  54. package/lib/tools/auth0/handlers/prompts.js +7 -0
  55. package/lib/tools/auth0/handlers/resourceServers.d.ts +1 -2
  56. package/lib/tools/auth0/handlers/resourceServers.js +9 -25
  57. package/lib/tools/auth0/handlers/roles.js +7 -0
  58. package/lib/tools/auth0/handlers/rules.d.ts +1 -0
  59. package/lib/tools/auth0/handlers/rules.js +9 -0
  60. package/lib/tools/auth0/handlers/rulesConfigs.d.ts +1 -0
  61. package/lib/tools/auth0/handlers/rulesConfigs.js +3 -0
  62. package/lib/tools/auth0/handlers/scimHandler.js +8 -1
  63. package/lib/tools/auth0/handlers/selfServiceProfiles.d.ts +1 -1
  64. package/lib/tools/auth0/handlers/selfServiceProfiles.js +10 -17
  65. package/lib/tools/auth0/handlers/tenant.d.ts +13 -1
  66. package/lib/tools/auth0/handlers/tenant.js +48 -4
  67. package/lib/tools/auth0/handlers/themes.js +11 -2
  68. package/lib/tools/auth0/handlers/triggers.js +6 -0
  69. package/lib/tools/auth0/handlers/userAttributeProfiles.js +16 -10
  70. package/lib/tools/auth0/index.d.ts +3 -0
  71. package/lib/tools/auth0/index.js +204 -0
  72. package/lib/tools/calculateChanges.d.ts +2 -2
  73. package/lib/tools/calculateChanges.js +21 -6
  74. package/lib/tools/calculateDryRunChanges.d.ts +71 -0
  75. package/lib/tools/calculateDryRunChanges.js +492 -0
  76. package/lib/tools/deploy.js +25 -1
  77. package/lib/tools/utils.d.ts +5 -0
  78. package/lib/tools/utils.js +64 -3
  79. package/lib/types.d.ts +17 -0
  80. package/lib/utils.d.ts +7 -0
  81. package/lib/utils.js +46 -2
  82. package/package.json +10 -6
@@ -0,0 +1,492 @@
1
+ "use strict";
2
+ /**
3
+ * Dry-run change calculation utilities.
4
+ *
5
+ * This module handles all logic specific to the --dry-run mode:
6
+ * - Comparing local vs. remote assets to detect creates, updates, and deletes
7
+ * - Normalizing asset representations (session durations, client name→ID, profiles)
8
+ * - Tracking and exporting a structured diff log
9
+ *
10
+ * The core deploy-time change calculation lives in calculateChanges.ts.
11
+ */
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.exportDiffLog = exports.getDiffLog = void 0;
17
+ exports.getObjectDifferences = getObjectDifferences;
18
+ exports.hasObjectDifferences = hasObjectDifferences;
19
+ exports.calculateDryRunChanges = calculateDryRunChanges;
20
+ exports.dryRunFormatAssets = dryRunFormatAssets;
21
+ const chalk_1 = __importDefault(require("chalk"));
22
+ const promises_1 = __importDefault(require("node:fs/promises"));
23
+ const logger_1 = __importDefault(require("../logger"));
24
+ const utils_1 = require("./utils");
25
+ const client_1 = require("./auth0/client");
26
+ const sessionDurationsToMinutes_1 = __importDefault(require("../sessionDurationsToMinutes"));
27
+ const clearTenantFlags = (tenant) => {
28
+ if (tenant.flags && !Object.keys(tenant.flags).length) {
29
+ delete tenant.flags;
30
+ }
31
+ };
32
+ /**
33
+ * Converts tenant session duration fields from hours to minutes so that
34
+ * local (hours) and remote (minutes) representations can be compared directly.
35
+ */
36
+ const normalizeTenantForDryRun = (tenant) => {
37
+ const normalizedTenant = { ...tenant };
38
+ clearTenantFlags(normalizedTenant);
39
+ const sessionDurations = (0, sessionDurationsToMinutes_1.default)({
40
+ session_lifetime: normalizedTenant.session_lifetime,
41
+ idle_session_lifetime: normalizedTenant.idle_session_lifetime,
42
+ ephemeral_session_lifetime: normalizedTenant.ephemeral_session_lifetime,
43
+ idle_ephemeral_session_lifetime: normalizedTenant.idle_ephemeral_session_lifetime,
44
+ });
45
+ if (Object.keys(sessionDurations).length > 0) {
46
+ delete normalizedTenant.session_lifetime;
47
+ delete normalizedTenant.idle_session_lifetime;
48
+ delete normalizedTenant.ephemeral_session_lifetime;
49
+ delete normalizedTenant.idle_ephemeral_session_lifetime;
50
+ Object.assign(normalizedTenant, sessionDurations);
51
+ }
52
+ return normalizedTenant;
53
+ };
54
+ const decodeBase64ToCertString = (base64Cert) => {
55
+ try {
56
+ return Buffer.from(base64Cert, 'base64').toString('utf-8');
57
+ }
58
+ catch (e) {
59
+ return base64Cert;
60
+ }
61
+ };
62
+ // In-memory store for diff log entries, keyed by resource type name.
63
+ const logStore = {};
64
+ const diffLog = (resourceTypeName, message) => {
65
+ if (!logStore[resourceTypeName]) {
66
+ logStore[resourceTypeName] = [];
67
+ }
68
+ logStore[resourceTypeName].push(...message);
69
+ };
70
+ const getDiffLog = (resourceTypeName) => resourceTypeName ? logStore[resourceTypeName] || [] : logStore;
71
+ exports.getDiffLog = getDiffLog;
72
+ /**
73
+ * Returns a human-readable label for an asset used as the keyObjPath prefix in diff messages.
74
+ * Named resources (those with a `name` field) use the name directly.
75
+ * Unnamed resources fall back to `${type}-${index + 1}` (1-based)
76
+ */
77
+ function getAssetLabel(asset, type, index) {
78
+ return typeof asset.name === 'string' && asset.name.length > 0
79
+ ? asset.name
80
+ : `${type}-${index + 1}`;
81
+ }
82
+ function shouldIgnoreDryRunField(currentPath, ignoreDryRunFields) {
83
+ return ignoreDryRunFields.some((field) => currentPath === field ||
84
+ currentPath.endsWith(`.${field}`) ||
85
+ currentPath.endsWith(`[${field}]`));
86
+ }
87
+ /**
88
+ * Recursively normalizes an array so that order-insensitive comparisons can be
89
+ * made. Objects within the array have their keys sorted; the array itself is
90
+ * sorted by the JSON stringification of each element.
91
+ */
92
+ function normalizeArrayValues(values) {
93
+ const normalizeValue = (value) => {
94
+ if (Array.isArray(value)) {
95
+ return normalizeArrayValues(value);
96
+ }
97
+ if (typeof value === 'object' && value !== null) {
98
+ return Object.keys(value)
99
+ .sort()
100
+ .reduce((normalized, key) => {
101
+ normalized[key] = normalizeValue(value[key]);
102
+ return normalized;
103
+ }, {});
104
+ }
105
+ return value;
106
+ };
107
+ return values
108
+ .map((value) => normalizeValue(value))
109
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right)));
110
+ }
111
+ /**
112
+ * Writes the accumulated diff log to a JSON file on disk.
113
+ * Useful for CI pipelines that want a machine-readable dry-run report.
114
+ */
115
+ const exportDiffLog = async (fileName, resourceTypeName) => {
116
+ const diffLogData = (0, exports.getDiffLog)(resourceTypeName);
117
+ try {
118
+ await promises_1.default.writeFile(`./${fileName}`, JSON.stringify(diffLogData, null, 2));
119
+ }
120
+ catch (error) {
121
+ logger_1.default.error(`Failed to export diff log: ${String(error)}`);
122
+ }
123
+ };
124
+ exports.exportDiffLog = exportDiffLog;
125
+ /**
126
+ * Compares two objects and returns an array of human-readable difference strings.
127
+ * Only considers keys present in `localObj` — extra keys in `remoteObj` are ignored.
128
+ *
129
+ * @param localObj - Local (desired) state
130
+ * @param remoteObj - Remote (current) state
131
+ * @param keyObjPath - Dot-separated JSON path used for nested tracking
132
+ * @param resourceTypeName - Resource type label used in log output
133
+ * @param ignoreDryRunFields - Field paths to skip during comparison
134
+ * @returns Array of difference descriptions, empty if objects are equivalent
135
+ */
136
+ function getObjectDifferences(localObj, remoteObj, keyObjPath = '', resourceTypeName = '', ignoreDryRunFields = []) {
137
+ const differences = [];
138
+ Object.keys(localObj).forEach((key) => {
139
+ const localValue = localObj[key];
140
+ const remoteValue = remoteObj[key];
141
+ const currentPath = keyObjPath ? `${keyObjPath}.${key}` : key;
142
+ if (shouldIgnoreDryRunField(currentPath, ignoreDryRunFields)) {
143
+ logger_1.default.debug(`[${chalk_1.default.blue(resourceTypeName)}] Ignoring key ${chalk_1.default.yellow(`"${currentPath}"`)} due to ignoreDryRunFields configuration.`);
144
+ return;
145
+ }
146
+ // Key exists locally but not remotely
147
+ if (!(key in remoteObj)) {
148
+ const message = `Key [${currentPath}] found in 'localObj' but not in 'remoteObj'.`;
149
+ logger_1.default.debug(`[${chalk_1.default.blue(resourceTypeName)}] Key ${chalk_1.default.yellow(`"${currentPath}"`)} found in 'localObj' but not in 'remoteObj'.`);
150
+ differences.push(message);
151
+ return;
152
+ }
153
+ // Handle arrays — normalize order before comparing
154
+ if (Array.isArray(localValue) && Array.isArray(remoteValue)) {
155
+ const normalizedLocalValue = normalizeArrayValues(localValue);
156
+ // Strip extra keys from remote objects before sorting — extra remote keys shift sort order,
157
+ // misaligning arrays and causing false "key in local but not in remote" differences.
158
+ const localKeySet = new Set(localValue.flatMap((item) => typeof item === 'object' && item !== null ? Object.keys(item) : []));
159
+ const remoteValueFiltered = localKeySet.size > 0
160
+ ? remoteValue.map((item) => typeof item === 'object' && item !== null
161
+ ? Object.fromEntries(Object.entries(item).filter(([k]) => localKeySet.has(k)))
162
+ : item)
163
+ : remoteValue;
164
+ const normalizedRemoteValue = normalizeArrayValues(remoteValueFiltered);
165
+ if (normalizedLocalValue.length !== normalizedRemoteValue.length) {
166
+ const message = `Array length difference for [${currentPath}]: local:${normalizedLocalValue.length} vs remote:${normalizedRemoteValue.length}`;
167
+ logger_1.default.debug(`[${chalk_1.default.blue(resourceTypeName)}] Array length difference for ${chalk_1.default.yellow(`"${currentPath}"`)}: local:${normalizedLocalValue.length} vs remote:${normalizedRemoteValue.length}`);
168
+ differences.push(message);
169
+ }
170
+ const hasObjects = normalizedLocalValue.some((item) => typeof item === 'object' && item !== null);
171
+ if (hasObjects) {
172
+ // Compare object arrays element-by-element (after normalization)
173
+ normalizedLocalValue.forEach((item, index) => {
174
+ if (typeof item === 'object' &&
175
+ item !== null &&
176
+ typeof normalizedRemoteValue[index] === 'object' &&
177
+ normalizedRemoteValue[index] !== null) {
178
+ const nestedDifferences = getObjectDifferences(item, normalizedRemoteValue[index], `${currentPath}[${index}]`, resourceTypeName, ignoreDryRunFields);
179
+ differences.push(...nestedDifferences);
180
+ }
181
+ else if (item !== normalizedRemoteValue[index]) {
182
+ const message = `Array item difference at [${currentPath}[${index}]]: local:${item} vs remote:${normalizedRemoteValue[index]}`;
183
+ differences.push(message);
184
+ }
185
+ });
186
+ }
187
+ else {
188
+ // Primitive arrays: order-insensitive comparison
189
+ const isDifferent = normalizedLocalValue.some((item, index) => item !== normalizedRemoteValue[index]);
190
+ if (isDifferent) {
191
+ const message = `Array content difference found for key [${currentPath}]`;
192
+ logger_1.default.debug(`[${chalk_1.default.blue(resourceTypeName)}] Array content difference found for key ${chalk_1.default.yellow(`"${currentPath}"`)}`);
193
+ differences.push(message);
194
+ }
195
+ }
196
+ return;
197
+ }
198
+ // Handle nested objects
199
+ if (typeof localValue === 'object' &&
200
+ localValue !== null &&
201
+ typeof remoteValue === 'object' &&
202
+ remoteValue !== null) {
203
+ const nestedDifferences = getObjectDifferences(localValue, remoteValue, currentPath, resourceTypeName, ignoreDryRunFields);
204
+ differences.push(...nestedDifferences);
205
+ return;
206
+ }
207
+ // Compare primitive values — omit array indices from path to reduce log noise
208
+ 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
+ }
214
+ }
215
+ });
216
+ return differences;
217
+ }
218
+ /**
219
+ * Returns true if `localObj` and `remoteObj` differ in any field present in
220
+ * `localObj`. Side-effect: appends found differences to the diff log.
221
+ */
222
+ function hasObjectDifferences(localObj, remoteObj, keyObjPath = '', resourceTypeName = '', ignoreDryRunFields = []) {
223
+ const differences = getObjectDifferences(localObj, remoteObj, keyObjPath, resourceTypeName, ignoreDryRunFields);
224
+ diffLog(resourceTypeName, differences);
225
+ return differences.length > 0;
226
+ }
227
+ /**
228
+ * Calculates the changes required between local and remote asset sets for dry-run operations.
229
+ *
230
+ * Unlike `calculateChanges`, this function does not mutate assets for API consumption.
231
+ * It is purely for reporting: determining which assets would be created, updated, or deleted.
232
+ *
233
+ * @param params.type - Resource type label used for logging
234
+ * @param params.assets - Local assets to be deployed
235
+ * @param params.existing - Remote assets currently on the tenant, or null
236
+ * @param params.identifiers - Fields used to match local↔remote assets (default: ['id', 'name'])
237
+ * @param params.ignoreDryRunFields - Fields to exclude from diff comparisons
238
+ */
239
+ function calculateDryRunChanges({ type, assets, existing, identifiers = ['id', 'name'], ignoreDryRunFields = [], }) {
240
+ const update = [];
241
+ const del = [];
242
+ const create = [];
243
+ const conflicts = [];
244
+ 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));
246
+ // Helper: returns true if a local and remote asset share at least one identifier value
247
+ const assetsMatch = (localAsset, remoteAsset) => identifiers.some((id) => {
248
+ if (Array.isArray(id)) {
249
+ const localValues = id.map((i) => localAsset[i]);
250
+ const remoteValues = id.map((i) => remoteAsset[i]);
251
+ return (localValues.every((v) => v) &&
252
+ remoteValues.every((v) => v) &&
253
+ localValues.join('-') === remoteValues.join('-'));
254
+ }
255
+ return localAsset[id] === remoteAsset[id];
256
+ });
257
+ // Identify created: local assets with no matching remote asset
258
+ const createdAssets = localAssets.filter((localAsset) => !remoteAssets.some((remoteAsset) => assetsMatch(localAsset, remoteAsset)));
259
+ create.push(...createdAssets);
260
+ // Identify updated: local assets that match a remote asset and have differences
261
+ const updatedAssets = localAssets.filter((localAsset, localAssetIndex) => {
262
+ const matchingRemoteAsset = remoteAssets.find((remoteAsset) => {
263
+ // Detect singleton resources (e.g. tenant) that carry none of the identifier fields on either
264
+ // side. For these, fall back to the lenient assetsMatch() used for CREATE/DELETE detection
265
+ // so that value-only changes (e.g. a flag flip) are still surfaced as UPDATE.
266
+ const isSingleton = identifiers.every((id) => {
267
+ if (Array.isArray(id)) {
268
+ return id.every((i) => localAsset[i] === undefined && remoteAsset[i] === undefined);
269
+ }
270
+ return localAsset[id] === undefined && remoteAsset[id] === undefined;
271
+ });
272
+ if (isSingleton) {
273
+ return assetsMatch(localAsset, remoteAsset);
274
+ }
275
+ // Stricter match check: identifier values must be non-null on both sides
276
+ return identifiers.some((id) => {
277
+ if (Array.isArray(id)) {
278
+ const localValues = id.map((i) => localAsset[i]);
279
+ const remoteValues = id.map((i) => remoteAsset[i]);
280
+ return (localValues.every((v) => v !== undefined && v !== null) &&
281
+ remoteValues.every((v) => v !== undefined && v !== null) &&
282
+ localValues.join('-') === remoteValues.join('-'));
283
+ }
284
+ return (localAsset[id] !== undefined &&
285
+ remoteAsset[id] !== undefined &&
286
+ localAsset[id] === remoteAsset[id]);
287
+ });
288
+ });
289
+ if (matchingRemoteAsset) {
290
+ // Backfill any identifier fields missing from the local asset (e.g. remote-assigned IDs)
291
+ identifiers.forEach((id) => {
292
+ if (Array.isArray(id)) {
293
+ id.forEach((idPart) => {
294
+ if (!localAsset[idPart] && matchingRemoteAsset[idPart]) {
295
+ localAsset[idPart] = matchingRemoteAsset[idPart];
296
+ }
297
+ });
298
+ }
299
+ else if (!localAsset[id] && matchingRemoteAsset[id]) {
300
+ localAsset[id] = matchingRemoteAsset[id];
301
+ }
302
+ });
303
+ return hasObjectDifferences(localAsset, matchingRemoteAsset, getAssetLabel(localAsset, type, localAssetIndex), type, ignoreDryRunFields);
304
+ }
305
+ return false;
306
+ });
307
+ update.push(...updatedAssets);
308
+ // Identify deleted: remote assets with no matching local asset
309
+ const deletedAssets = remoteAssets
310
+ .filter((remoteAsset) => !localAssets.some((localAsset) => assetsMatch(localAsset, remoteAsset)))
311
+ .map((remoteAsset) => {
312
+ // Ensure all identifier fields are present on the returned asset for tracking
313
+ const assetWithIdentifiers = { ...remoteAsset };
314
+ identifiers.forEach((id) => {
315
+ if (Array.isArray(id)) {
316
+ id.forEach((idPart) => {
317
+ if (remoteAsset[idPart]) {
318
+ assetWithIdentifiers[idPart] = remoteAsset[idPart];
319
+ }
320
+ });
321
+ }
322
+ else if (remoteAsset[id]) {
323
+ assetWithIdentifiers[id] = remoteAsset[id];
324
+ }
325
+ });
326
+ return assetWithIdentifiers;
327
+ });
328
+ del.push(...deletedAssets);
329
+ const hasChanges = del.length > 0 || create.length > 0 || conflicts.length > 0 || update.length > 0;
330
+ if (hasChanges) {
331
+ logger_1.default.debug(`[DryRun] calculated changes for [${type}]:${JSON.stringify({
332
+ create: create.length,
333
+ update: update.length,
334
+ del: del.length,
335
+ conflicts: conflicts.length,
336
+ })}`);
337
+ }
338
+ return { del, create, conflicts, update };
339
+ }
340
+ /**
341
+ * Normalizes local assets so they can be accurately compared against remote assets.
342
+ *
343
+ * Transformations applied:
344
+ * - Converts client name references to client IDs (clientGrants, databases, connections, resourceServers)
345
+ * - Resolves connection profile and user attribute profile names to IDs (express_configuration)
346
+ * - Decodes base64-encoded SAML certificates to PEM strings
347
+ * - Resolves organization connection names to connection IDs
348
+ * - Normalizes tenant session duration fields
349
+ * - Removes empty branding templates (avoids false diff against remote)
350
+ *
351
+ * @param localAssets - Local assets object (will be mutated)
352
+ * @param authAPIclient - Auth0 management API client for fetching remote reference data
353
+ * @returns The mutated assets object with normalized values
354
+ */
355
+ async function dryRunFormatAssets(localAssets, authAPIclient) {
356
+ const clientsRemoteData = await (0, client_1.paginate)(authAPIclient.clients.list, {
357
+ paginate: true,
358
+ include_totals: true,
359
+ });
360
+ if (localAssets.clientGrants) {
361
+ const { clientGrants } = localAssets;
362
+ localAssets.clientGrants = clientGrants.map((clientGrant) => {
363
+ if (clientGrant.client_id) {
364
+ const originalName = clientGrant.client_id;
365
+ const resolvedId = (0, utils_1.convertClientNameToId)(originalName, clientsRemoteData);
366
+ if (resolvedId !== originalName) {
367
+ clientGrant._clientName = originalName;
368
+ }
369
+ clientGrant.client_id = resolvedId;
370
+ }
371
+ return clientGrant;
372
+ });
373
+ }
374
+ if (localAssets.resourceServers) {
375
+ localAssets.resourceServers = localAssets.resourceServers.map((resourceServer) => {
376
+ if (resourceServer.client_id) {
377
+ resourceServer.client_id = (0, utils_1.convertClientNameToId)(resourceServer.client_id, clientsRemoteData);
378
+ }
379
+ return resourceServer;
380
+ });
381
+ }
382
+ if (localAssets.databases && localAssets.clients) {
383
+ const { databases } = localAssets;
384
+ localAssets.databases = databases.map((db) => {
385
+ if (db.enabled_clients && db.enabled_clients.length > 0) {
386
+ db.enabled_clients = db.enabled_clients.map((enabledClientName) => (0, utils_1.convertClientNameToId)(enabledClientName, clientsRemoteData));
387
+ }
388
+ return db;
389
+ });
390
+ }
391
+ if (localAssets.clients?.some((client) => client.express_configuration)) {
392
+ const [connectionProfiles, userAttributeProfiles] = await Promise.all([
393
+ (0, client_1.paginate)(authAPIclient.connectionProfiles.list, {
394
+ checkpoint: true,
395
+ take: 10,
396
+ }),
397
+ (0, client_1.paginate)(authAPIclient.userAttributeProfiles.list, {
398
+ checkpoint: true,
399
+ include_totals: true,
400
+ is_global: false,
401
+ take: 10,
402
+ }),
403
+ ]);
404
+ localAssets.clients = localAssets.clients.map((client) => {
405
+ if (!client.express_configuration)
406
+ return client;
407
+ const userAttributeProfileName = client.express_configuration.user_attribute_profile_id;
408
+ if (userAttributeProfileName) {
409
+ const userAttributeProfile = userAttributeProfiles.find((profile) => profile.name === userAttributeProfileName);
410
+ if (userAttributeProfile?.id) {
411
+ client.express_configuration.user_attribute_profile_id = userAttributeProfile.id;
412
+ }
413
+ }
414
+ const connectionProfileName = client.express_configuration.connection_profile_id;
415
+ if (connectionProfileName) {
416
+ const connectionProfile = connectionProfiles.find((profile) => profile.name === connectionProfileName);
417
+ if (connectionProfile?.id) {
418
+ client.express_configuration.connection_profile_id = connectionProfile.id;
419
+ }
420
+ }
421
+ const oktaOinClientName = client.express_configuration.okta_oin_client_id;
422
+ if (oktaOinClientName) {
423
+ const oktaOinClient = clientsRemoteData.find((remoteClient) => remoteClient.name === oktaOinClientName);
424
+ if (oktaOinClient?.client_id) {
425
+ client.express_configuration.okta_oin_client_id = oktaOinClient.client_id;
426
+ }
427
+ }
428
+ return client;
429
+ });
430
+ }
431
+ if (localAssets.actions) {
432
+ const { actions } = localAssets;
433
+ localAssets.actions = actions.map((action) => {
434
+ if ('deployed' in action) {
435
+ action.all_changes_deployed = action.deployed;
436
+ delete action.deployed;
437
+ }
438
+ return action;
439
+ });
440
+ }
441
+ if (localAssets.connections) {
442
+ const { connections } = localAssets;
443
+ localAssets.connections = connections.map((connection) => {
444
+ if (connection.strategy === 'samlp' && connection.options) {
445
+ if ('cert' in connection.options) {
446
+ connection.options.cert = decodeBase64ToCertString(connection.options.cert);
447
+ }
448
+ }
449
+ if (connection.options?.idpinitiated?.client_id) {
450
+ connection.options.idpinitiated.client_id = (0, utils_1.convertClientNameToId)(connection.options.idpinitiated.client_id, clientsRemoteData);
451
+ }
452
+ if (connection.enabled_clients) {
453
+ connection.enabled_clients = connection.enabled_clients.map((clientName) => (0, utils_1.convertClientNameToId)(clientName, clientsRemoteData));
454
+ }
455
+ return connection;
456
+ });
457
+ }
458
+ if (localAssets.organizations?.some((organization) => organization.connections?.length)) {
459
+ const existingConnections = await (0, client_1.paginate)(authAPIclient.connections.list, {
460
+ checkpoint: true,
461
+ });
462
+ localAssets.organizations = localAssets.organizations.map((organization) => {
463
+ if (!organization.connections?.length) {
464
+ return organization;
465
+ }
466
+ organization.connections = organization.connections
467
+ .map((connection) => {
468
+ const formattedConnection = { ...connection };
469
+ const connectionName = formattedConnection.name;
470
+ if (connectionName) {
471
+ formattedConnection.connection_id = existingConnections.find((existingConnection) => existingConnection.name === connectionName)?.id;
472
+ delete formattedConnection.name;
473
+ }
474
+ return formattedConnection;
475
+ })
476
+ .filter((connection) => !!connection.connection_id);
477
+ return organization;
478
+ });
479
+ }
480
+ if (localAssets.tenant) {
481
+ localAssets.tenant = normalizeTenantForDryRun(localAssets.tenant);
482
+ }
483
+ if (localAssets.branding) {
484
+ const { branding } = localAssets;
485
+ // Empty templates array signals "no templates" — skip remote comparison to avoid false diffs
486
+ if (branding.templates && branding.templates.length === 0) {
487
+ delete branding.templates;
488
+ }
489
+ localAssets.branding = branding;
490
+ }
491
+ return localAssets;
492
+ }
@@ -6,13 +6,37 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.default = deploy;
7
7
  const auth0_1 = __importDefault(require("./auth0"));
8
8
  const logger_1 = __importDefault(require("../logger"));
9
+ const utils_1 = require("../utils");
10
+ function zeroChangeSummary(auth0) {
11
+ return auth0.handlers.reduce((accum, h) => {
12
+ accum[h.type] = { deleted: 0, created: 0, updated: 0 };
13
+ return accum;
14
+ }, {});
15
+ }
9
16
  async function deploy(assets, client, config) {
10
17
  // Setup log level
11
- logger_1.default.level = process.env.AUTH0_DEBUG === 'true' ? 'debug' : 'info';
18
+ const isDebug = process.env.AUTH0_DEBUG === 'true';
19
+ logger_1.default.level = isDebug ? 'debug' : 'info';
20
+ const dryRunMode = config('AUTH0_DRY_RUN');
21
+ // Normalize boolean true (EA compat) → 'preview'
22
+ const effectiveMode = dryRunMode === true || dryRunMode === 'true' ? 'preview' : dryRunMode;
23
+ if (dryRunMode && effectiveMode !== 'preview') {
24
+ throw new Error(`Invalid AUTH0_DRY_RUN value: ${dryRunMode}. Use true or 'preview'.`);
25
+ }
26
+ const isInteractive = !!config('AUTH0_DRY_RUN_INTERACTIVE');
27
+ const shouldApplyAfterPreview = (0, utils_1.isTruthy)(config('AUTH0_DRY_RUN_APPLY'));
12
28
  logger_1.default.info(`Getting access token for ${config('AUTH0_CLIENT_ID') !== undefined ? `${config('AUTH0_CLIENT_ID')}/` : ''}${config('AUTH0_DOMAIN')}`);
13
29
  const auth0 = new auth0_1.default(client, assets, config);
14
30
  // Validate Assets
15
31
  await auth0.validate();
32
+ if (effectiveMode === 'preview') {
33
+ const hasChanges = await auth0.dryRun({
34
+ interactive: isInteractive && !shouldApplyAfterPreview,
35
+ });
36
+ if (!shouldApplyAfterPreview || !hasChanges) {
37
+ return zeroChangeSummary(auth0);
38
+ }
39
+ }
16
40
  // Process changes
17
41
  await auth0.processChanges();
18
42
  return auth0.handlers.reduce((accum, h) => {
@@ -1,4 +1,5 @@
1
1
  import { Asset, Assets, CalculatedChanges, KeywordMappings } from '../types';
2
+ import { ConfigFunction } from '../configFactory';
2
3
  export declare const keywordReplaceArrayRegExp: (key: any) => RegExp;
3
4
  export declare const keywordReplaceStringRegExp: (key: any) => RegExp;
4
5
  export declare function keywordArrayReplace(input: string, mappings: KeywordMappings): string;
@@ -24,6 +25,7 @@ export declare function filterExcluded(changes: CalculatedChanges, exclude: stri
24
25
  export declare function filterIncluded(changes: CalculatedChanges, include: string[]): CalculatedChanges;
25
26
  export declare function areArraysEquals(x: any[], y: any[]): boolean;
26
27
  export declare const obfuscateSensitiveValues: (data: Asset | Asset[] | null, sensitiveFieldsToObfuscate: string[]) => Asset | Asset[] | null;
28
+ export declare const validateNoUnresolvedPlaceholders: (data: Asset | null, resourceType: string, resourceName: string) => Asset | null;
27
29
  export declare const stripObfuscatedFieldsFromPayload: (data: Asset | Asset[] | null, obfuscatedFields: string[]) => Asset | Asset[] | null;
28
30
  export declare const detectInsufficientScopeError: <T>(fn: Function) => Promise<{
29
31
  hadSufficientScopes: true;
@@ -55,3 +57,6 @@ export declare function maskSecretAtPath({ resourceTypeName, maskedKeyName, mask
55
57
  * @returns True if third-party clients should be excluded, false otherwise.
56
58
  */
57
59
  export declare const shouldExcludeThirdPartyClients: (configFn: (key: string) => any) => boolean;
60
+ export declare function sortGuardianFactors(factors: Asset[]): Asset[];
61
+ export declare function isDryRun(config: ConfigFunction): boolean;
62
+ export declare function printCLIMessage(message: string): void;
@@ -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.obfuscateSensitiveValues = exports.keywordReplaceStringRegExp = exports.keywordReplaceArrayRegExp = void 0;
39
+ exports.shouldExcludeThirdPartyClients = exports.isForbiddenFeatureError = exports.isDeprecatedError = exports.detectInsufficientScopeError = 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;
@@ -56,6 +56,9 @@ exports.filterIncluded = filterIncluded;
56
56
  exports.areArraysEquals = areArraysEquals;
57
57
  exports.sleep = sleep;
58
58
  exports.maskSecretAtPath = maskSecretAtPath;
59
+ exports.sortGuardianFactors = sortGuardianFactors;
60
+ exports.isDryRun = isDryRun;
61
+ exports.printCLIMessage = printCLIMessage;
59
62
  const path_1 = __importDefault(require("path"));
60
63
  const fs_1 = __importStar(require("fs"));
61
64
  const dot_prop_1 = __importDefault(require("dot-prop"));
@@ -182,15 +185,16 @@ function getEnabledClients(assets, connection, existing, clients) {
182
185
  return undefined; // If no enabled clients passed in, explicitly ignore from management, preventing unintentional disabling of connection.
183
186
  const excludedClientsByNames = (assets.exclude && assets.exclude.clients) || [];
184
187
  const excludedClients = convertClientNamesToIds(excludedClientsByNames, clients);
188
+ const allExcluded = [...excludedClientsByNames, ...excludedClients];
185
189
  const enabledClients = [
186
- ...convertClientNamesToIds(connection.enabled_clients || [], clients).filter((item) => ![...excludedClientsByNames, ...excludedClients].includes(item)),
190
+ ...convertClientNamesToIds(connection.enabled_clients || [], clients).filter((item) => !allExcluded.includes(item)),
187
191
  ];
188
192
  // If client is excluded and in the existing connection this client is enabled, it should keep enabled
189
193
  // If client is excluded and in the existing connection this client is disabled, it should keep disabled
190
194
  existing.forEach((conn) => {
191
195
  if (conn.name === connection.name) {
192
196
  excludedClients.forEach((excludedClient) => {
193
- if (conn.enabled_clients.includes(excludedClient)) {
197
+ if (conn.enabled_clients?.includes(excludedClient)) {
194
198
  enabledClients.push(excludedClient);
195
199
  }
196
200
  });
@@ -255,6 +259,38 @@ const obfuscateSensitiveValues = (data, sensitiveFieldsToObfuscate) => {
255
259
  return newAsset;
256
260
  };
257
261
  exports.obfuscateSensitiveValues = obfuscateSensitiveValues;
262
+ const UNRESOLVED_PLACEHOLDER_REGEX = /^(##.+##|@@.+@@)$/;
263
+ // Recursively collects all fields in an asset that still contain an unresolved
264
+ // ##...## (string) or @@...@@ (array) keyword placeholder.
265
+ const collectUnresolvedPlaceholders = (data, parentPath = '') => {
266
+ if (data === null || typeof data !== 'object')
267
+ return [];
268
+ const found = [];
269
+ Object.keys(data).forEach((key) => {
270
+ const fullPath = parentPath ? `${parentPath}.${key}` : key;
271
+ const value = data[key];
272
+ if (typeof value === 'string' && UNRESOLVED_PLACEHOLDER_REGEX.test(value)) {
273
+ found.push({ path: fullPath, value });
274
+ }
275
+ else if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
276
+ found.push(...collectUnresolvedPlaceholders(value, fullPath));
277
+ }
278
+ });
279
+ return found;
280
+ };
281
+ // Throws an error if any field in the asset contains an unresolved ##...## or @@...@@ placeholder,
282
+ // listing all offending fields so the user can fix them before deploying.
283
+ const validateNoUnresolvedPlaceholders = (data, resourceType, resourceName) => {
284
+ if (data === null)
285
+ return data;
286
+ const unresolved = collectUnresolvedPlaceholders(data);
287
+ if (unresolved.length > 0) {
288
+ const fields = unresolved.map(({ path, value }) => ` - "${path}": ${value}`).join('\n');
289
+ throw new Error(`Unresolved placeholder(s) found in ${resourceType} "${resourceName}":\n${fields}\nPlease ensure all keyword mappings are defined before deploying.`);
290
+ }
291
+ return data;
292
+ };
293
+ exports.validateNoUnresolvedPlaceholders = validateNoUnresolvedPlaceholders;
258
294
  // The reverse of `obfuscateSensitiveValues()`, preventing an obfuscated value from being passed to the API
259
295
  const stripObfuscatedFieldsFromPayload = (data, obfuscatedFields) => {
260
296
  if (data === null)
@@ -334,3 +370,28 @@ const shouldExcludeThirdPartyClients = (configFn) => {
334
370
  return value === 'true' || value === true;
335
371
  };
336
372
  exports.shouldExcludeThirdPartyClients = shouldExcludeThirdPartyClients;
373
+ // Sort guardian factors by name
374
+ function sortGuardianFactors(factors) {
375
+ // if no factors, return empty array
376
+ if (!factors || factors.length === 0)
377
+ return [];
378
+ return factors.sort((a, b) => {
379
+ const nameA = a.name || '';
380
+ const nameB = b.name || '';
381
+ if (nameA < nameB)
382
+ return -1;
383
+ if (nameA > nameB)
384
+ return 1;
385
+ return 0;
386
+ });
387
+ }
388
+ // Check if dry-run flag is enabled
389
+ function isDryRun(config) {
390
+ if (typeof config !== 'function')
391
+ return false;
392
+ return config('AUTH0_DRY_RUN') === true || config('AUTH0_DRY_RUN') === 'true';
393
+ }
394
+ // Print a message to the CLI message console
395
+ function printCLIMessage(message) {
396
+ process.stdout.write(message + '\n');
397
+ }