auth0-deploy-cli 8.41.0 → 8.43.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 (42) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/lib/context/directory/handlers/actionModules.js +11 -12
  3. package/lib/context/directory/handlers/actions.js +11 -13
  4. package/lib/context/directory/handlers/databases.js +4 -4
  5. package/lib/context/directory/handlers/hooks.js +12 -1
  6. package/lib/context/directory/handlers/rules.js +12 -1
  7. package/lib/context/directory/index.js +7 -8
  8. package/lib/context/yaml/index.js +7 -8
  9. package/lib/tools/auth0/handlers/attackProtection.js +16 -5
  10. package/lib/tools/auth0/handlers/clientAuthCredentials.d.ts +2 -1
  11. package/lib/tools/auth0/handlers/clientAuthCredentials.js +7 -0
  12. package/lib/tools/auth0/handlers/clientAuthCredentialsPre.d.ts +2 -1
  13. package/lib/tools/auth0/handlers/clientAuthCredentialsPre.js +5 -0
  14. package/lib/tools/auth0/handlers/clientGrants.d.ts +1 -1
  15. package/lib/tools/auth0/handlers/clients.d.ts +46 -0
  16. package/lib/tools/auth0/handlers/clients.js +91 -1
  17. package/lib/tools/auth0/handlers/connections.d.ts +32 -6
  18. package/lib/tools/auth0/handlers/connections.js +39 -4
  19. package/lib/tools/auth0/handlers/databases.js +11 -2
  20. package/lib/tools/auth0/handlers/default.js +6 -6
  21. package/lib/tools/auth0/handlers/guardianFactorTemplates.js +13 -4
  22. package/lib/tools/auth0/handlers/guardianPhoneFactorMessageTypes.js +11 -9
  23. package/lib/tools/auth0/handlers/guardianPhoneFactorSelectedProvider.js +11 -9
  24. package/lib/tools/auth0/handlers/hooks.js +1 -1
  25. package/lib/tools/auth0/handlers/networkACLs.d.ts +18 -0
  26. package/lib/tools/auth0/handlers/networkACLs.js +9 -0
  27. package/lib/tools/auth0/handlers/organizations.d.ts +28 -0
  28. package/lib/tools/auth0/handlers/organizations.js +127 -2
  29. package/lib/tools/auth0/handlers/phoneTemplates.d.ts +4 -1
  30. package/lib/tools/auth0/handlers/phoneTemplates.js +67 -10
  31. package/lib/tools/auth0/handlers/roles.d.ts +4 -0
  32. package/lib/tools/auth0/handlers/roles.js +10 -0
  33. package/lib/tools/auth0/handlers/rules.js +19 -7
  34. package/lib/tools/auth0/handlers/tenant.d.ts +23 -0
  35. package/lib/tools/auth0/handlers/tenant.js +24 -0
  36. package/lib/tools/auth0/handlers/themes.d.ts +37 -1
  37. package/lib/tools/auth0/handlers/themes.js +64 -0
  38. package/lib/tools/auth0/index.js +9 -0
  39. package/lib/tools/utils.d.ts +1 -0
  40. package/lib/tools/utils.js +25 -2
  41. package/lib/types.d.ts +1 -0
  42. package/package.json +2 -2
@@ -129,6 +129,22 @@ exports.schema = {
129
129
  required: ['active'],
130
130
  additionalProperties: false,
131
131
  },
