auth0-deploy-cli 8.42.0 → 8.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +51 -1
  2. package/lib/context/defaults.d.ts +7 -0
  3. package/lib/context/defaults.js +74 -30
  4. package/lib/context/directory/handlers/actionModules.js +11 -12
  5. package/lib/context/directory/handlers/actions.js +11 -13
  6. package/lib/context/directory/handlers/attackProtection.js +1 -1
  7. package/lib/context/directory/handlers/branding.js +9 -1
  8. package/lib/context/directory/handlers/clients.js +9 -3
  9. package/lib/context/directory/handlers/connections.js +10 -4
  10. package/lib/context/directory/handlers/databases.js +4 -4
  11. package/lib/context/directory/handlers/emailTemplates.js +9 -0
  12. package/lib/context/directory/handlers/hooks.js +12 -1
  13. package/lib/context/directory/handlers/prompts.js +11 -3
  14. package/lib/context/directory/handlers/rules.js +12 -1
  15. package/lib/context/directory/index.js +7 -8
  16. package/lib/context/yaml/handlers/attackProtection.js +1 -1
  17. package/lib/context/yaml/handlers/branding.js +8 -1
  18. package/lib/context/yaml/handlers/clients.js +4 -1
  19. package/lib/context/yaml/handlers/connections.js +3 -1
  20. package/lib/context/yaml/handlers/flows.js +7 -0
  21. package/lib/context/yaml/handlers/forms.js +7 -0
  22. package/lib/context/yaml/handlers/prompts.js +7 -1
  23. package/lib/context/yaml/index.js +7 -8
  24. package/lib/readonly.js +5 -0
  25. package/lib/tools/auth0/handlers/attackProtection.js +3 -1
  26. package/lib/tools/auth0/handlers/clients.d.ts +21 -0
  27. package/lib/tools/auth0/handlers/clients.js +21 -0
  28. package/lib/tools/auth0/handlers/connectionProfiles.d.ts +27 -0
  29. package/lib/tools/auth0/handlers/connectionProfiles.js +30 -0
  30. package/lib/tools/auth0/handlers/connections.d.ts +11 -6
  31. package/lib/tools/auth0/handlers/connections.js +33 -7
  32. package/lib/tools/auth0/handlers/databases.js +12 -3
  33. package/lib/tools/auth0/handlers/default.d.ts +23 -0
  34. package/lib/tools/auth0/handlers/default.js +21 -10
  35. package/lib/tools/auth0/handlers/organizations.d.ts +29 -1
  36. package/lib/tools/auth0/handlers/organizations.js +165 -17
  37. package/lib/tools/auth0/handlers/phoneProvider.js +10 -2
  38. package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
  39. package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
  40. package/lib/tools/auth0/handlers/roles.d.ts +4 -0
  41. package/lib/tools/auth0/handlers/roles.js +10 -0
  42. package/lib/tools/calculateDryRunChanges.js +12 -8
  43. package/lib/types.d.ts +1 -0
  44. package/lib/utils.js +5 -2
  45. package/package.json +2 -2
@@ -131,6 +131,19 @@ exports.schema = {
131
131
  type: 'string',
132
132
  enum: Object.values(auth0_1.Management.OrganizationThirdPartyClientAccessEnum),
133
133
  },
134
+ is_app_entitlement_active: { type: 'boolean' },
135
+ clients: {
136
+ type: 'array',
137
+ items: {
138
+ type: 'object',
139
+ properties: {
140
+ client_id: { type: 'string' },
141
+ use_for_member_access: { type: 'boolean' },
142
+ },
143
+ required: ['client_id', 'use_for_member_access'],
144
+ },
145
+ default: [],
146
+ },
134
147
  },
135
148
  required: ['name'],
136
149
  },
@@ -172,6 +185,7 @@ class OrganizationsHandler extends default_1.default {
172
185
  const organization = { ...org };
173
186
  delete organization.connections;
174
187
  delete organization.client_grants;
188
+ delete organization.clients;
175
189
  if ('discovery_domains' in organization) {
176
190
  delete organization.discovery_domains;
177
191
  }
@@ -180,8 +194,9 @@ class OrganizationsHandler extends default_1.default {
180
194
  throw new Error(`Organization "${organization.name}" was created but the response did not include an ID. Skipping connection/grant association.`);
181
195
  }
182
196
  const createdId = created.id;
197
+ const retryConfig = this.getRetryConfig();
183
198
  if (typeof org.connections !== 'undefined' && org.connections.length > 0) {
184
- await Promise.all(org.connections.map((conn) => this.client.organizations.connections.create(createdId, conn)));
199
+ await Promise.all(org.connections.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.create(createdId, conn), retryConfig)));
185
200
  }
