isc-transforms-mcp 1.0.19 → 1.0.21

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.
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "sailpoint.isc.transforms.concat.schema.json",
4
4
  "title": "SailPoint ISC Transform Schema - concat",
5
- "description": "Strict schema derived from the SailPoint official Concatenation operation documentation. Concatenates an array of values (static strings or nested transform objects) into a single string output.",
5
+ "description": "Strict schema for the SailPoint ISC concat (Concatenation) transform. Joins an ordered array of values (static strings or nested transform outputs) into a single combined string. Key behaviors: (1) Items are joined in order with NO automatic separator — spaces, hyphens, or other delimiters must be added as explicit string entries in the array. (2) Each array entry can be a static string literal or a nested transform object whose output string is used. (3) A single-entry values array is valid JSON but semantically pointless — use the nested transform directly in that case.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -12,16 +12,18 @@
12
12
  ],
13
13
  "properties": {
14
14
  "type": {
15
- "const": "concat"
15
+ "const": "concat",
16
+ "description": "Transform operation type. Must be exactly 'concat'."
16
17
  },
17
18
  "name": {
18
19
  "type": "string",
19
- "minLength": 1
20
+ "minLength": 1,
21
+ "description": "Display name for this transform, shown in UI dropdowns and identity profile mappings."
20
22
  },
21
23
  "requiresPeriodicRefresh": {
22
24
  "type": "boolean",
23
25
  "default": false,
24
- "description": "Whether the transform logic should be reevaluated nightly as part of identity refresh. Default false."
26
+ "description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Default is false."
25
27
  },
26
28
  "attributes": {
27
29
  "type": "object",
@@ -29,15 +31,17 @@
29
31
  "required": [
30
32
  "values"
31
33
  ],
34
+ "description": "Required container for concat operation attributes. The only valid attribute is 'values'.",
32
35
  "properties": {
33
36
  "values": {
34
37
  "type": "array",
35
- "minItems": 2,
36
- "description": "Array of items to join. Each item must be a static string or a nested transform object.",
38
+ "minItems": 1,
39
+ "description": "Required. Ordered array of items to join into a single output string. Items are concatenated in sequence with no automatic separator. Rules: (1) Each entry must be a static string literal or a nested transform object {type, attributes} whose output is used. (2) To include spaces, hyphens, or any other separator, add them as explicit string entries between the value entries. (3) A single-entry array is valid but pointless — the nested transform can be used directly. Examples: [firstName, ' ', lastName] → 'Jane Doe'; [jobTitle, ' - ', jobCode] → 'Engineer - ENG001'.",
37
40
  "items": {
38
- "oneOf": [
41
+ "anyOf": [
39
42
  {
40
- "type": "string"
43
+ "type": "string",
44
+ "description": "Static string literal included as-is in the output (e.g., ' ', ' - ', ' (Contractor)')."
41
45
  },
42
46
  {
43
47
  "$ref": "#/$defs/NestedTransform"
@@ -51,7 +55,7 @@
51
55
  "$defs": {
52
56
  "NestedTransform": {
53
57
  "type": "object",
54
- "description": "Nested transform object used inside other transforms (docs examples often omit 'name' on nested transforms). Operation-specific validation should be applied by the referenced operation schema.",
58
+ "description": "A nested transform object used as one element in the values array. Its output string is inserted at the corresponding position in the concatenated result. The 'name' field is optional in nested transforms per SailPoint docs.",
55
59
  "additionalProperties": false,
56
60
  "required": [
57
61
  "type",
@@ -59,18 +63,22 @@
59
63
  ],
60
64
  "properties": {
61
65
  "id": {
62
- "type": "string"
66
+ "type": "string",
67
+ "description": "Optional ID when referencing an existing saved transform."
63
68
  },
64
69
  "name": {
65
70
  "type": "string",
66
- "minLength": 1
71
+ "minLength": 1,
72
+ "description": "Optional display name for the nested transform."
67
73
  },
68
74
  "type": {
69
75
  "type": "string",
70
- "minLength": 1
76
+ "minLength": 1,
77
+ "description": "The operation type of the nested transform (e.g., 'accountAttribute', 'identityAttribute', 'static', 'lower', 'trim')."
71
78
  },
72
79
  "requiresPeriodicRefresh": {
73
- "type": "boolean"
80
+ "type": "boolean",
81
+ "description": "Whether this nested transform re-evaluates during nightly refresh."
74
82
  },
75
83
  "attributes": {
76
84
  "type": "object",
@@ -82,8 +90,54 @@
82
90
  },
83
91
  "examples": [
84
92
  {
93
+ "name": "Full Name from HR Source",
94
+ "type": "concat",
95
+ "attributes": {
96
+ "values": [
97
+ {
98
+ "type": "accountAttribute",
99
+ "attributes": {
100
+ "sourceName": "HR Source",
101
+ "attributeName": "FirstName"
102
+ }
103
+ },
104
+ " ",
105
+ {
106
+ "type": "accountAttribute",
107
+ "attributes": {
108
+ "sourceName": "HR Source",
109
+ "attributeName": "LastName"
110
+ }
111
+ }
112
+ ]
113
+ }
114
+ },
115
+ {
116
+ "name": "Job Title with Code",
117
+ "type": "concat",
118
+ "attributes": {
119
+ "values": [
120
+ {
121
+ "type": "accountAttribute",
122
+ "attributes": {
123
+ "sourceName": "HR Source",
124
+ "attributeName": "JobTitle"
125
+ }
126
+ },
127
+ " - ",
128
+ {
129
+ "type": "accountAttribute",
130
+ "attributes": {
131
+ "sourceName": "HR Source",
132
+ "attributeName": "JobCode"
133
+ }
134
+ }
135
+ ]
136
+ }
137
+ },
138
+ {
139
+ "name": "Contractor Display Name",
85
140
  "type": "concat",
86
- "name": "Test Concat Transform",
87
141
  "attributes": {
88
142
  "values": [
89
143
  {
@@ -104,6 +158,38 @@
104
158
  " (Contractor)"
105
159
  ]
106
160
  }
161
+ },
162
+ {
163
+ "name": "Lowercase Email from Identity Attributes",
164
+ "type": "concat",
165
+ "attributes": {
166
+ "values": [
167
+ {
168
+ "type": "lower",
169
+ "attributes": {
170
+ "input": {
171
+ "type": "identityAttribute",
172
+ "attributes": {
173
+ "name": "firstname"
174
+ }
175
+ }
176
+ }
177
+ },
178
+ ".",
179
+ {
180
+ "type": "lower",
181
+ "attributes": {
182
+ "input": {
183
+ "type": "identityAttribute",
184
+ "attributes": {
185
+ "name": "lastname"
186
+ }
187
+ }
188
+ }
189
+ },
190
+ "@example.com"
191
+ ]
192
+ }
107
193
  }
108
194
  ]
109
195
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "sailpoint.isc.transforms.usernameGenerator.schema.json",
4
4
  "title": "SailPoint ISC Transform Schema - usernameGenerator",
5
- "description": "Strict schema derived from SailPoint official Username Generator operation documentation. This transform is intended for use within an account create profile; doc examples show the transform object embedded under a create profile attribute entry's 'transform' field and do not include a transform-level 'name'.",
5
+ "description": "Strict schema for the SailPoint ISC usernameGenerator transform. Generates a unique username by evaluating an ordered list of format string patterns until a unique value is found. Patterns use dollar-sign token notation ($varName or ${varName}) referencing dynamic variables defined as additional attribute keys. IMPORTANT: (1) The pattern containing '${uniqueCounter}' MUST be the last entry patterns after it are never evaluated. (2) If all patterns are exhausted without finding a unique value, an IllegalStateException is thrown. (3) This transform is designed for account create profiles, not standalone identity profile mappings. The 'name' field is optional when used inside a create profile.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -10,20 +10,26 @@
10
10
  "attributes"
11
11
  ],
12
12
  "properties": {
13
+ "type": {
14
+ "const": "usernameGenerator",
15
+ "description": "Transform operation type. Must be exactly 'usernameGenerator'."
16
+ },
13
17
  "name": {
14
18
  "type": "string",
15
19
  "minLength": 1,
16
- "description": "Optional. In general transform syntax, only the root-level transform has a name; nested transforms omit it."
20
+ "description": "Optional display name. Typically omitted when the transform is embedded within an account create profile attribute definition."
17
21
  },
18
- "type": {
19
- "const": "usernameGenerator"
22
+ "requiresPeriodicRefresh": {
23
+ "type": "boolean",
24
+ "default": false,
25
+ "description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Default is false."
20
26
  },
21
27
  "attributes": {
22
28
  "type": "object",
23
- "description": "Username generator configuration plus optional dynamic variables (per docs).",
24
29
  "required": [
25
30
  "patterns"
26
31
  ],
32
+ "description": "Required container. Must include 'patterns'. Optional cloud control fields: cloudMaxSize, cloudMaxUniqueChecks, cloudRequired, sourceCheck. Any additional keys are dynamic variable definitions — the key name becomes a $token usable in patterns, and its value is a static string or a nested transform that supplies the substitution value.",
27
33
  "properties": {
28
34
  "patterns": {
29
35
  "type": "array",
@@ -32,60 +38,90 @@
32
38
  "type": "string",
33
39
  "minLength": 1
34
40
  },
35
- "description": "A JSON array of patterns for the generator to evaluate for uniqueness, in sequential order. Docs allow using 'uniqueCounter' as a reserved variable (shown as ${uniqueCounter} in examples). Docs also state that if a pattern contains uniqueCounter, it must be the last pattern."
41
+ "description": "Required. Ordered list of format strings evaluated sequentially until a unique value is found. Each string may contain $varName or ${varName} tokens referencing dynamic variables defined as sibling attribute keys. Rules: (1) The pattern containing '${uniqueCounter}' MUST be the last entry it auto-increments on each collision and patterns after it are never reached. (2) If no pattern contains '${uniqueCounter}' and all patterns produce values that already exist, the generator throws IllegalStateException. (3) The generator validates uniqueness only for the attribute marked as 'accountID' in the account schema.",
42
+ "examples": [
43
+ ["$fn$ln", "$fn.$ln${uniqueCounter}"],
44
+ ["$fn$ln", "$fi$ln", "$fn$mi$ln${uniqueCounter}"]
45
+ ]
36
46
  },
37
47
  "sourceCheck": {
38
48
  "type": "boolean",
39
49
  "default": false,
40
- "description": "Whether the generator checks only the ISC database or queries the target system directly. true = check target system directly (only if the system supports getObject); false = check only the ISC database (default)."
50
+ "description": "Optional. Determines where uniqueness is validated. true = query the target system directly (only if the source supports the getObject operation — falls back to ISC database for sources that don't). false = check only the ISC database (default). The generator always validates the attribute marked as 'accountID' in the account schema."
51
+ },
52
+ "cloudMaxSize": {
53
+ "type": "integer",
54
+ "minimum": 1,
55
+ "description": "Optional. Maximum character length for generated usernames. Generated values exceeding this length are automatically truncated to this size before the uniqueness check."
56
+ },
57
+ "cloudMaxUniqueChecks": {
58
+ "type": "integer",
59
+ "minimum": 1,
60
+ "maximum": 50,
61
+ "description": "Optional. Maximum number of uniqueness check iterations before the generator throws IllegalStateException. Must be between 1 and 50 (the documented maximum). Applies to the ${uniqueCounter} expansion loop — when the counter exceeds this limit without finding a unique value, the transform fails."
62
+ },
63
+ "cloudRequired": {
64
+ "type": "boolean",
65
+ "description": "Internal flag. Must remain true. Changing this value may cause unexpected behavior."
41
66
  }
42
67
  },
43
68
  "additionalProperties": {
44
- "description": "Dynamic variable value used in patterns. Docs describe these as transforms.",
45
- "$ref": "#/$defs/NestedTransform"
69
+ "description": "Dynamic variable definition. The key name becomes a $token referenced in patterns (e.g., key 'fn' is used as '$fn' or '${fn}' in a pattern). The value can be a static string or a nested transform object whose output supplies the substitution value.",
70
+ "anyOf": [
71
+ {
72
+ "type": "string",
73
+ "description": "Static string value for this variable token."
74
+ },
75
+ {
76
+ "$ref": "#/$defs/NestedTransform"
77
+ }
78
+ ]
46
79
  }
47
80
  }
48
81
  },
49
82
  "$defs": {
50
83
  "NestedTransform": {
51
84
  "type": "object",
52
- "description": "Nested transform object used as a dynamic variable (e.g., fn, ln, fi, mi). Docs examples for nested transforms typically omit 'name'.",
85
+ "description": "A nested transform object used as a dynamic variable value. Its output string is substituted for the $token in patterns. Common types: identityAttribute (to read a name attribute), substring (to extract initials), accountAttribute. The 'name' field is optional in nested transforms per SailPoint docs.",
53
86
  "additionalProperties": false,
54
87
  "required": [
55
- "type"
88
+ "type",
89
+ "attributes"
56
90
  ],
57
91
  "properties": {
58
92
  "id": {
59
- "type": "string"
93
+ "type": "string",
94
+ "description": "Optional ID when referencing an existing saved transform."
60
95
  },
61
96
  "name": {
62
97
  "type": "string",
63
- "minLength": 1
98
+ "minLength": 1,
99
+ "description": "Optional display name for the nested transform."
64
100
  },
65
101
  "type": {
66
102
  "type": "string",
67
- "minLength": 1
103
+ "minLength": 1,
104
+ "description": "The operation type of the nested transform (e.g., 'identityAttribute', 'substring', 'accountAttribute', 'lower')."
68
105
  },
69
106
  "requiresPeriodicRefresh": {
70
- "type": "boolean"
107
+ "type": "boolean",
108
+ "description": "Whether this nested transform re-evaluates during nightly refresh."
71
109
  },
72
110
  "attributes": {
73
- "type": [
74
- "object",
75
- "null"
76
- ],
111
+ "type": "object",
77
112
  "additionalProperties": true,
78
- "description": "Operation-specific attributes for the nested transform (may be omitted for operations without attributes)."
113
+ "description": "Operation-specific attributes for the nested transform."
79
114
  }
80
115
  }
81
116
  }
82
117
  },
83
118
  "examples": [
84
119
  {
120
+ "name": "Basic Username Generator",
85
121
  "type": "usernameGenerator",
86
122
  "attributes": {
87
- "sourceCheck": true,
88
123
  "patterns": [
124
+ "$fn$ln",
89
125
  "$fn.$ln${uniqueCounter}"
90
126
  ],
91
127
  "fn": {
@@ -101,6 +137,91 @@
101
137
  }
102
138
  }
103
139
  }
140
+ },
141
+ {
142
+ "name": "Username with Initial and Cloud Controls",
143
+ "type": "usernameGenerator",
144
+ "attributes": {
145
+ "sourceCheck": true,
146
+ "cloudMaxSize": 20,
147
+ "cloudMaxUniqueChecks": 50,
148
+ "patterns": [
149
+ "$fn$ln",
150
+ "$fi$ln",
151
+ "$fn$mi$ln${uniqueCounter}"
152
+ ],
153
+ "fn": {
154
+ "type": "identityAttribute",
155
+ "attributes": {
156
+ "name": "firstname"
157
+ }
158
+ },
159
+ "ln": {
160
+ "type": "identityAttribute",
161
+ "attributes": {
162
+ "name": "lastname"
163
+ }
164
+ },
165
+ "fi": {
166
+ "type": "substring",
167
+ "attributes": {
168
+ "begin": 0,
169
+ "end": 1,
170
+ "input": {
171
+ "type": "identityAttribute",
172
+ "attributes": {
173
+ "name": "firstname"
174
+ }
175
+ }
176
+ }
177
+ },
178
+ "mi": {
179
+ "type": "substring",
180
+ "attributes": {
181
+ "begin": 0,
182
+ "end": 1,
183
+ "input": {
184
+ "type": "identityAttribute",
185
+ "attributes": {
186
+ "name": "middleName"
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+ },
193
+ {
194
+ "name": "Lowercase Email-Style Username",
195
+ "type": "usernameGenerator",
196
+ "attributes": {
197
+ "cloudMaxSize": 64,
198
+ "patterns": [
199
+ "$fn.$ln",
200
+ "$fn.$ln${uniqueCounter}"
201
+ ],
202
+ "fn": {
203
+ "type": "lower",
204
+ "attributes": {
205
+ "input": {
206
+ "type": "identityAttribute",
207
+ "attributes": {
208
+ "name": "firstname"
209
+ }
210
+ }
211
+ }
212
+ },
213
+ "ln": {
214
+ "type": "lower",
215
+ "attributes": {
216
+ "input": {
217
+ "type": "identityAttribute",
218
+ "attributes": {
219
+ "name": "lastname"
220
+ }
221
+ }
222
+ }
223
+ }
224
+ }
104
225
  }
105
226
  ]
106
227
  }
@@ -18,7 +18,7 @@ const ALLOWED_ATTRS = {
18
18
  ]),
19
19
  base64Decode: new Set(["input"]),
20
20
  base64Encode: new Set(["input"]),
21
- concat: new Set(["values", "input"]),
21
+ concat: new Set(["values"]),
22
22
  conditional: "open", // dynamic variable keys allowed per docs
23
23
  dateCompare: new Set(["firstDate", "secondDate", "operator", "positiveCondition", "negativeCondition"]),
24
24
  dateFormat: new Set(["input", "inputFormat", "outputFormat"]),
@@ -49,7 +49,7 @@ const ALLOWED_ATTRS = {
49
49
  substring: new Set(["begin", "beginOffset", "end", "endOffset", "input"]),
50
50
  trim: new Set(["input"]),
51
51
  upper: new Set(["input"]),
52
- usernameGenerator: new Set(["patterns", "sourceCheck"]),
52
+ usernameGenerator: "open", // patterns + sourceCheck + cloudMaxSize/Checks/Required + dynamic variable keys (fn, ln, etc.)
53
53
  uuid: new Set([]),
54
54
  // Rule-backed ops (normalized to type=rule, but linted by operation key)
55
55
  generateRandomString: new Set(["name", "operation", "length", "includeNumbers", "includeSpecialChars"]),
@@ -876,30 +876,122 @@ function lintDateFormat(attrs) {
876
876
  return msgs;
877
877
  }
878
878
  // ---------------------------------------------------------------------------
879
- // 12. usernameGenerator — patterns array
879
+ // 12. usernameGenerator — patterns, tokens, cloud* fields, dynamic variable cross-check
880
+ // Docs: https://developer.sailpoint.com/docs/extensibility/transforms/operations/username-generator
880
881
  // ---------------------------------------------------------------------------
882
+ // Known non-variable attribute keys for usernameGenerator
883
+ const USERNAME_GENERATOR_KNOWN_KEYS = new Set([
884
+ "patterns", "sourceCheck", "cloudMaxSize", "cloudMaxUniqueChecks", "cloudRequired",
885
+ ]);
881
886
  function lintUsernameGenerator(attrs) {
882
887
  const msgs = [];
883
888
  const patterns = attrs?.patterns;
889
+ // --- 1. patterns: required non-empty array of format strings ---
884
890
  if (patterns !== undefined) {
885
891
  if (!Array.isArray(patterns) || patterns.length === 0) {
886
- push(msgs, "error", "patterns must be a non-empty array of strings.", "attributes.patterns");
892
+ push(msgs, "error", "patterns must be a non-empty array of format strings (e.g., ['$fn$ln', '$fn.$ln${uniqueCounter}']).", "attributes.patterns");
887
893
  }
888
894
  else {
889
- const bad = patterns.findIndex((p) => typeof p !== "string" || p.trim().length === 0);
890
- if (bad >= 0) {
891
- push(msgs, "error", `Each pattern must be a non-empty string. Invalid at index [${bad}].`, `attributes.patterns[${bad}]`);
895
+ // 1a. Each entry must be a non-empty string
896
+ patterns.forEach((p, idx) => {
897
+ if (typeof p !== "string" || p.trim().length === 0) {
898
+ push(msgs, "error", `patterns[${idx}] must be a non-empty format string. ` +
899
+ "Use $varName or ${varName} tokens for variable substitution.", `attributes.patterns[${idx}]`);
900
+ }
901
+ });
902
+ // 1b. ${uniqueCounter} must be last — patterns after it are never evaluated
903
+ const ucIdx = patterns.findIndex((p) => typeof p === "string" && p.includes("uniqueCounter"));
904
+ if (ucIdx >= 0 && ucIdx !== patterns.length - 1) {
905
+ push(msgs, "error", "The pattern containing '${uniqueCounter}' must be the last entry in the patterns array. " +
906
+ "The generator stops after exhausting the uniqueCounter pattern — any patterns listed after it are never evaluated.", "attributes.patterns");
907
+ }
908
+ // 1c. No uniqueCounter at all — exhausting all patterns throws IllegalStateException
909
+ if (ucIdx === -1) {
910
+ push(msgs, "warn", "No pattern contains '${uniqueCounter}'. If all patterns generate values that already exist, " +
911
+ "the generator throws an IllegalStateException. " +
912
+ "Add a final fallback pattern with '${uniqueCounter}' (e.g., '$fn$ln${uniqueCounter}') to handle conflicts.", "attributes.patterns");
913
+ }
914
+ // 1d. Token syntax info
915
+ push(msgs, "info", "Pattern tokens use dollar-sign notation: $varName (simple) or ${varName} (formal). " +
916
+ "Each variable name (e.g., $fn, $ln, $fi) must be defined as an additional key in the attributes object, " +
917
+ "set to a static string or a nested transform that supplies the value. " +
918
+ "The reserved token ${uniqueCounter} auto-increments when a generated value already exists.", "attributes.patterns");
919
+ // 1e. Cross-check: tokens used in patterns must have a matching variable defined in attributes
920
+ const RESERVED_TOKENS = new Set(["uniqueCounter"]);
921
+ const definedVars = new Set(Object.keys(attrs ?? {}).filter((k) => !USERNAME_GENERATOR_KNOWN_KEYS.has(k)));
922
+ const referencedTokens = new Set();
923
+ for (const p of patterns) {
924
+ if (typeof p !== "string")
925
+ continue;
926
+ const re = /\$\{?([A-Za-z_][A-Za-z0-9_]*)\}?/g;
927
+ let m;
928
+ while ((m = re.exec(p)) !== null) {
929
+ referencedTokens.add(m[1]);
930
+ }
892
931
  }
893
- // uniqueCounter must be last per docs
894
- const idx = patterns.findIndex((p) => typeof p === "string" && p.includes("uniqueCounter"));
895
- if (idx >= 0 && idx !== patterns.length - 1) {
896
- push(msgs, "warn", "Pattern containing 'uniqueCounter' should be last in the patterns array.", "attributes.patterns");
932
+ for (const tok of referencedTokens) {
933
+ if (RESERVED_TOKENS.has(tok))
934
+ continue;
935
+ if (!definedVars.has(tok)) {
936
+ push(msgs, "warn", `Pattern references '$${tok}' but no variable '${tok}' is defined in attributes. ` +
937
+ `Add a '${tok}' key to attributes as a static string or a nested transform (e.g., identityAttribute).`, `attributes.${tok}`);
938
+ }
939
+ }
940
+ // 1f. Cross-check: variables defined in attributes but never referenced in any pattern
941
+ for (const varName of definedVars) {
942
+ if (!referencedTokens.has(varName)) {
943
+ push(msgs, "warn", `Variable '${varName}' is defined in attributes but not referenced as $${varName} in any pattern. ` +
944
+ "Remove it to keep the transform clean, or check for a typo in the pattern.", `attributes.${varName}`);
945
+ }
897
946
  }
898
947
  }
899
948
  }
900
- if (attrs?.sourceCheck !== undefined && typeof attrs.sourceCheck !== "boolean") {
901
- push(msgs, "error", "sourceCheck must be a boolean.", "attributes.sourceCheck");
949
+ // --- 2. sourceCheck ---
950
+ if (attrs?.sourceCheck !== undefined) {
951
+ if (typeof attrs.sourceCheck !== "boolean") {
952
+ push(msgs, "error", "sourceCheck must be a boolean. " +
953
+ "true = check the target system directly (only if the source supports getObject). " +
954
+ "false = check only the ISC database (default).", "attributes.sourceCheck");
955
+ }
956
+ else if (attrs.sourceCheck === true) {
957
+ push(msgs, "info", "sourceCheck: true validates uniqueness against the target system directly. " +
958
+ "This only works for sources that support the getObject operation — " +
959
+ "for sources that don't, the check automatically falls back to the ISC database.", "attributes.sourceCheck");
960
+ }
961
+ }
962
+ // --- 3. cloudMaxSize: positive integer — truncates generated values exceeding this length ---
963
+ if (attrs?.cloudMaxSize !== undefined) {
964
+ const n = Number(attrs.cloudMaxSize);
965
+ if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
966
+ push(msgs, "error", "cloudMaxSize must be a positive integer. Generated usernames longer than this value will be truncated to this length.", "attributes.cloudMaxSize");
967
+ }
968
+ else {
969
+ push(msgs, "info", `cloudMaxSize: ${n} — generated values exceeding ${n} characters will be automatically truncated.`, "attributes.cloudMaxSize");
970
+ }
971
+ }
972
+ // --- 4. cloudMaxUniqueChecks: positive integer, maximum 50 ---
973
+ if (attrs?.cloudMaxUniqueChecks !== undefined) {
974
+ const n = Number(attrs.cloudMaxUniqueChecks);
975
+ if (!Number.isFinite(n) || n <= 0 || !Number.isInteger(n)) {
976
+ push(msgs, "error", "cloudMaxUniqueChecks must be a positive integer (maximum: 50). " +
977
+ "The generator throws IllegalStateException when this number of uniqueness iterations is exceeded.", "attributes.cloudMaxUniqueChecks");
978
+ }
979
+ else if (n > 50) {
980
+ push(msgs, "error", `cloudMaxUniqueChecks ${n} exceeds the documented maximum of 50. ` +
981
+ "Values above 50 cause an error at runtime. Set to 50 or less.", "attributes.cloudMaxUniqueChecks");
982
+ }
983
+ else {
984
+ push(msgs, "info", `cloudMaxUniqueChecks: ${n} — generator throws IllegalStateException after ${n} failed uniqueness iterations.`, "attributes.cloudMaxUniqueChecks");
985
+ }
902
986
  }
987
+ // --- 5. cloudRequired: internal flag — must remain true ---
988
+ if (attrs?.cloudRequired !== undefined && attrs.cloudRequired !== true) {
989
+ push(msgs, "warn", "cloudRequired is an internal flag that must remain true. " +
990
+ "Setting it to any other value may cause unexpected behavior.", "attributes.cloudRequired");
991
+ }
992
+ // --- 6. Standalone use limitation ---
993
+ push(msgs, "info", "usernameGenerator is designed specifically for account create profiles. " +
994
+ "It should be placed within a create profile attribute definition — not used as a standalone identity profile attribute transform.", "type");
903
995
  return msgs;
904
996
  }
905
997
  // ---------------------------------------------------------------------------
@@ -1320,6 +1412,63 @@ function lintRfc5646(attrs) {
1320
1412
  return msgs;
1321
1413
  }
1322
1414
  // ---------------------------------------------------------------------------
1415
+ // lintConcat — concat (Concatenation)
1416
+ // Docs: https://developer.sailpoint.com/docs/extensibility/transforms/operations/concatenation
1417
+ // Joins an ordered array of strings / nested-transform outputs into one string.
1418
+ // No automatic separators — spaces, hyphens, etc. must be explicit array entries.
1419
+ // ---------------------------------------------------------------------------
1420
+ function lintConcat(attrs) {
1421
+ const msgs = [];
1422
+ const values = attrs?.values;
1423
+ // --- 1. values is the only attribute and is required ---
1424
+ if (values === undefined || values === null) {
1425
+ push(msgs, "error", "values is required for concat. Provide an ordered array of strings and/or nested transform objects " +
1426
+ "whose outputs will be joined into a single string.", "attributes.values");
1427
+ return msgs;
1428
+ }
1429
+ if (!Array.isArray(values)) {
1430
+ push(msgs, "error", "values must be an array of strings and/or nested transform objects.", "attributes.values");
1431
+ return msgs;
1432
+ }
1433
+ if (values.length === 0) {
1434
+ push(msgs, "error", "values array must not be empty. Provide at least one string or nested transform.", "attributes.values");
1435
+ return msgs;
1436
+ }
1437
+ // --- 2. Warn if only a single entry — concat is pointless with one value ---
1438
+ if (values.length === 1) {
1439
+ push(msgs, "warn", "values array has only one entry. concat is designed to join multiple values — " +
1440
+ "consider using the nested transform directly instead of wrapping it in a concat.", "attributes.values");
1441
+ }
1442
+ // --- 3. Validate each item: must be string or a nested transform object ---
1443
+ values.forEach((item, idx) => {
1444
+ if (item === null || item === undefined) {
1445
+ push(msgs, "warn", `values[${idx}] is null/undefined — this will produce the string "null"/"undefined" in the output. ` +
1446
+ "Remove it or replace with a static string or a nested transform.", `attributes.values[${idx}]`);
1447
+ }
1448
+ else if (typeof item === "string") {
1449
+ // Valid — static strings (including spaces, separators, literals) are expected
1450
+ }
1451
+ else if (isPlainObject(item)) {
1452
+ if (typeof item.type !== "string" || item.type.trim() === "") {
1453
+ push(msgs, "error", `values[${idx}] is an object but is missing a 'type' field — it does not look like a valid nested transform. ` +
1454
+ "Add a 'type' (e.g., 'accountAttribute', 'identityAttribute', 'static').", `attributes.values[${idx}]`);
1455
+ }
1456
+ }
1457
+ else {
1458
+ push(msgs, "error", `values[${idx}] must be a string or a nested transform object {type, attributes}. ` +
1459
+ `Got: ${typeof item}.`, `attributes.values[${idx}]`);
1460
+ }
1461
+ });
1462
+ // --- 4. Separator hint: inform if no explicit spacing string found between transform objects ---
1463
+ const allTransforms = values.every((v) => isPlainObject(v));
1464
+ if (allTransforms && values.length > 1) {
1465
+ push(msgs, "info", "concat does not insert any separator between values automatically. " +
1466
+ "If the output needs spaces, hyphens, or other delimiters, add them as explicit string entries " +
1467
+ "in the values array (e.g., [firstName, \" \", lastName]).", "attributes.values");
1468
+ }
1469
+ return msgs;
1470
+ }
1471
+ // ---------------------------------------------------------------------------
1323
1472
  // Main lintTransform export
1324
1473
  // ---------------------------------------------------------------------------
1325
1474
  export function lintTransform(input) {
@@ -1410,6 +1559,8 @@ export function lintTransform(input) {
1410
1559
  messages.push(...lintRandom(requestedType, attrs));
1411
1560
  if (requestedType === "rfc5646")
1412
1561
  messages.push(...lintRfc5646(attrs));
1562
+ if (requestedType === "concat")
1563
+ messages.push(...lintConcat(attrs));
1413
1564
  // --- Recursive nested transform lint ---
1414
1565
  // Recursively lint every nested transform found inside attributes.
1415
1566
  // We start from normalized.attributes (not the root) to avoid double-linting root.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isc-transforms-mcp",
3
- "version": "1.0.19",
3
+ "version": "1.0.21",
4
4
  "type": "module",
5
5
  "description": "MCP server for SailPoint Identity Security Cloud (ISC) Transform authoring — scaffold, strict lint, catalog, and safe upsert to live tenants.",
6
6
  "author": {