132
+ cross_app_access_requesting_app: {
133
+ type: 'object',
134
+ properties: {
135
+ active: { type: 'boolean' },
136
+ },
137
+ required: ['active'],
138
+ additionalProperties: false,
139
+ },
140
+ cross_app_access_resource_app: {
141
+ type: 'object',
142
+ properties: {
143
+ status: { type: 'string', enum: ['enabled', 'disabled'] },
144
+ },
145
+ required: ['status'],
146
+ additionalProperties: false,
147
+ },
132
148
  directory_provisioning_configuration: {
133
149
  type: 'object',
134
150
  properties: {
@@ -156,6 +172,9 @@ exports.schema = {
156
172
  type: 'object',
157
173
  properties: {
158
174
  id: { type: 'string' },
175
+ name: { type: 'string' },
176
+ email: { type: 'string' },
177
+ direct_members_count: { type: 'number' },
159
178
  },
160
179
  required: ['id'],
161
180
  },
@@ -467,12 +486,28 @@ class ConnectionsHandler extends default_1.default {
467
486
  try {
468
487
  const groups = [];
469
488
  let page = await this.client.connections.directoryProvisioning.listSynchronizedGroups(connectionId, {});
470
- for (const g of page.data ?? [])
471
- groups.push({ id: g.id });
489
+ for (const g of page.data ?? []) {
490
+ groups.push({
491
+ id: g.id,
492
+ ...(g.name !== undefined && { name: g.name }),
493
+ ...(g.email !== undefined && { email: g.email }),
494
+ ...(g.direct_members_count !== undefined && {
495
+ direct_members_count: g.direct_members_count,
496
+ }),
497
+ });
498
+ }
472
499
  while (page.hasNextPage?.()) {
473
500
  page = await page.getNextPage();
474
- for (const g of page.data ?? [])
475
- groups.push({ id: g.id });
501
+ for (const g of page.data ?? []) {
502
+ groups.push({
503
+ id: g.id,
504
+ ...(g.name !== undefined && { name: g.name }),
505
+ ...(g.email !== undefined && { email: g.email }),
506
+ ...(g.direct_members_count !== undefined && {
507
+ direct_members_count: g.direct_members_count,
508
+ }),
509
+ });
510
+ }
476
511
  }
477
512
  return groups;
478
513
  }
@@ -462,12 +462,21 @@ class DatabaseHandler extends default_1.default {
462
462
  }
463
463
  return connection;
464
464
  });
465
+ // A connection can return both legacy password fields and `password_options` on read, but
466
+ // they're mutually exclusive on write. Strip the legacy fields to keep the export deployable.
467
+ const dbConnectionsNormalized = dbConnectionsWithActionNames.map((connection) => {
468
+ if (connection.options && 'password_options' in connection.options) {
469
+ const { passwordPolicy, password_complexity_options, password_history, password_no_personal_info, password_dictionary, ...options } = connection.options;
470
+ return { ...connection, options };
471
+ }
472
+ return connection;
473
+ });
465
474
  // If options option is empty for all connection, log the missing options scope.
466
- const isOptionExists = dbConnectionsWithActionNames.every((c) => c.options && Object.keys(c.options).length > 0);
475
+ const isOptionExists = dbConnectionsNormalized.every((c) => c.options && Object.keys(c.options).length > 0);
467
476
  if (!isOptionExists) {
468
477
  logger_1.default.warn(`Insufficient scope the read:connections_options scope is required to get ${this.type} options.`);
469
478
  }
470
- this.existing = dbConnectionsWithActionNames;
479
+ this.existing = dbConnectionsNormalized;
471
480
  return this.existing;
472
481
  }
473
482
  async calcChanges(assets) {
@@ -328,8 +328,8 @@ class APIHandler {
328
328
  const updateFN = this.getClientFN(this.functions.update);
329
329
  const updatePayload = (() => {
330
330
  const data = (0, utils_1.stripFields)({ ...updateItem }, this.stripUpdateFields);
331
- const stripped = (0, utils_1.stripObfuscatedFieldsFromPayload)(data, this.sensitiveFieldsToObfuscate);
332
- return (0, utils_1.validateNoUnresolvedPlaceholders)(stripped, this.type, this.objString(updateItem));
331
+ const obfuscatedStripped = (0, utils_1.stripObfuscatedFieldsFromPayload)(data, this.sensitiveFieldsToObfuscate);
332
+ return (0, utils_1.stripUnresolvedPlaceholders)(obfuscatedStripped, this.type, this.objString(updateItem));
333
333
  })();
334
334
  return updateFN(updateItem[this.id], updatePayload);
335
335
  }, retryConfig)
