scimgateway 6.1.9 → 6.1.10

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/README.md CHANGED
@@ -1303,6 +1303,13 @@ MIT © [Jarle Elshaug](https://www.elshaug.xyz)
1303
1303
 
1304
1304
  ## Change log
1305
1305
 
1306
+ ### v6.1.10
1307
+
1308
+ [Fixed]
1309
+
1310
+ - plugin-entra-id: user group membership now includes nested (transitive) groups (`direct` and `indirect`)
1311
+ - Docker example files `config/docker/.dockerignore` and `docker-compose-mssql.yml` were missing
1312
+
1306
1313
  ### v6.1.9
1307
1314
 
1308
1315
  [Improved]
@@ -34,8 +34,8 @@ export class HelperRest {
34
34
  this.scimgateway = scimgateway
35
35
  this.idleTimeout = (scimgateway as any)?.config?.scimgateway.idleTimeout || 120
36
36
  this.idleTimeout = this.idleTimeout - 1
37
- if (optionalEntities && optionalEntities.entity) this.config_entity = utils.copyObj(optionalEntities.entity) ?? {}
38
- else this.config_entity = utils.copyObj(scimgateway.getConfig())?.entity ?? {}
37
+ if (optionalEntities && optionalEntities.entity) this.config_entity = structuredClone(optionalEntities.entity) ?? {}
38
+ else this.config_entity = structuredClone(scimgateway.getConfig())?.entity ?? {}
39
39
 
40
40
  for (const baseEntity in this.config_entity) {
41
41
  const connectionObj = this.config_entity[baseEntity]?.connection
@@ -387,7 +387,7 @@ export class HelperRest {
387
387
  const method = 'POST'
388
388
  let connOpt: any = {}
389
389
  if (connectionObj.options && typeof connectionObj.options === 'object') {
390
- connOpt = utils.copyObj(connectionObj.options)
390
+ connOpt = structuredClone(connectionObj.options)
391
391
  }
392
392
  if (!connOpt.headers) connOpt.headers = {}
393
393
  connOpt.headers['Content-Type'] = 'application/x-www-form-urlencoded' // body must be query string formatted (no JSON)
@@ -494,7 +494,7 @@ export class HelperRest {
494
494
  let orgConnection: any
495
495
  if (opt?.connection) { // allow overriding/extending configuration connection by caller argument opt.connection
496
496
  let org = connectionObj
497
- orgConnection = utils.copyObj(org)
497
+ orgConnection = structuredClone(org)
498
498
  if (!org) org = {}
499
499
  org = utils.extendObj(org, opt.connection)
500
500
  }
@@ -533,7 +533,7 @@ export class HelperRest {
533
533
  }
534
534
 
535
535
  if (connectionObj.options) { // http connect options
536
- const connOpt: any = utils.copyObj(connectionObj.options)
536
+ const connOpt: any = structuredClone(connectionObj.options)
537
537
  try {
538
538
  // using fs.readFileSync().toString() instead of Bun.file().text() for nodejs compability
539
539
  if (connOpt?.tls?.key) connOpt.tls.key = fs.readFileSync(connOpt.tls.key).toString()
@@ -565,7 +565,7 @@ export class HelperRest {
565
565
  if (ctx?.headers?.get) { // Auth PassThrough using ctx header
566
566
  this._serviceClient[baseEntity].options.headers['Authorization'] = ctx.headers.get('authorization')
567
567
  }
568
- const cli: any = utils.copyObj(this._serviceClient[baseEntity]) // client ready
568
+ const cli: any = structuredClone(this._serviceClient[baseEntity]) // client ready
569
569
 
570
570
  // failover support
571
571
  path = this._serviceClient[baseEntity].baseUrl + path
@@ -611,7 +611,7 @@ export class HelperRest {
611
611
 
612
612
  // merge any argument options - basic auth header is supported through {auth:{type:"basic",options:{username:"username",password:"password"}}}
613
613
  if (opt) {
614
- const o: any = utils.copyObj(opt)
614
+ const o: any = structuredClone(opt)
615
615
  if (o?.auth?.type === 'basic') {
616
616
  options.headers['Authorization'] = 'Basic ' + Buffer.from(`${o.auth?.options?.username}:${o.auth?.options?.password}`).toString('base64')
617
617
  delete o.auth
@@ -65,6 +65,8 @@ const config = scimgateway.getConfig()
65
65
  scimgateway.authPassThroughAllowed = false
66
66
  // end - mandatory plugin initialization
67
67
 
68
+ const newHelper = new HelperRest(scimgateway)
69
+
68
70
  if (config.map) { // having licensDetails map here instead of config file
69
71
  config.map.licenseDetails = {
70
72
  servicePlanId: {
@@ -413,6 +415,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
413
415
  const body = null
414
416
  let path
415
417
  let options: Record<string, any> = {}
418
+ let isUserMemberOf = getObj?.operator === 'eq' && getObj?.attribute === 'members.value'
416
419
 
417
420
  if (!Object.hasOwn(getObj, 'count')) getObj.count = 500
418
421
  if (getObj.count > 500) getObj.count = 500 // Entra ID max 999
@@ -428,10 +431,10 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
428
431
  if (includeMembers) path = `/groups?$filter=${getObj.attribute} eq '${getObj.value}'&$select=${attrs.join()}&$expand=members($select=id,displayName)`
429
432
  else path = `/groups?$filter=${getObj.attribute} eq '${getObj.value}'&$select=${attrs.join()}`
430
433
  }
431
- } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') {
434
+ } else if (isUserMemberOf) {
432
435
  // mandatory - return all groups the user 'id' (getObj.value) is member of - correspond to getGroupMembers() in versions < 4.x.x
433
436
  // Resources = [{ id: <id-group>> , displayName: <displayName-group>, members [{value: <id-user>}] }]
434
- path = `/users/${getObj.value}/memberOf/microsoft.graph.group?$top=${getObj.count}&$count=true&select=id,displayName`
437
+ path = `/users/${getObj.value}/transitiveMemberOf/microsoft.graph.group?$top=${getObj.count}&$count=true&select=id,displayName`
435
438
  } else {
436
439
  // optional - simpel filtering
437
440
  if (getObj.attribute) {
@@ -471,8 +474,69 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
471
474
  if (!ctx) ctx = { paging }
472
475
  else ctx.paging = paging
473
476
 
477
+ const newCtx = { ...ctx }
478
+ newCtx.paging = { startIndex: 1 }
479
+
474
480
  try {
475
- let response = await helper.doRequest(baseEntity, method, path, body, ctx, options)
481
+ let response: any
482
+ let responseMemberOf: any
483
+ if (!isUserMemberOf) response = await helper.doRequest(baseEntity, method, path, body, ctx, options)
484
+ else {
485
+ // request both the default transitiveMemberOf (includes nested groups) and memberOf because we want to distinguish SCIM type=direct/indirect
486
+ const pathMemberOf = `/users/${getObj.value}/memberOf/microsoft.graph.group?$top=${getObj.count}&$count=true&select=id,displayName`
487
+ const allErrors: string[] = []
488
+ const results = await Promise.allSettled([
489
+ helper.doRequest(baseEntity, method, path, body, ctx, options),
490
+ newHelper.doRequest(baseEntity, method, pathMemberOf, body, newCtx, options), // using newHelper to avoid shared internal helperRest paging
491
+ ])
492
+ const errors = results
493
+ .filter(r => r.status === 'rejected')
494
+ .map(r => (r as PromiseRejectedResult).reason.message)
495
+ .filter(msg => !msg.includes('already exist'))
496
+ allErrors.push(...errors)
497
+
498
+ if (allErrors.length > 0) {
499
+ throw new Error(allErrors.join(', '))
500
+ }
501
+
502
+ response = (results[0] as PromiseFulfilledResult<any>).value // includes all groups (also nested)
503
+ responseMemberOf = (results[1] as PromiseFulfilledResult<any>).value // do not include nested groups
504
+
505
+ let nextStartIndex = scimgateway.getNextStartIndex(responseMemberOf.body.value.length * 2, newCtx.paging.startIndex, responseMemberOf.body.value.length)
506
+ if (nextStartIndex > newCtx.paging.startIndex && responseMemberOf && responseMemberOf.body.value && Array.isArray(responseMemberOf.body.value)) {
507
+ // use paging to ensure responseMemberOf is complete
508
+ let totalResults = responseMemberOf.body.value.length
509
+ let startIndex = 1
510
+ let res: any
511
+ do {
512
+ try {
513
+ startIndex = nextStartIndex
514
+ newCtx.paging.startIndex = startIndex
515
+ res = await newHelper.doRequest(baseEntity, method, pathMemberOf, body, newCtx, options)
516
+ } catch (err) { void 0 }
517
+ if (res?.body && res.body.value && Array.isArray(res.body.value) && res.body.value.length > 0) {
518
+ const count = res.body.value.length
519
+ totalResults += count
520
+ nextStartIndex = scimgateway.getNextStartIndex(totalResults + count, startIndex, count)
521
+ for (let i = 0; i < res.body.value.length; i++) {
522
+ if (!res.body.value[i].id) continue
523
+ responseMemberOf.body.value.push(res.body.value[i])
524
+ }
525
+ }
526
+ } while (nextStartIndex > startIndex)
527
+ }
528
+
529
+ if (response.body && response.body.value && Array.isArray(response.body.value)) {
530
+ const directIds = new Set()
531
+ if (responseMemberOf.body && responseMemberOf.body.value && Array.isArray(responseMemberOf.body.value)) {
532
+ responseMemberOf.body.value.forEach((el: any) => directIds.add(el.id))
533
+ }
534
+ response.body.value.forEach((el: any) => {
535
+ if (directIds.has(el.id)) el.type = 'direct'
536
+ else el.type = 'indirect'
537
+ })
538
+ }
539
+ }
476
540
  if (!response.body) {
477
541
  throw new Error(`invalid response: ${JSON.stringify(response)}`)
478
542
  }
@@ -494,6 +558,7 @@ scimgateway.getGroups = async (baseEntity, getObj, attributes, ctx) => {
494
558
  } else if (getObj.operator === 'eq' && getObj.attribute === 'members.value') { // Not using expand-members. Only includes current user as member, but should have requested all...
495
559
  members = [{
496
560
  value: getObj.value,
561
+ type: response.body.value[i].type || 'direct',
497
562
  }]
498
563
  }
499
564
 
@@ -53,10 +53,12 @@ if (!fsExistsSync('../../config/wsdls/UserService.wsdl')) {
53
53
  fs.writeFileSync('../../config/wsdls/UserService.wsdl', fs.readFileSync('./config/wsdls/UserService.wsdl'))
54
54
  }
55
55
 
56
- fs.writeFileSync('../../config/docker/docker-compose.yml', fs.readFileSync('./config/docker/docker-compose.yml'))
57
- fs.writeFileSync('../../config/docker/Dockerfile', fs.readFileSync('./config/docker/Dockerfile'))
56
+ fs.writeFileSync('../../config/docker/.dockerignore', fs.readFileSync('./config/docker/.dockerignore'))
58
57
  fs.writeFileSync('../../config/docker/DataDockerfile', fs.readFileSync('./config/docker/DataDockerfile'))
59
58
  fs.writeFileSync('../../config/docker/docker-compose-debug.yml', fs.readFileSync('./config/docker/docker-compose-debug.yml'))
59
+ fs.writeFileSync('../../config/docker/docker-compose-mssql.yml', fs.readFileSync('./config/docker/docker-compose-mssql.yml'))
60
+ fs.writeFileSync('../../config/docker/docker-compose.yml', fs.readFileSync('./config/docker/docker-compose.yml'))
61
+ fs.writeFileSync('../../config/docker/Dockerfile', fs.readFileSync('./config/docker/Dockerfile'))
60
62
 
61
63
  fs.writeFileSync('../../LICENSE', fs.readFileSync('./LICENSE'))
62
64
  if (!fsExistsSync('../../index.ts')) fs.writeFileSync('../../index.ts', fs.readFileSync('./index.ts'))
@@ -757,7 +757,7 @@ export class ScimGateway {
757
757
  }
758
758
  if (utils.getEncrypted(authToken, arr[i].clientSecret) === arr[i].clientSecret) {
759
759
  arr[i].isTokenRequested = true // flagged as true to not allow repeated resolvements because token will also be cleared when expired
760
- const baseEntities = utils.copyObj(arr[i].baseEntities)
760
+ const baseEntities = structuredClone(arr[i].baseEntities)
761
761
  let expires
762
762
  let readOnly = false
763
763
  if (arr[i].readOnly && arr[i].readOnly === true) readOnly = true
@@ -857,7 +857,7 @@ export class ScimGateway {
857
857
  }
858
858
 
859
859
  const getHandlerSchemas = async (ctx: Context) => {
860
- let tx = utils.copyObj(this.scimDef.Schemas)
860
+ let tx = structuredClone(this.scimDef.Schemas)
861
861
  if (this.config.endpoint?.map) {
862
862
  // endpointMapper being used
863
863
  // Schemas returned should instead reflect what is defined in the plugin config file
@@ -891,7 +891,7 @@ export class ScimGateway {
891
891
  uniqueness: (item.mapTo === 'userName') ? 'server' : 'none',
892
892
  }
893
893
  if (item['x-agent-schema']) {
894
- const agentSchema = utils.copyObj(item['x-agent-schema'])
894
+ const agentSchema = structuredClone(item['x-agent-schema'])
895
895
  if (agentSchema.description) {
896
896
  attr.description = agentSchema.description
897
897
  delete agentSchema.description
@@ -909,7 +909,7 @@ export class ScimGateway {
909
909
  if (names.length > 1) {
910
910
  const userNameFound = attr.name.includes('userName')
911
911
  for (let i = 0; i < names.length; i++) {
912
- let attrCopy = utils.copyObj(attr)
912
+ let attrCopy = structuredClone(attr)
913
913
  const name = names[i].trim()
914
914
  attrCopy.name = name
915
915
  if (name === 'id') continue
@@ -959,7 +959,7 @@ export class ScimGateway {
959
959
  delete subAttr.uniqueness
960
960
  }
961
961
  if (item['x-agent-schema']) {
962
- const hints = utils.copyObj(item['x-agent-schema'])
962
+ const hints = structuredClone(item['x-agent-schema'])
963
963
  if (hints.description) {
964
964
  subAttr.description = hints.description
965
965
  delete hints.description
@@ -1147,7 +1147,7 @@ export class ScimGateway {
1147
1147
  ctx.request.body = body // now json - ensure final info log will be masked
1148
1148
  jsonBody = body
1149
1149
  }
1150
- jsonBody = utils.copyObj(jsonBody) // no changes to original
1150
+ jsonBody = structuredClone(jsonBody) // no changes to original
1151
1151
  } catch (err: any) {
1152
1152
  logger.error(`${gwName} [oauth] token request error: ${err.message}`)
1153
1153
  ctx.response.status = 401
@@ -1184,7 +1184,7 @@ export class ScimGateway {
1184
1184
  if (!arr[i].baseEntities.includes(baseEntity)) continue
1185
1185
  }
1186
1186
  token = utils.getEncrypted(jsonBody.client_secret, jsonBody.client_secret)
1187
- baseEntities = utils.copyObj(arr[i].baseEntities)
1187
+ baseEntities = structuredClone(arr[i].baseEntities)
1188
1188
  if (arr[i].readOnly && arr[i].readOnly === true) readOnly = true
1189
1189
  if (arr[i].expires_in && !isNaN(arr[i].expires_in)) expires = arr[i].expires_in
1190
1190
  else expires = oAuthTokenExpire
@@ -1291,7 +1291,7 @@ export class ScimGateway {
1291
1291
  logger.debug(`${gwName} [Get ${handle.description}] ${getObj.attribute}=${getObj.value}`, { baseEntity: ctx?.routeObj?.baseEntity })
1292
1292
 
1293
1293
  try {
1294
- const ob = utils.copyObj(getObj)
1294
+ const ob = structuredClone(getObj)
1295
1295
  const attributes: string[] = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
1296
1296
  if (attributes.length > 0 && !attributes.includes('id')) attributes.push('id')
1297
1297
  logger.debug(`${gwName} calling ${handle.getMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
@@ -1523,7 +1523,7 @@ export class ScimGateway {
1523
1523
  getObj.count = ctx.query.count ? parseInt(ctx.query.count, 10) : 200 // defaults to 200 (plugin may override)
1524
1524
 
1525
1525
  let res: any
1526
- const obj: any = utils.copyObj(getObj)
1526
+ const obj: any = structuredClone(getObj)
1527
1527
  const attributes: string[] = ctx.query.attributes ? ctx.query.attributes.split(',').map((item: string) => item.trim()) : []
1528
1528
  if (attributes.length > 0 && !attributes.includes('id')) attributes.push('id') // id is mandatory
1529
1529
 
@@ -1659,7 +1659,7 @@ export class ScimGateway {
1659
1659
  try {
1660
1660
  if (!jsonBody) throw new Error('missing body')
1661
1661
  if (typeof jsonBody !== 'object' || jsonBody === null) throw new Error('body is not JSON')
1662
- jsonBody = utils.copyObj(jsonBody) // no changes to original
1662
+ jsonBody = structuredClone(jsonBody) // no changes to original
1663
1663
  } catch (err: any) {
1664
1664
  const [e, statusCode] = utilsScim.jsonErr(this.config.scimgateway.scim.version, pluginName, 500, err)
1665
1665
  ctx.response.status = statusCode
@@ -1978,7 +1978,7 @@ export class ScimGateway {
1978
1978
  if (!this.config.scimgateway.scim.groupMemberOfUser) {
1979
1979
  for (let i = 0; i < scimdata.groups.length; i++) {
1980
1980
  if (!scimdata.groups[i].value) continue
1981
- const obj: any = utils.copyObj(scimdata.groups[i])
1981
+ const obj: any = structuredClone(scimdata.groups[i])
1982
1982
  obj.value = decodeURIComponent(obj.value)
1983
1983
  groups.push(obj)
1984
1984
  }
@@ -1991,7 +1991,7 @@ export class ScimGateway {
1991
1991
  res = await replaceUsrGrp(ctx.routeObj.handle, baseEntity, id, scimdata, this.config.scimgateway.scim.usePutSoftSync, ctx.passThrough, undefined)
1992
1992
  } else {
1993
1993
  logger.debug(`${gwName} calling ${handle.modifyMethod}`, { baseEntity: ctx?.routeObj?.baseEntity })
1994
- finalScimdata = utils.copyObj(scimdata)
1994
+ finalScimdata = structuredClone(scimdata)
1995
1995
  res = await (this as any)[handle.modifyMethod](baseEntity, id, scimdata, ctx.passThrough)
1996
1996
  }
1997
1997
 
@@ -2106,7 +2106,7 @@ export class ScimGateway {
2106
2106
  let objGroups: any
2107
2107
  if (obj.groups) {
2108
2108
  if (!this.config.scimgateway.scim.groupMemberOfUser) {
2109
- objGroups = utils.copyObj(obj.groups)
2109
+ objGroups = structuredClone(obj.groups)
2110
2110
  delete obj.groups
2111
2111
  }
2112
2112
  }
@@ -2282,7 +2282,7 @@ export class ScimGateway {
2282
2282
  const postBulkHandler = async (ctx: Context) => {
2283
2283
  const baseEntity = ctx.routeObj.baseEntity
2284
2284
  logger.debug(`${gwName} [Bulk Operations]`, { baseEntity: ctx?.routeObj?.baseEntity })
2285
- const bulkBody: SCIMBulkRequest = utils.copyObj(ctx.request.body)
2285
+ const bulkBody: SCIMBulkRequest = structuredClone(ctx.request.body)
2286
2286
  try {
2287
2287
  if (!bulkBody) throw new Error('missing body')
2288
2288
  if (typeof bulkBody !== 'object') throw new Error('body is not JSON')
@@ -2758,9 +2758,11 @@ export class ScimGateway {
2758
2758
  if (!res.Resources[i].id) continue
2759
2759
  const el: any = {}
2760
2760
  el.value = res.Resources[i].id
2761
+ const type = (Array.isArray(res.Resources[i].members) && res.Resources[i].members[0]?.type === 'indirect') ? 'indirect' : 'direct'
2762
+ el.type = { value: type }
2761
2763
  if (res.Resources[i].displayName) el.display = res.Resources[i].displayName
2762
- if (isScimv2) el.type = 'direct'
2763
- else el.type = { value: 'direct' }
2764
+ if (isScimv2) el.type = type
2765
+ else el.type = { value: type }
2764
2766
  groups.push(el) // { "value": "Admins", "display": "Admins", "type": "direct"}
2765
2767
  }
2766
2768
  nextStartIndex = utilsScim.getNextStartIndex(res.totalResults, startIndex, res.Resources.length)
@@ -3629,7 +3631,7 @@ export class ScimGateway {
3629
3631
  logger.info(`${gwName} starting SCIM Stream subscribers...`)
3630
3632
  const sub: any = new stream.Subscriber(this, funcHandler)
3631
3633
  for (const baseEntity in this.config.scimgateway.stream.subscriber.entity) {
3632
- const cfgSub: any = utils.copyObj(this.config.scimgateway.stream.subscriber.entity[baseEntity])
3634
+ const cfgSub: any = structuredClone(this.config.scimgateway.stream.subscriber.entity[baseEntity])
3633
3635
  cfgSub.baseUrls = this.config.scimgateway.stream.baseUrls
3634
3636
  cfgSub.certificate = this.config.scimgateway.stream.certificate
3635
3637
  cfgSub.usePutSoftSync = this.config.scimgateway.scim.usePutSoftSync
@@ -3643,7 +3645,7 @@ export class ScimGateway {
3643
3645
  logger.info(`${gwName} starting SCIM Stream publishers...`)
3644
3646
  const pub: any = new stream.Publisher(this)
3645
3647
  for (const baseEntity in this.config.scimgateway.stream.publisher.entity) {
3646
- const cfgPub: any = utils.copyObj(this.config.scimgateway.stream.publisher.entity[baseEntity])
3648
+ const cfgPub: any = structuredClone(this.config.scimgateway.stream.publisher.entity[baseEntity])
3647
3649
  cfgPub.baseUrls = this.config.scimgateway.stream.baseUrls
3648
3650
  cfgPub.certificate = this.config.scimgateway.stream.certificate
3649
3651
  pub.add(baseEntity, cfgPub)
@@ -3795,6 +3797,7 @@ export class ScimGateway {
3795
3797
 
3796
3798
  /**
3797
3799
  * copyObj returns a copy of the object
3800
+ * Note: prefer using structuredClone(obj)
3798
3801
  * @param obj object to be copied
3799
3802
  * @returns copy of object
3800
3803
  **/
@@ -3812,6 +3815,17 @@ export class ScimGateway {
3812
3815
  return utils.extendObj(obj, src)
3813
3816
  }
3814
3817
 
3818
+ /**
3819
+ * getNextStartIndex returns the next SCIM pagination startIndex based on current result set
3820
+ * @param totalResults current totalResults
3821
+ * @param startIndex: current startIndex
3822
+ * @param count: current count
3823
+ * @returns next startIndex
3824
+ **/
3825
+ getNextStartIndex(totalResults: number, startIndex: number, itemsPerPage: number): number {
3826
+ return utilsScim.getNextStartIndex(totalResults, startIndex, itemsPerPage)
3827
+ }
3828
+
3815
3829
  /**
3816
3830
  * Lock for mutual exclusion
3817
3831
  * - const lock = new scimgateway.Lock()
package/lib/utils-scim.ts CHANGED
@@ -26,7 +26,7 @@ type SCIMBulkOperation = {
26
26
  */
27
27
  export function convertedScim(obj: any, multiValueTypes: string[]): any {
28
28
  let err: any = null
29
- const scimdata: any = utils.copyObj(obj)
29
+ const scimdata: any = structuredClone(obj)
30
30
  if (scimdata.schemas) delete scimdata.schemas
31
31
  const newMulti: Record<string, any> = {}
32
32
  if (!multiValueTypes) multiValueTypes = []
@@ -156,7 +156,7 @@ export function convertedScim(obj: any, multiValueTypes: string[]): any {
156
156
  export function convertedScim20(obj: any, multiValueTypes: string[]): any {
157
157
  if (!obj.Operations || !Array.isArray(obj.Operations)) return {}
158
158
  let scimdata: { [key: string]: any } = { meta: { attributes: [] } } // meta is used for deleted attributes
159
- const o: any = utils.copyObj(obj)
159
+ const o: any = structuredClone(obj)
160
160
  const arrPrimaryDone: any = []
161
161
  const primaryOrgType: any = {}
162
162
 
@@ -199,7 +199,7 @@ export function convertedScim20(obj: any, multiValueTypes: string[]): any {
199
199
  typeElement = arrMatches[3] // streetAddress
200
200
 
201
201
  if (type === 'primary' && !arrPrimaryDone.includes(arrMatches[1])) { // make sure primary is included
202
- const pObj: any = utils.copyObj(element)
202
+ const pObj: any = structuredClone(element)
203
203
  pObj.path = pObj.path.substring(0, pObj.path.lastIndexOf('.')) + '.primary'
204
204
  pObj.value = primaryValue
205
205
  o.Operations.push(pObj)
package/lib/utils.ts CHANGED
@@ -192,7 +192,8 @@ export const copyObj = (o: any): any => { // deep copy/clone faster than JSON.pa
192
192
  if (typeof v === 'object' && v !== null) {
193
193
  const objProp = Object.getPrototypeOf(v) // e.g. HttpsProxyAgent {}
194
194
  if (objProp !== null && objProp !== Object.getPrototypeOf({}) && objProp !== Object.getPrototypeOf([])) {
195
- output[key] = Object.assign(Object.create(v), v) // e.g. { HttpsProxyAgent {...} }
195
+ const proto = Object.getPrototypeOf(v)
196
+ output[key] = Object.assign(Object.create(proto), v) // e.g. { HttpsProxyAgent {...} }
196
197
  } else output[key] = copyObj(v)
197
198
  } else output[key] = v
198
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scimgateway",
3
- "version": "6.1.9",
3
+ "version": "6.1.10",
4
4
  "type": "module",
5
5
  "description": "Using SCIM protocol as a gateway for user provisioning to other endpoints",
6
6
  "author": "Jarle Elshaug <jarle.elshaug@gmail.com> (https://elshaug.xyz)",