186
201
  if (typeof org.client_grants !== 'undefined' && org.client_grants.length > 0) {
187
202
  await Promise.all(org.client_grants.map((organizationClientGrants) => this.createOrganizationClientGrants(createdId, this.getClientGrantIDByClientName(organizationClientGrants.client_id))));
@@ -195,6 +210,19 @@ class OrganizationsHandler extends default_1.default {
195
210
  throw new Error(`Problem creating discovery domain ${discoveryDomain?.domain} for organization ${createdId}\n${err}`);
196
211
  })));
197
212
  }
213
+ if (typeof org.clients !== 'undefined' && org.clients.length > 0) {
214
+ const clientsToCreate = org.clients
215
+ .map((oc) => ({
216
+ client_id: this.getClientIdByClientName(oc.client_id),
217
+ use_for_member_access: oc.use_for_member_access,
218
+ }))
219
+ .filter((oc) => !!oc.client_id);
220
+ if (clientsToCreate.length > 0) {
221
+ await this.createOrganizationClients(createdId, clientsToCreate).catch((err) => {
222
+ throw new Error(`Problem creating org clients for organization ${createdId}\n${err}`);
223
+ });
224
+ }
225
+ }
198
226
  return created;
199
227
  }
200
228
  async createOrganizations(creates) {
@@ -213,15 +241,24 @@ class OrganizationsHandler extends default_1.default {
213
241
  .promise();
214
242
  }
215
243
  async updateOrganization(org, organizations) {
216
- const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, } = await organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
244
+ const existingOrg = organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
245
+ const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, clients: existingOrgClients = [], } = existingOrg;
217
246
  const params = { id: org.id };
218
- const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, } = org;
247
+ const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, clients: organizationClients, } = org;
219
248
  delete org.connections;
220
249
  delete org.name;
221
250
  delete org.id;
222
251
  delete org.client_grants;
223
252
  delete org.discovery_domains;
224
- await this.client.organizations.update(params.id, org);
253
+ delete org.clients;
254
+ // Only PATCH if top-level properties actually differ from the existing state.
255
+ // Compare only the keys present in the desired config against those same keys
256
+ // on the remote org, so that fields the user didn't specify are not considered.
257
+ let changed = false;
258
+ if (!(0, lodash_1.isEqual)(org, (0, lodash_1.pick)(existingOrg, Object.keys(org)))) {
259
+ await this.client.organizations.update(params.id, org);
260
+ changed = true;
261
+ }
225
262
  // organization connections
226
263
  const connectionsToRemove = existingConnections.filter((c) => !connections.find((x) => x.connection_id === c.connection_id));
227
264
  const connectionsToAdd = connections.filter((c) => !existingConnections.find((x) => x.connection_id === c.connection_id));
@@ -232,27 +269,27 @@ class OrganizationsHandler extends default_1.default {
232
269
  x.organization_access_level !== c.organization_access_level ||
233
270
  x.organization_connection_name !== c.organization_connection_name ||
234
271
  x.is_enabled !== (c.is_enabled ?? true))));
272
+ if (connectionsToUpdate.length > 0 ||
273
+ connectionsToAdd.length > 0 ||
274
+ connectionsToRemove.length > 0) {
275
+ changed = true;
276
+ }
277
+ const retryConfig = this.getRetryConfig();
235
278
  // Handle updates first