@@ -347,8 +347,8 @@ class APIHandler {
347
347
  const createFunction = this.getClientFN(this.functions.create);
348
348
  const createPayload = (() => {
349
349
  const strippedPayload = (0, utils_1.stripFields)(createItem, this.stripCreateFields);
350
- const stripped = (0, utils_1.stripObfuscatedFieldsFromPayload)(strippedPayload, this.sensitiveFieldsToObfuscate);
351
- return (0, utils_1.validateNoUnresolvedPlaceholders)(stripped, this.type, this.objString(createItem));
350
+ const obfuscatedStripped = (0, utils_1.stripObfuscatedFieldsFromPayload)(strippedPayload, this.sensitiveFieldsToObfuscate);
351
+ return (0, utils_1.stripUnresolvedPlaceholders)(obfuscatedStripped, this.type, this.objString(createItem));
352
352
  })();
353
353
  return createFunction(createPayload);
354
354
  }, retryConfig)
@@ -369,8 +369,8 @@ class APIHandler {
369
369
  const updateFN = this.getClientFN(this.functions.update);
370
370
  const updatePayload = (() => {
371
371
  const data = (0, utils_1.stripFields)({ ...updateItem }, this.stripUpdateFields);
372
- const stripped = (0, utils_1.stripObfuscatedFieldsFromPayload)(data, this.sensitiveFieldsToObfuscate);
373
- return (0, utils_1.validateNoUnresolvedPlaceholders)(stripped, this.type, this.objString(updateItem));
372
+ const obfuscatedStripped = (0, utils_1.stripObfuscatedFieldsFromPayload)(data, this.sensitiveFieldsToObfuscate);
373
+ return (0, utils_1.stripUnresolvedPlaceholders)(obfuscatedStripped, this.type, this.objString(updateItem));
374
374
  })();
375
375
  return updateFN(updateItem[this.id], updatePayload);
376
376
  }, retryConfig)
