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.
- package/CHANGELOG.md +51 -1
- package/lib/context/defaults.d.ts +7 -0
- package/lib/context/defaults.js +74 -30
- package/lib/context/directory/handlers/actionModules.js +11 -12
- package/lib/context/directory/handlers/actions.js +11 -13
- package/lib/context/directory/handlers/attackProtection.js +1 -1
- package/lib/context/directory/handlers/branding.js +9 -1
- package/lib/context/directory/handlers/clients.js +9 -3
- package/lib/context/directory/handlers/connections.js +10 -4
- package/lib/context/directory/handlers/databases.js +4 -4
- package/lib/context/directory/handlers/emailTemplates.js +9 -0
- package/lib/context/directory/handlers/hooks.js +12 -1
- package/lib/context/directory/handlers/prompts.js +11 -3
- package/lib/context/directory/handlers/rules.js +12 -1
- package/lib/context/directory/index.js +7 -8
- package/lib/context/yaml/handlers/attackProtection.js +1 -1
- package/lib/context/yaml/handlers/branding.js +8 -1
- package/lib/context/yaml/handlers/clients.js +4 -1
- package/lib/context/yaml/handlers/connections.js +3 -1
- package/lib/context/yaml/handlers/flows.js +7 -0
- package/lib/context/yaml/handlers/forms.js +7 -0
- package/lib/context/yaml/handlers/prompts.js +7 -1
- package/lib/context/yaml/index.js +7 -8
- package/lib/readonly.js +5 -0
- package/lib/tools/auth0/handlers/attackProtection.js +3 -1
- package/lib/tools/auth0/handlers/clients.d.ts +21 -0
- package/lib/tools/auth0/handlers/clients.js +21 -0
- 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 +11 -6
- package/lib/tools/auth0/handlers/connections.js +33 -7
- package/lib/tools/auth0/handlers/databases.js +12 -3
- package/lib/tools/auth0/handlers/default.d.ts +23 -0
- package/lib/tools/auth0/handlers/default.js +21 -10
- package/lib/tools/auth0/handlers/organizations.d.ts +29 -1
- package/lib/tools/auth0/handlers/organizations.js +165 -17
- 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/roles.d.ts +4 -0
- package/lib/tools/auth0/handlers/roles.js +10 -0
- package/lib/tools/calculateDryRunChanges.js +12 -8
- package/lib/types.d.ts +1 -0
- package/lib/utils.js +5 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
125
|
-
|
|
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 (
|
|
128
|
-
await this.updatePhoneTemplates(
|
|
144
|
+
if (hasId.length > 0) {
|
|
145
|
+
await this.updatePhoneTemplates(hasId);
|
|
129
146
|
}
|
|
130
147
|
}
|
|
131
148
|
async createPhoneTemplate(template) {
|
|
132
|
-
|
|
133
|
-
|
|
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((
|
|
141
|
-
|
|
142
|
-
|
|
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
|
}
|
|
@@ -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
|
|
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/types.d.ts
CHANGED
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
|
-
...
|
|
309
|
-
|
|
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.
|
|
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.
|
|
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",
|