236
- await Promise.all(connectionsToUpdate.map((conn) => this.client.organizations.connections
237
- .update(params.id, conn.connection_id, {
279
+ await Promise.all(connectionsToUpdate.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.update(params.id, conn.connection_id, {
238
280
  organization_connection_name: conn.organization_connection_name,
239
281
  assign_membership_on_login: conn.assign_membership_on_login,
240
282
  show_as_button: conn.show_as_button,
241
283
  is_signup_enabled: conn.is_signup_enabled,
242
284
  is_enabled: conn.is_enabled,
243
285
  organization_access_level: conn.organization_access_level,
244
- })
245
- .catch(() => {
286
+ }), retryConfig).catch(() => {
246
287
  throw new Error(`Problem updating Enabled Connection ${conn.connection_id} for organizations ${params.id}`);
247
288
  })));
248
- await Promise.all(connectionsToAdd.map((conn) => this.client.organizations.connections
249
- .create(params.id, (0, lodash_1.omit)(conn, 'connection'))
250
- .catch(() => {
289
+ await Promise.all(connectionsToAdd.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.create(params.id, (0, lodash_1.omit)(conn, 'connection')), retryConfig).catch(() => {
251
290
  throw new Error(`Problem adding Enabled Connection ${conn.connection_id} for organizations ${params.id}`);
252
291
  })));
253
- await Promise.all(connectionsToRemove.map((conn) => this.client.organizations.connections
254
- .delete(params.id, conn.connection_id)
255
- .catch(() => {
292
+ await Promise.all(connectionsToRemove.map((conn) => (0, default_1.retryWithExponentialBackoff)(() => this.client.organizations.connections.delete(params.id, conn.connection_id), retryConfig).catch(() => {
256
293
  throw new Error(`Problem removing Enabled Connection ${conn.connection_id} for organizations ${params.id}`);
257
294
  })));
258
295
  // organization client_grants
@@ -266,6 +303,9 @@ class OrganizationsHandler extends default_1.default {
266
303
  ?.map((clientGrant) => ({
267
304
  grant_id: this.getClientGrantIDByClientName(clientGrant.client_id),
268
305
  })) || [];
306
+ if (orgClientGrantsToAdd.length > 0 || orgClientGrantsToRemove.length > 0) {
307
+ changed = true;
308
+ }
269
309
  // Handle updates first
270
310
  await Promise.all(orgClientGrantsToAdd.map((orgClientGrant) => this.createOrganizationClientGrants(params.id, orgClientGrant.grant_id).catch(() => {
271
311
  throw new Error(`Problem adding organization clientGrant ${orgClientGrant.grant_id} for organizations ${params.id}`);
@@ -287,6 +327,9 @@ class OrganizationsHandler extends default_1.default {
287
327
  };
288
328
  })
289
329
  .filter(Boolean) || [];
330
+ if (orgDiscoveryDomainsToUpdate.length > 0 || orgDiscoveryDomainsToAdd.length > 0) {
331
+ changed = true;
332
+ }
290
333
  for (const { id, domain, ...updateParams } of orgDiscoveryDomainsToUpdate) {
291
334
  try {
292
335
  await this.updateOrganizationDiscoveryDomain(params.id, id, domain, updateParams);
@@ -310,6 +353,7 @@ class OrganizationsHandler extends default_1.default {
310
353
  if (orgDiscoveryDomainsToRemove.length > 0) {
311
354
  if (this.config('AUTH0_ALLOW_DELETE') === 'true' ||
312
355
  this.config('AUTH0_ALLOW_DELETE') === true) {
356
+ changed = true;
313
357
  for (const domain of orgDiscoveryDomainsToRemove) {
314
358
  try {
315
359
  await this.deleteOrganizationDiscoveryDomain(params.id, domain.domain, domain.id);
@@ -324,12 +368,66 @@ class OrganizationsHandler extends default_1.default {
324
368
  \n${orgDiscoveryDomainsToRemove.map((i) => this.objString(i)).join('\n')}`);
325
369
  }
326
370
  }
327
- return params;
371
+ // organization clients
372
+ const orgClientsToAdd = (organizationClients || [])
373
+ .filter((c) => !existingOrgClients.find((x) => x.client_id === c.client_id))
374
+ .map((oc) => ({
375
+ client_id: this.getClientIdByClientName(oc.client_id),
376
+ use_for_member_access: oc.use_for_member_access,
377
+ }))
378
+ .filter((oc) => !!oc.client_id);
379
+ const orgClientsToRemove = (existingOrgClients || [])
380
+ .filter((c) => !(organizationClients || []).find((x) => x.client_id === c.client_id))
381
+ .map((oc) => this.getClientIdByClientName(oc.client_id))
382
+ .filter(Boolean);
383
+ const orgClientsToUpdate = (organizationClients || [])
384
+ .filter((c) => {
385
+ const existing = existingOrgClients.find((x) => x.client_id === c.client_id);
386
+ return existing && existing.use_for_member_access !== c.use_for_member_access;
387
+ })
388
+ .map((oc) => ({
389
+ client_id: this.getClientIdByClientName(oc.client_id),
390
+ use_for_member_access: oc.use_for_member_access,
391
+ }))
392
+ .filter((oc) => !!oc.client_id);
393
+ if (orgClientsToAdd.length > 0) {
394
+ changed = true;
395
+ await this.createOrganizationClients(params.id, orgClientsToAdd).catch((err) => {
396
+ throw new Error(`Problem adding org clients for organization ${params.id}\n${err}`);
397
+ });
398
+ }
399
+ if (orgClientsToRemove.length > 0) {
400
+ if (this.config('AUTH0_ALLOW_DELETE') === 'true' ||
401
+ this.config('AUTH0_ALLOW_DELETE') === true) {
402
+ changed = true;
403
+ await this.deleteOrganizationClients(params.id, orgClientsToRemove).catch((err) => {
404
+ throw new Error(`Problem removing org clients for organization ${params.id}\n${err}`);
405
+ });
406
+ }
407
+ else {
408
+ logger_1.default.warn(`Detected the following organization client associations should be removed. Doing so may be destructive.\nYou can enable deletes by setting 'AUTH0_ALLOW_DELETE' to true in the config\n${orgClientsToRemove.join('\n')}`);
409
+ }
410
+ }
411
+ if (orgClientsToUpdate.length > 0) {
412
+ changed = true;
413
+ }
414
+ await Promise.all(orgClientsToUpdate.map((oc) => this.client.organizations.clients
415
+ .update(params.id, oc.client_id, {
416
+ use_for_member_access: oc.use_for_member_access,
417
+ })
418
+ .catch((err) => {
419
+ throw new Error(`Problem updating org client ${oc.client_id} for organization ${params.id}\n${err}`);
420
+ })));
421
+ return changed ? params : null;
328
422
  }
329
423
  getClientGrantIDByClientName(clientsName) {
330
424
  const found = this.formattedClientGrants.find((c) => c.client_id === clientsName);
331
425
  return found?.grant_id || '';
332
426
  }
427
+ getClientIdByClientName(clientName) {
428
+ const found = this.allClients?.find((c) => c.name === clientName);
429
+ return found?.client_id || '';
430
+ }
333
431
  async getFormattedClientGrants() {
334
432
  const [clients, clientGrants] = await Promise.all([
335
433
  (0, client_1.paginate)(this.client.clients.list, {
@@ -339,6 +437,8 @@ class OrganizationsHandler extends default_1.default {
339
437
  paginate: true,
340
438
  }),
341
439
  ]);
440
+ // Store clients for org-client name→ID resolution
441
+ this.allClients = clients;
342
442
  // Convert clients by name to the id and store it in the formattedClientGrants
343
443
  const formattedClientGrantsMapping = clientGrants?.map((clientGrant) => {
344
444
  const { id, client_id: clientName } = clientGrant;
@@ -356,8 +456,10 @@ class OrganizationsHandler extends default_1.default {
356
456
  data: updates || [],
357
457
  generator: (item) => this.updateOrganization(item, orgs)
358
458
  .then((data) => {
359
- this.didUpdate(data);
360
- this.updated += 1;
459
+ if (data) {
460
+ this.didUpdate(data);
461
+ this.updated += 1;
462
+ }
361
463
  })
362
464
  .catch((err) => {
363
465
  throw new Error(`Problem updating ${this.type} ${this.objString(item)}\n${err}`);
@@ -394,6 +496,14 @@ class OrganizationsHandler extends default_1.default {
394
496
  if (organizationDiscoveryDomains) {
395
497
  org.discovery_domains = organizationDiscoveryDomains;
396
498
  }
499
+ // Get org-client associations
500
+ const orgClients = await this.getAllOrganizationClients(org.id);
501
+ if (orgClients) {
502
+ org.clients = orgClients.map((oc) => ({
503
+ client_id: (0, utils_1.convertClientIdToName)(oc.client_id, clients),
504
+ use_for_member_access: oc.use_for_member_access,
505
+ }));
506
+ }
397
507
  }
398
508
  this.existing = organizations;
399
509
  return this.existing;
@@ -539,6 +649,44 @@ class OrganizationsHandler extends default_1.default {
539
649
  logger_1.default.debug(`Deleting discovery domain ${discoveryDomain} for organization ${organizationId}`);
540
650
  await this.client.organizations.discoveryDomains.delete(organizationId, discoveryDomainId);
541
651
  }
652
+ async getAllOrganizationClients(organizationId) {
653
+ const allOrgClients = [];
654
+ try {
655
+ let orgClients = await this.client.organizations.clients.list(organizationId);
656
+ allOrgClients.push(...orgClients.data);
657
+ while (orgClients.hasNextPage()) {
658
+ orgClients = await orgClients.getNextPage();
659
+ allOrgClients.push(...orgClients.data);
660
+ }
661
+ return allOrgClients;
662
+ }
663
+ catch (err) {
664
+ if (err.statusCode === 404 || err.statusCode === 501) {
665
+ return null;
666
+ }
667
+ if (err.statusCode === 403 || err.errorCode === 'feature_not_enabled') {
668
+ logger_1.default.debug('Org-to-app entitlement is not enabled for this tenant. Skipping org-client associations.');
669
+ return null;
670
+ }
671
+ throw err;
672
+ }
673
+ }
674
+ async createOrganizationClients(organizationId, clients) {
675
+ const BATCH_SIZE = 10;
676
+ for (let i = 0; i < clients.length; i += BATCH_SIZE) {
677
+ const batch = clients.slice(i, i + BATCH_SIZE);
678
+ logger_1.default.debug(`Creating ${batch.length} org client(s) for organization ${organizationId}`);
679
+ await this.client.organizations.clients.create(organizationId, { clients: batch });
680
+ }
681
+ }
682
+ async deleteOrganizationClients(organizationId, clientIds) {
683
+ const BATCH_SIZE = 10;
684
+ for (let i = 0; i < clientIds.length; i += BATCH_SIZE) {
685
+ const batch = clientIds.slice(i, i + BATCH_SIZE);
686
+ logger_1.default.debug(`Deleting ${batch.length} org client(s) for organization ${organizationId}`);
687
+ await this.client.organizations.clients.delete(organizationId, { clients: batch });
688
+ }
689
+ }
542
690
  }
543
691
  exports.default = OrganizationsHandler;
544
692
  __decorate([
@@ -202,12 +202,20 @@ class PhoneProviderHandler extends default_1.default {
202
202
  }
203
203
  const currentProviders = await this.getPhoneProviders();
204
204
  const providerReqPayload = (() => {
205
+ // Work on a shallow copy so we don't mutate the caller's asset (phoneProviders[0]).
206
+ // The SDK response type doesn't declare `credentials`, so widen it here.
207
+ const payload = { ...phoneProviders[0] };
205
208
  // Removing id from update and create payloads, otherwise API will error
206
209
  // id may be required to handle if `--export_ids=true`
207
- const payload = phoneProviders[0];
208
- if (payload && 'id' in payload) {
210
+ if ('id' in payload) {
209
211
  delete payload.id;
210
212
  }
213
+ // The `custom` provider requires a `credentials` object in the payload even
214
+ // when it is empty. Exported configs omit credentials, so inject an empty
215
+ // object here to avoid a "Missing required property: credentials" 400 error.
216
+ if (payload.name === 'custom' && payload.credentials == null) {
217
+ payload.credentials = {};
218
+ }
211
219
  return payload;
212
220
  })();
213
221
  if (currentProviders === null || currentProviders.length === 0) {
@@ -54,7 +54,10 @@ export default class PhoneTemplatesHandler extends DefaultHandler {
54
54
  objString(template: any): string;
55
55
  getType(): Promise<PhoneTemplate[]>;
56
56
  processChanges(assets: Assets): Promise<void>;
57
- createPhoneTemplate(template: any): Promise<Asset>;
57
+ createPhoneTemplate(template: any): Promise<{
58
+ asset: Asset;
59
+ action: 'create' | 'update';
60
+ }>;
58
61
  createPhoneTemplates(creates: CalculatedChanges['create']): Promise<void>;
59
62
  updatePhoneTemplate(template: any): Promise<Asset>;
60
63
  updatePhoneTemplates(updates: CalculatedChanges['update']): Promise<void>;
@@ -44,6 +44,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.schema = void 0;
46
46
  const default_1 = __importStar(require("./default"));
47
+ const utils_1 = require("../../utils");
47
48
  const logger_1 = __importDefault(require("../../../logger"));
48
49
  exports.schema = {
49
50
  type: 'array',
@@ -121,25 +122,77 @@ class PhoneTemplatesHandler extends default_1.default {
121
122
  if (del.length > 0) {
122
123
  await this.deletePhoneTemplates(del);
123
124
  }
124
- if (create.length > 0) {
125
- await this.createPhoneTemplates(create);
125
+ // On newly created tenants the phone templates returned by the API have no
126
+ // ID until they are explicitly created. calcChanges matches assets by `type`
127
+ // and routes them all to `update`, but PATCH requires an ID. Split out the
128
+ // templates whose existing counterpart has no ID yet and create them instead.
129
+ const missingId = [];
130
+ const hasId = [];
131
+ update.forEach((template) => {
132
+ const existing = this.existing?.find((t) => t.type === template.type);
133
+ if (existing?.id) {
134
+ hasId.push(template);
135
+ }
136
+ else {
137
+ missingId.push(template);
138
+ }
139
+ });
140
+ const toCreate = [...create, ...missingId];
141
+ if (toCreate.length > 0) {
142
+ await this.createPhoneTemplates(toCreate);
126
143
  }
127
- if (update.length > 0) {
128
- await this.updatePhoneTemplates(update);
144
+ if (hasId.length > 0) {
145
+ await this.updatePhoneTemplates(hasId);
129
146
  }
130
147
  }
131
148
  async createPhoneTemplate(template) {
132
- const created = await this.client.branding.phone.templates.create(template);
133
- return created;
149
+ // The create endpoint only accepts type/disabled/content; strip read-only
150
+ // fields (channel, customizable, tenant) that the API would reject.
151
+ const createPayload = (0, utils_1.stripFields)(template, this.stripCreateFields);
152
+ // The API rejects an empty `content.from`, which is what fresh tenants
153
+ // export. Drop it when blank (cloning `content`, which stripFields only
154
+ // shallow-copied, so the caller's original asset isn't mutated).
155
+ if (createPayload.content && !createPayload.content.from) {
156
+ createPayload.content = { ...createPayload.content };
157
+ delete createPayload.content.from;
158
+ }
159
+ try {
160
+ const created = await this.client.branding.phone.templates.create(createPayload);
161
+ return { asset: created, action: 'create' };
162
+ }
163
+ catch (err) {
164
+ // A 409 means the template already exists on the tenant (it was created
165
+ // between our list call and this create, or the list endpoint didn't
166
+ // surface its ID). Re-fetch to pick up the ID and fall back to an update.
167
+ if (err.statusCode === 409) {
168
+ logger_1.default.debug(`Phone template type '${template.type}' already exists, falling back to update`);
169
+ // Reassigns the shared `this.existing`; safe under concurrency since
170
+ // every re-fetch returns the same consistent list. If it still lacks an
171
+ // ID, updatePhoneTemplate warns and skips.
172
+ const response = await this.client.branding.phone.templates.list();
173
+ this.existing = response.templates ?? [];
174
+ const updated = await this.updatePhoneTemplate(template);
175
+ return { asset: updated, action: 'update' };
176
+ }
177
+ throw err;
178
+ }
134
179
  }
135
180
  async createPhoneTemplates(creates) {
136
181
  await this.client.pool
137
182
  .addEachTask({
138
183
  data: creates || [],
139
184
  generator: (item) => this.createPhoneTemplate(item)
140
- .then((data) => {
141
- this.didCreate(data);
142
- this.created += 1;
185
+ .then(({ asset, action }) => {
186
+ // A 409 fallback actually performed an update; count it as such so
187
+ // the import summary doesn't report a phantom "create".
188
+ if (action === 'update') {
189
+ this.didUpdate(asset);
190
+ this.updated += 1;
191
+ }
192
+ else {
193
+ this.didCreate(asset);
194
+ this.created += 1;
195
+ }
143
196
  })
144
197
  .catch((err) => {
145
198
  throw new Error(`Problem creating ${this.type} ${this.objString(item)}\n${err}`);
@@ -160,7 +213,6 @@ class PhoneTemplatesHandler extends default_1.default {
160
213
  logger_1.default.debug(`Stripping ${this.type} create-only fields ${JSON.stringify(stripUpdateFields)}`);
161
214
  const updatePayload = {
162
215
  content: {
163
- from: template.content?.from,
164
216
  body: {
165
217
  text: template.content?.body?.text,
166
218
  voice: template.content?.body?.voice,
@@ -168,6 +220,11 @@ class PhoneTemplatesHandler extends default_1.default {
168
220
  },
169
221
  disabled: template.disabled,
170
222
  };
223
+ // `content.from` is optional but the API rejects an empty string, which is
224
+ // what fresh tenants export. Only include it when it has a value.
225
+ if (template.content?.from) {
226
+ updatePayload.content.from = template.content.from;
227
+ }
171
228
  const updated = await this.client.branding.phone.templates.update(existing.id, updatePayload);
172
229
  return updated;
173
230
  }
@@ -14,6 +14,10 @@ export declare const schema: {
14
14
  description: {
15
15
  type: string;
16
16
  };
17
+ type: {
18
+ type: string;
19
+ enum: string[];
20
+ };
17
21
  permissions: {
18
22
  type: string;
19
23
  items: {
@@ -56,6 +56,7 @@ exports.schema = {
56
56
  name: { type: 'string' },
57
57
  id: { type: 'string' },
58
58
  description: { type: 'string' },
59
+ type: { type: 'string', enum: ['tenant', 'organization'] },
59
60
  permissions: {
60
61
  type: 'array',
61
62
  items: {
@@ -81,6 +82,9 @@ class RolesHandler extends default_1.default {
81
82
  async createRole(data) {
82
83
  const role = { ...data };
83
84
  delete role.permissions;
85
+ // deploy-cli only manages tenant-level roles, so `type` is stripped on write; it
86
+ // defaults to `tenant` on create. This keeps exports (which carry `type`) round-trippable.
87
+ delete role.type;
84
88
  const created = await this.client.roles.create(role);
85
89
  if (created.id && typeof data.permissions !== 'undefined' && data.permissions.length > 0) {
86
90
  await this.client.roles.permissions.add(created.id, { permissions: data.permissions });
@@ -133,6 +137,9 @@ class RolesHandler extends default_1.default {
133
137
  const newPermissions = data.permissions;
134
138
  delete data.permissions;
135
139
  delete data.id;
140
+ // deploy-cli only manages tenant-level roles, so `type` is stripped on write; this
141
+ // lets files that already carry it (from a prior export) be imported cleanly.
142
+ delete data.type;
136
143
  await this.client.roles.update(params.id, data);
137
144
  if (typeof existingRole.permissions !== 'undefined' && existingRole.permissions.length > 0) {
138
145
  await this.client.roles.permissions.delete(params.id, {
@@ -164,9 +171,12 @@ class RolesHandler extends default_1.default {
164
171
  return this.existing;
165
172
  }
166
173
  try {
174
+ // Only manage tenant-level roles; exclude org-scoped roles surfaced when
175
+ // the api2_org_level_roles_ea flag is enabled.
167
176
  const roles = await (0, client_1.paginate)(this.client.roles.list, {
168
177
  paginate: true,
169
178
  include_totals: true,
179
+ type: 'tenant',
170
180
  });
171
181
  for (let index = 0; index < roles.length; index++) {
172
182
  const allPermission = [];
@@ -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)) {
package/lib/types.d.ts CHANGED
@@ -31,6 +31,7 @@ type SharedPaginationParams = {
31
31
  include_totals?: boolean;
32
32
  id?: string;
33
33
  strategy?: Management.ConnectionStrategyEnum[];
34
+ type?: Management.RoleTypeEnum;
34
35
  };
35
36
  export type CheckpointPaginationParams = SharedPaginationParams & {
36
37
  from?: string;
package/lib/utils.js CHANGED
@@ -301,12 +301,15 @@ exports.decodeBase64ToCertString = decodeBase64ToCertString;
301
301
  // Format connection options by converting client IDs to client names for SAML connections
302
302
  const getFormattedOptions = (connection, clients) => {
303
303
  try {
304
+ const { idpinitiated } = connection.options;
304
305
  return {
305
306
  options: {
306
307
  ...connection.options,
307
308
  idpinitiated: {
308
- ...connection.options.idpinitiated,
309
- client_id: convertClientIdToName(connection.options.idpinitiated.client_id, clients),
309
+ ...idpinitiated,
310
+ ...(idpinitiated.client_id && {
311
+ client_id: convertClientIdToName(idpinitiated.client_id, clients),
312
+ }),
310
313
  },
311
314
  },
312
315
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth0-deploy-cli",
3
- "version": "8.42.0",
3
+ "version": "8.44.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": "^6.1.0",
38
+ "auth0": "^6.3.0",
39
39
  "chalk": "5.6.2",
40
40
  "dot-prop": "^5.3.0",
41
41
  "fs-extra": "^10.1.0",