@@ -60,11 +60,20 @@ class GuardianFactorTemplatesHandler extends default_1.default {
60
60
  await Promise.all(guardianFactorTemplates.map(async (fatorTemplates) => {
61
61
  const { name, ...data } = fatorTemplates;
62
62
  const params = { name: fatorTemplates.name };
63
- if (name === 'sms') {
64
- await this.client.guardian.factors.sms.setTemplates(data);
63
+ try {
64
+ if (name === 'sms') {
65
+ await this.client.guardian.factors.sms.setTemplates(data);
66
+ }
67
+ else if (name === 'phone') {
68
+ await this.client.guardian.factors.phone.setTemplates(data);
69
+ }
65
70
  }
66
- else if (name === 'phone') {
67
- await this.client.guardian.factors.phone.setTemplates(data);
71
+ catch (err) {
72
+ if ((0, utils_1.isForbiddenFeatureError)(err, this.type)) {
73
+ // Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import.
74
+ return;
75
+ }
76
+ throw err;
68
77
  }
69
78
  this.didUpdate(params);
70
79
  this.updated += 1;
@@ -25,14 +25,7 @@ const isFeatureUnavailableError = (err) => {
25
25
  // Older Management API version where the endpoint is not available.
26
26
  return true;
27
27
  }
28
- if (err.statusCode === 403 &&
29
- err.originalError &&
30
- err.originalError.response &&
31
- err.originalError.response.body &&
32
- err.originalError.response.body.errorCode === 'voice_mfa_not_allowed') {
33
- // Recent Management API version, but with feature explicitly disabled.
34
- return true;
35
- }
28
+ // 403s (feature explicitly disabled) are handled by isForbiddenFeatureError.
36
29
  return false;
37
30
  };
38
31
  class GuardianPhoneMessageTypesHandler extends default_1.default {
@@ -75,7 +68,16 @@ class GuardianPhoneMessageTypesHandler extends default_1.default {
75
68
  }
76
69
  }
77
70
  const data = guardianPhoneFactorMessageTypes;
78
- await this.client.guardian.factors.phone.setMessageTypes(data);
71
+ try {
72
+ await this.client.guardian.factors.phone.setMessageTypes(data);
73
+ }
74
+ catch (err) {
75
+ if (isFeatureUnavailableError(err) || (0, utils_1.isForbiddenFeatureError)(err, this.type)) {
76
+ // Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import.
77
+ return;
78
+ }
79
+ throw err;
80
+ }
79
81
  this.updated += 1;
80
82
  this.didUpdate(guardianPhoneFactorMessageTypes);
81
83
  }
@@ -22,14 +22,7 @@ const isFeatureUnavailableError = (err) => {
22
22
  // Older Management API version where the endpoint is not available.
23
23
  return true;
24
24
  }
25
- if (err.statusCode === 403 &&
26
- err.originalError &&
27
- err.originalError.response &&
28
- err.originalError.response.body &&
29
- err.originalError.response.body.errorCode === 'hooks_not_allowed') {
30
- // Recent Management API version, but with feature explicitly disabled.
31
- return true;
32
- }
25
+ // 403s (feature explicitly disabled) are handled by isForbiddenFeatureError.
33
26
  return false;
34
27
  };
35
28
  class GuardianPhoneSelectedProviderHandler extends default_1.default {
@@ -72,7 +65,16 @@ class GuardianPhoneSelectedProviderHandler extends default_1.default {
72
65
  }
73
66
  }
74
67
  const data = guardianPhoneFactorSelectedProvider;
75
- await this.client.guardian.factors.phone.setProvider(data);
68
+ try {
69
+ await this.client.guardian.factors.phone.setProvider(data);
70
+ }
71
+ catch (err) {
72
+ if (isFeatureUnavailableError(err) || (0, utils_1.isForbiddenFeatureError)(err, this.type)) {
73
+ // Feature is deprecated/disabled on this tenant; warn and skip instead of failing the import.
74
+ return;
75
+ }
76
+ throw err;
77
+ }
76
78
  this.updated += 1;
77
79
  this.didUpdate(guardianPhoneFactorSelectedProvider);
78
80
  }
@@ -151,7 +151,7 @@ class HooksHandler extends default_1.default {
151
151
  .get(hook.id)
152
152
  .then((hookWithCode) => this.client.hooks.secrets
153
153
  .get(hook.id)
154
- .then(({ data: secrets }) => ({ ...hookWithCode, secrets })))));
154
+ .then((secrets) => ({ ...hookWithCode, secrets })))));
155
155
  return this.existing;
156
156
  }
157
157
  catch (err) {
@@ -85,6 +85,15 @@ export declare const schema: {
85
85
  uniqueItems: boolean;
86
86
  minItems: number;
87
87
  };
88
+ auth0_managed: {
89
+ type: string;
90
+ items: {
91
+ type: string;
92
+ pattern: string;
93
+ };
94
+ uniqueItems: boolean;
95
+ minItems: number;
96
+ };
88
97
  geo_country_codes: {
89
98
  type: string;
90
99
  items: {
@@ -176,6 +185,15 @@ export declare const schema: {
176
185
  uniqueItems: boolean;
177
186
  minItems: number;
178
187
  };
188
+ auth0_managed: {
189
+ type: string;
190
+ items: {
191
+ type: string;
192
+ pattern: string;
193
+ };
194
+ uniqueItems: boolean;
195
+ minItems: number;
196
+ };
179
197
  geo_country_codes: {
180
198
  type: string;
181
199
  items: {
@@ -69,6 +69,15 @@ const MatchSchema = {
69
69
  uniqueItems: true,
70
70
  minItems: 1,
71
71
  },
72
+ auth0_managed: {
73
+ type: 'array',
74
+ items: {
75
+ type: 'string',
76
+ pattern: '^auth0\\.[^.\\s]+$',
77
+ },
78
+ uniqueItems: true,
79
+ minItems: 1,
80
+ },
72
81
  geo_country_codes: {
73
82
  type: 'array',
74
83
  items: {
@@ -1,6 +1,7 @@
1
1
  import { Management } from 'auth0';
2
2
  import DefaultHandler from './default';
3
3
  import { Asset, Assets, CalculatedChanges } from '../../../types';
4
+ import { Client } from './clients';
4
5
  export declare const schema: {
5
6
  type: string;
6
7
  items: {
@@ -108,6 +109,25 @@ export declare const schema: {
108
109
  type: string;
109
110
  enum: ("allow" | "block")[];
110
111
  };
112
+ is_app_entitlement_active: {
113
+ type: string;
114
+ };
115
+ clients: {
116
+ type: string;
117
+ items: {
118
+ type: string;
119
+ properties: {
120
+ client_id: {
121
+ type: string;
122
+ };
123
+ use_for_member_access: {
124
+ type: string;
125
+ };
126
+ };
127
+ required: string[];
128
+ };
129
+ default: never[];
130
+ };
111
131
  };
112
132
  required: string[];
113
133
  };
@@ -119,6 +139,7 @@ type FormattedClientGrants = {
119
139
  export default class OrganizationsHandler extends DefaultHandler {
120
140
  existing: Asset[];
121
141
  formattedClientGrants: FormattedClientGrants[];
142
+ allClients: Client[];
122
143
  constructor(config: DefaultHandler);
123
144
  deleteOrganization(org: any): Promise<void>;
124
145
  deleteOrganizations(data: Asset[]): Promise<void>;
@@ -128,6 +149,7 @@ export default class OrganizationsHandler extends DefaultHandler {
128
149
  id: any;
129
150
  }>;
130
151
  getClientGrantIDByClientName(clientsName: string): string;
152
+ getClientIdByClientName(clientName: string): string;
131
153
  getFormattedClientGrants(): Promise<FormattedClientGrants[]>;
132
154
  updateOrganizations(updates: CalculatedChanges['update'], orgs: Asset[]): Promise<void>;
133
155
  getType(): Promise<Asset[]>;
@@ -141,5 +163,11 @@ export default class OrganizationsHandler extends DefaultHandler {
141
163
  createOrganizationDiscoveryDomain(organizationId: string, discoveryDomain: Management.CreateOrganizationDiscoveryDomainRequestContent): Promise<Management.CreateOrganizationDiscoveryDomainResponseContent>;
142
164
  updateOrganizationDiscoveryDomain(organizationId: string, discoveryDomainId: string, discoveryDomain: string, discoveryDomainUpdate: Management.UpdateOrganizationDiscoveryDomainRequestContent): Promise<Management.UpdateOrganizationDiscoveryDomainResponseContent>;
143
165
  deleteOrganizationDiscoveryDomain(organizationId: string, discoveryDomain: string, discoveryDomainId: string): Promise<void>;
166
+ getAllOrganizationClients(organizationId: string): Promise<Management.OrganizationClient[] | null>;
167
+ createOrganizationClients(organizationId: string, clients: Array<{
168
+ client_id: string;
169
+ use_for_member_access: boolean;
170
+ }>): Promise<void>;
171
+ deleteOrganizationClients(organizationId: string, clientIds: string[]): Promise<void>;
144
172
  }
145
173
  export {};
@@ -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
  }
@@ -195,6 +209,19 @@ class OrganizationsHandler extends default_1.default {
195
209
  throw new Error(`Problem creating discovery domain ${discoveryDomain?.domain} for organization ${createdId}\n${err}`);
196
210
  })));
197
211
  }
212
+ if (typeof org.clients !== 'undefined' && org.clients.length > 0) {
213
+ const clientsToCreate = org.clients
214
+ .map((oc) => ({
215
+ client_id: this.getClientIdByClientName(oc.client_id),
216
+ use_for_member_access: oc.use_for_member_access,
217
+ }))
218
+ .filter((oc) => !!oc.client_id);
219
+ if (clientsToCreate.length > 0) {
220
+ await this.createOrganizationClients(createdId, clientsToCreate).catch((err) => {
221
+ throw new Error(`Problem creating org clients for organization ${createdId}\n${err}`);
222
+ });
223
+ }
224
+ }
198
225
  return created;
199
226
  }
200
227
  async createOrganizations(creates) {
@@ -213,14 +240,15 @@ class OrganizationsHandler extends default_1.default {
213
240
  .promise();
214
241
  }
215
242
  async updateOrganization(org, organizations) {
216
- const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, } = await organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
243
+ const { connections: existingConnections, client_grants: existingClientGrants, discovery_domains: existingDiscoveryDomains, clients: existingOrgClients = [], } = await organizations.find((orgToUpdate) => orgToUpdate.name === org.name);
217
244
  const params = { id: org.id };
218
- const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, } = org;
245
+ const { connections, client_grants: organizationClientGrants, discovery_domains: organizationDiscoveryDomains, clients: organizationClients, } = org;
219
246
  delete org.connections;
220
247
  delete org.name;
221
248
  delete org.id;
222
249
  delete org.client_grants;
223
250
  delete org.discovery_domains;
251
+ delete org.clients;
224
252
  await this.client.organizations.update(params.id, org);
225
253
  // organization connections
226
254
  const connectionsToRemove = existingConnections.filter((c) => !connections.find((x) => x.connection_id === c.connection_id));
@@ -324,12 +352,61 @@ class OrganizationsHandler extends default_1.default {
324
352
  \n${orgDiscoveryDomainsToRemove.map((i) => this.objString(i)).join('\n')}`);
325
353
  }
326
354
  }
355
+ // organization clients
356
+ const orgClientsToAdd = (organizationClients || [])
357
+ .filter((c) => !existingOrgClients.find((x) => x.client_id === c.client_id))
358
+ .map((oc) => ({
359
+ client_id: this.getClientIdByClientName(oc.client_id),
360
+ use_for_member_access: oc.use_for_member_access,
361
+ }))
362
+ .filter((oc) => !!oc.client_id);
363
+ const orgClientsToRemove = (existingOrgClients || [])
364
+ .filter((c) => !(organizationClients || []).find((x) => x.client_id === c.client_id))
365
+ .map((oc) => this.getClientIdByClientName(oc.client_id))
366
+ .filter(Boolean);
367
+ const orgClientsToUpdate = (organizationClients || [])
368
+ .filter((c) => {
369
+ const existing = existingOrgClients.find((x) => x.client_id === c.client_id);
370
+ return existing && existing.use_for_member_access !== c.use_for_member_access;
371
+ })
372
+ .map((oc) => ({
373
+ client_id: this.getClientIdByClientName(oc.client_id),
374
+ use_for_member_access: oc.use_for_member_access,
375
+ }))
376
+ .filter((oc) => !!oc.client_id);
377
+ if (orgClientsToAdd.length > 0) {
378
+ await this.createOrganizationClients(params.id, orgClientsToAdd).catch((err) => {
379
+ throw new Error(`Problem adding org clients for organization ${params.id}\n${err}`);
380
+ });
381
+ }
382
+ if (orgClientsToRemove.length > 0) {
383
+ if (this.config('AUTH0_ALLOW_DELETE') === 'true' ||
384
+ this.config('AUTH0_ALLOW_DELETE') === true) {
385
+ await this.deleteOrganizationClients(params.id, orgClientsToRemove).catch((err) => {
386
+ throw new Error(`Problem removing org clients for organization ${params.id}\n${err}`);
387
+ });
388
+ }
389
+ else {
390
+ 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')}`);
391
+ }
392
+ }
393
+ await Promise.all(orgClientsToUpdate.map((oc) => this.client.organizations.clients
394
+ .update(params.id, oc.client_id, {
395
+ use_for_member_access: oc.use_for_member_access,
396
+ })
397
+ .catch((err) => {
398
+ throw new Error(`Problem updating org client ${oc.client_id} for organization ${params.id}\n${err}`);
399
+ })));
327
400
  return params;
328
401
  }
329
402
  getClientGrantIDByClientName(clientsName) {
330
403
  const found = this.formattedClientGrants.find((c) => c.client_id === clientsName);
331
404
  return found?.grant_id || '';
332
405
  }
406
+ getClientIdByClientName(clientName) {
407
+ const found = this.allClients?.find((c) => c.name === clientName);
408
+ return found?.client_id || '';
409
+ }
333
410
  async getFormattedClientGrants() {
334
411
  const [clients, clientGrants] = await Promise.all([
335
412
  (0, client_1.paginate)(this.client.clients.list, {
@@ -339,6 +416,8 @@ class OrganizationsHandler extends default_1.default {
339
416
  paginate: true,
340
417
  }),
341
418
  ]);
419
+ // Store clients for org-client name→ID resolution
420
+ this.allClients = clients;
342
421
  // Convert clients by name to the id and store it in the formattedClientGrants
343
422
  const formattedClientGrantsMapping = clientGrants?.map((clientGrant) => {
344
423
  const { id, client_id: clientName } = clientGrant;
@@ -394,6 +473,14 @@ class OrganizationsHandler extends default_1.default {
394
473
  if (organizationDiscoveryDomains) {
395
474
  org.discovery_domains = organizationDiscoveryDomains;
396
475
  }
476
+ // Get org-client associations
477
+ const orgClients = await this.getAllOrganizationClients(org.id);
478
+ if (orgClients) {
479
+ org.clients = orgClients.map((oc) => ({
480
+ client_id: (0, utils_1.convertClientIdToName)(oc.client_id, clients),
481
+ use_for_member_access: oc.use_for_member_access,
482
+ }));
483
+ }
397
484
  }
398
485
  this.existing = organizations;
399
486
  return this.existing;
@@ -539,6 +626,44 @@ class OrganizationsHandler extends default_1.default {
539
626
  logger_1.default.debug(`Deleting discovery domain ${discoveryDomain} for organization ${organizationId}`);
540
627
  await this.client.organizations.discoveryDomains.delete(organizationId, discoveryDomainId);
541
628
  }
629
+ async getAllOrganizationClients(organizationId) {
630
+ const allOrgClients = [];
631
+ try {
632
+ let orgClients = await this.client.organizations.clients.list(organizationId);
633
+ allOrgClients.push(...orgClients.data);
634
+ while (orgClients.hasNextPage()) {
635
+ orgClients = await orgClients.getNextPage();
636
+ allOrgClients.push(...orgClients.data);
637
+ }
638
+ return allOrgClients;
639
+ }
640
+ catch (err) {
641
+ if (err.statusCode === 404 || err.statusCode === 501) {
642
+ return null;
643
+ }
644
+ if (err.statusCode === 403 || err.errorCode === 'feature_not_enabled') {
645
+ logger_1.default.debug('Org-to-app entitlement is not enabled for this tenant. Skipping org-client associations.');
646
+ return null;
647
+ }
648
+ throw err;
649
+ }
650
+ }
651
+ async createOrganizationClients(organizationId, clients) {
652
+ const BATCH_SIZE = 10;
653
+ for (let i = 0; i < clients.length; i += BATCH_SIZE) {
654
+ const batch = clients.slice(i, i + BATCH_SIZE);
655
+ logger_1.default.debug(`Creating ${batch.length} org client(s) for organization ${organizationId}`);
656
+ await this.client.organizations.clients.create(organizationId, { clients: batch });
657
+ }
658
+ }
659
+ async deleteOrganizationClients(organizationId, clientIds) {
660
+ const BATCH_SIZE = 10;
661
+ for (let i = 0; i < clientIds.length; i += BATCH_SIZE) {
662
+ const batch = clientIds.slice(i, i + BATCH_SIZE);
663
+ logger_1.default.debug(`Deleting ${batch.length} org client(s) for organization ${organizationId}`);
664
+ await this.client.organizations.clients.delete(organizationId, { clients: batch });
665
+ }
666
+ }
542
667
  }
543
668
  exports.default = OrganizationsHandler;
544
669
  __decorate([
@@ -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
  }