isc-transforms-mcp 1.0.6 → 1.0.8

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.accountAttribute.schema.json",
4
4
  "title": "SailPoint ISC Transform Schema - accountAttribute",
5
- "description": "Strict schema derived from the SailPoint official Account Attribute operation documentation.",
5
+ "description": "Strict schema derived from the SailPoint official Account Attribute operation documentation. Retrieves a specific attribute value from an account associated with a source on an identity.",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": [
@@ -12,15 +12,18 @@
12
12
  ],
13
13
  "properties": {
14
14
  "type": {
15
- "const": "accountAttribute"
15
+ "const": "accountAttribute",
16
+ "description": "Transform operation type. Must be exactly 'accountAttribute'."
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
- "default": false
25
+ "default": false,
26
+ "description": "If true, re-evaluates this transform during the nightly identity refresh cycle. Set to true when the source account data changes frequently. Default is false."
24
27
  },
25
28
  "attributes": {
26
29
  "type": "object",
@@ -32,113 +35,95 @@
32
35
  "sourceName": {
33
36
  "type": "string",
34
37
  "minLength": 1,
35
- "description": "Source display name to search (e.g., 'Active Directory'). If the display name changes, this reference must be updated."
38
+ "description": "Source display name to search (e.g., 'Active Directory'). WARNING: references the mutable display name — if the source is renamed, this value must be updated. For stability, prefer applicationName. Mutually exclusive with applicationId and applicationName."
36
39
  },
37
40
  "applicationId": {
38
41
  "type": "string",
39
42
  "minLength": 1,
40
- "description": "Alternative to sourceName: source external GUID."
43
+ "description": "External GUID of the source. Stable alternative to sourceName that does not break if the source is renamed. Mutually exclusive with sourceName and applicationName."
41
44
  },
42
45
  "applicationName": {
43
46
  "type": "string",
44
47
  "minLength": 1,
45
- "description": "Alternative to sourceName: source immutable name."
48
+ "description": "Immutable internal source name. Most stable source reference — does not change when the source display name changes. Mutually exclusive with sourceName and applicationId."
46
49
  },
47
50
  "attributeName": {
48
51
  "type": "string",
49
52
  "minLength": 1,
50
- "description": "Account attribute to return (must match the account attribute name visible in the UI or source schema)."
53
+ "description": "Name of the account attribute to retrieve. Must exactly match the attribute name visible in the source schema / account attribute list in the ISC UI."
51
54
  },
52
55
  "accountSortAttribute": {
53
56
  "type": "string",
54
57
  "minLength": 1,
55
58
  "default": "created",
56
- "description": "Attribute name to sort returned accounts by when multiple accounts exist. Default 'created' (ascending; oldest wins)."
59
+ "description": "Account schema attribute used to sort multiple accounts before selecting one. Default is 'created' (ascending oldest account wins). Must be a valid account schema attribute name."
57
60
  },
58
61
  "accountSortDescending": {
59
62
  "type": "boolean",
60
63
  "default": false,
61
- "description": "Sort order when multiple accounts exist. Default false (ascending)."
64
+ "description": "Controls sort order when multiple accounts exist. false = ascending (oldest first, default). true = descending (newest first)."
62
65
  },
63
66
  "accountReturnFirstLink": {
64
67
  "type": "boolean",
65
68
  "default": false,
66
- "description": "If true, return the value from the first account in the sorted list even if null; if false, return the first non-null value. Default false."
69
+ "description": "Controls null handling when multiple accounts exist. false = skip accounts with null values and return the first non-null (default). true = return the first sorted account's value even if it is null."
67
70
  },
68
71
  "accountFilter": {
69
72
  "type": "string",
70
73
  "minLength": 1,
71
- "description": "Database filter (sailpoint.object.Filter) ANDed with the default source+identity filter. Only searchable fields are supported (nativeIdentity, displayName, entitlements)."
74
+ "description": "Database-level filter using sailpoint.object.Filter syntax. Applied before account retrieval. IMPORTANT: only the following fields are searchable at database level — nativeIdentity, displayName, entitlements. For other attributes (e.g. custom fields, status) use accountPropertyFilter. Example: !(nativeIdentity.startsWith(\"*DELETED*\"))"
72
75
  },
73
76
  "accountPropertyFilter": {
74
77
  "type": "string",
75
78
  "minLength": 1,
76
- "description": "In-memory filter (sailpoint.object.Filter) applied after retrieval. All account attributes can be referenced."
79
+ "description": "In-memory filter using sailpoint.object.Filter syntax. Applied after account retrieval. All account attributes are available for filtering (not limited to searchable fields). Example: (status != \"terminated\")"
77
80
  },
78
81
  "input": {
79
- "type": "object",
80
- "description": "Explicit input object. If omitted, the transform uses implicit input configured via UI/source+attribute mapping.",
81
- "additionalProperties": true
82
+ "description": "Explicit input data to override the implicit source+attribute mapping. Can be a nested transform object or a static string value.",
83
+ "oneOf": [
84
+ {
85
+ "type": "string"
86
+ },
87
+ {
88
+ "type": "object",
89
+ "required": ["type"],
90
+ "properties": {
91
+ "type": { "type": "string", "minLength": 1 },
92
+ "attributes": { "type": "object" }
93
+ },
94
+ "additionalProperties": true
95
+ }
96
+ ]
82
97
  }
83
98
  },
84
99
  "allOf": [
85
100
  {
86
- "description": "Exactly one source reference must be provided: sourceName OR applicationId OR applicationName.",
101
+ "description": "Exactly one source reference must be provided: sourceName OR applicationId OR applicationName. Using more than one is not allowed.",
87
102
  "oneOf": [
88
103
  {
89
- "required": [
90
- "sourceName"
91
- ],
104
+ "required": ["sourceName"],
92
105
  "not": {
93
106
  "anyOf": [
94
- {
95
- "required": [
96
- "applicationId"
97
- ]
98
- },
99
- {
100
- "required": [
101
- "applicationName"
102
- ]
103
- }
107
+ { "required": ["applicationId"] },
108
+ { "required": ["applicationName"] }
104
109
  ]
105
110
  }
106
111
  },
107
112
  {
108
- "required": [
109
- "applicationId"
110
- ],
113
+ "required": ["applicationId"],
111
114
  "not": {
112
115
  "anyOf": [
113
- {
114
- "required": [
115
- "sourceName"
116
- ]
117
- },
118
- {
119
- "required": [
120
- "applicationName"
121
- ]
122
- }
116
+ { "required": ["sourceName"] },
117
+ { "required": ["applicationName"] }
123
118
  ]
124
119
  }
125
120
  },
126
121
  {
127
- "required": [
128
- "applicationName"
129
- ],
122
+ "required": ["applicationName"],
130
123
  "not": {
131
124
  "anyOf": [
132
- {
133
- "required": [
134
- "sourceName"
135
- ]
136
- },
137
- {
138
- "required": [
139
- "applicationId"
140
- ]
141
- }
125
+ { "required": ["sourceName"] },
126
+ { "required": ["applicationId"] }
142
127
  ]
143
128
  }
144
129
  }
@@ -149,16 +134,34 @@
149
134
  },
150
135
  "examples": [
151
136
  {
137
+ "name": "Get hire date from HR source",
152
138
  "type": "accountAttribute",
153
- "name": "Account Attribute Transform",
154
139
  "attributes": {
155
140
  "sourceName": "Corporate HR",
156
- "attributeName": "HIREDATE",
141
+ "attributeName": "HIREDATE"
142
+ }
143
+ },
144
+ {
145
+ "name": "Get status from newest non-service account",
146
+ "type": "accountAttribute",
147
+ "requiresPeriodicRefresh": true,
148
+ "attributes": {
149
+ "applicationName": "corp-active-directory",
150
+ "attributeName": "employeeStatus",
157
151
  "accountSortAttribute": "created",
158
152
  "accountSortDescending": true,
159
- "accountReturnFirstLink": true,
153
+ "accountReturnFirstLink": false,
154
+ "accountFilter": "!(nativeIdentity.startsWith(\"*DELETED*\"))",
160
155
  "accountPropertyFilter": "(WORKER_STATUS__c == \"active\")"
161
156
  }
157
+ },
158
+ {
159
+ "name": "Get department using immutable source ID",
160
+ "type": "accountAttribute",
161
+ "attributes": {
162
+ "applicationId": "2c91808b7cda982781e0a6b92e0f01a9",
163
+ "attributeName": "department"
164
+ }
162
165
  }
163
166
  ]
164
167
  }
@@ -28,14 +28,34 @@
28
28
  "additionalProperties": false,
29
29
  "properties": {
30
30
  "inputFormat": {
31
- "type": "string",
32
31
  "default": "ISO8601",
33
- "description": "Format of the incoming date. Java SimpleDateFormat pattern or built-in named format."
32
+ "description": "Format of the incoming date. Must be one of the 5 named formats or a Java SimpleDateFormat pattern.",
33
+ "anyOf": [
34
+ {
35
+ "enum": ["ISO8601", "LDAP", "PEOPLE_SOFT", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32"],
36
+ "description": "Built-in named format."
37
+ },
38
+ {
39
+ "type": "string",
40
+ "not": { "pattern": "^[A-Z][A-Z0-9_]+$" },
41
+ "description": "Java SimpleDateFormat pattern (e.g. dd-MM-yyyy, yyyy-MM-dd'T'HH:mm:ssZ)."
42
+ }
43
+ ]
34
44
  },
35
45
  "outputFormat": {
36
- "type": "string",
37
46
  "default": "ISO8601",
38
- "description": "Desired output format. Java SimpleDateFormat pattern or built-in named format."
47
+ "description": "Desired output format. Must be one of the 5 named formats or a Java SimpleDateFormat pattern.",
48
+ "anyOf": [
49
+ {
50
+ "enum": ["ISO8601", "LDAP", "PEOPLE_SOFT", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32"],
51
+ "description": "Built-in named format."
52
+ },
53
+ {
54
+ "type": "string",
55
+ "not": { "pattern": "^[A-Z][A-Z0-9_]+$" },
56
+ "description": "Java SimpleDateFormat pattern (e.g. dd-MM-yyyy, yyyy-MM-dd'T'HH:mm:ssZ)."
57
+ }
58
+ ]
39
59
  },
40
60
  "input": {
41
61
  "description": "Explicitly defines the input data passed into the transform. Docs specify an object; in practice this is a nested transform object. If omitted, the transform uses the UI-configured source+attribute input.",
@@ -176,29 +176,85 @@ function lintRuleBackedInvariants(requestedType, normalized, msgs) {
176
176
  // ---------------------------------------------------------------------------
177
177
  function lintAccountAttribute(attrs) {
178
178
  const msgs = [];
179
+ // --- 1. Source reference: exactly one of sourceName / applicationId / applicationName ---
179
180
  const sourceFields = ["sourceName", "applicationId", "applicationName"];
180
181
  const presentSources = sourceFields.filter((f) => attrs?.[f] !== undefined && attrs?.[f] !== null && String(attrs[f]).trim() !== "");
181
182
  if (presentSources.length === 0) {
182
- push(msgs, "error", "accountAttribute requires exactly one source reference: sourceName, applicationId, or applicationName.", "attributes");
183
+ push(msgs, "error", "accountAttribute requires exactly one source reference: sourceName (display name), " +
184
+ "applicationId (external GUID), or applicationName (immutable internal name).", "attributes");
183
185
  }
184
186
  else if (presentSources.length > 1) {
185
- push(msgs, "error", `accountAttribute must have exactly ONE source reference; found multiple: ${presentSources.join(", ")}. Remove all but one.`, "attributes");
187
+ push(msgs, "error", `accountAttribute must have exactly ONE source reference; found multiple: ${presentSources.join(", ")}. ` +
188
+ "Remove all but one. Prefer applicationName for stability (display names can change).", "attributes");
186
189
  }
187
- // Type checks for optional boolean attrs
190
+ // --- 2. sourceName: non-empty, and warn about display-name fragility ---
191
+ if (attrs?.sourceName !== undefined) {
192
+ if (typeof attrs.sourceName !== "string" || attrs.sourceName.trim() === "") {
193
+ push(msgs, "error", "sourceName must be a non-empty string matching the source's display name.", "attributes.sourceName");
194
+ }
195
+ else {
196
+ push(msgs, "warn", "sourceName references the source display name, which can change. " +
197
+ "If the source is renamed the transform will break. Consider using applicationName (immutable) for long-term stability.", "attributes.sourceName");
198
+ }
199
+ }
200
+ // --- 3. applicationId: non-empty string ---
201
+ if (attrs?.applicationId !== undefined) {
202
+ if (typeof attrs.applicationId !== "string" || attrs.applicationId.trim() === "") {
203
+ push(msgs, "error", "applicationId must be a non-empty string (external GUID of the source).", "attributes.applicationId");
204
+ }
205
+ }
206
+ // --- 4. applicationName: non-empty string ---
207
+ if (attrs?.applicationName !== undefined) {
208
+ if (typeof attrs.applicationName !== "string" || attrs.applicationName.trim() === "") {
209
+ push(msgs, "error", "applicationName must be a non-empty string (immutable internal source name).", "attributes.applicationName");
210
+ }
211
+ }
212
+ // --- 5. attributeName: required (caught by schema), but also check non-empty ---
213
+ if (attrs?.attributeName !== undefined) {
214
+ if (typeof attrs.attributeName !== "string" || attrs.attributeName.trim() === "") {
215
+ push(msgs, "error", "attributeName must be a non-empty string matching the account attribute name in the source schema.", "attributes.attributeName");
216
+ }
217
+ }
218
+ // --- 6. accountSortAttribute: non-empty string; default is 'created' ---
219
+ if (attrs?.accountSortAttribute !== undefined) {
220
+ if (typeof attrs.accountSortAttribute !== "string") {
221
+ push(msgs, "error", "accountSortAttribute must be a string (schema attribute name). Default is 'created'.", "attributes.accountSortAttribute");
222
+ }
223
+ else if (attrs.accountSortAttribute.trim() === "") {
224
+ push(msgs, "error", "accountSortAttribute must not be empty. Omit it to use the default ('created'), or provide a valid account schema attribute name.", "attributes.accountSortAttribute");
225
+ }
226
+ }
227
+ // --- 7. accountSortDescending: boolean ---
188
228
  if (attrs?.accountSortDescending !== undefined && typeof attrs.accountSortDescending !== "boolean") {
189
- push(msgs, "error", "accountSortDescending must be a boolean.", "attributes.accountSortDescending");
229
+ push(msgs, "error", "accountSortDescending must be a boolean (true = descending, false = ascending). Default is false.", "attributes.accountSortDescending");
190
230
  }
231
+ // --- 8. accountReturnFirstLink: boolean ---
191
232
  if (attrs?.accountReturnFirstLink !== undefined && typeof attrs.accountReturnFirstLink !== "boolean") {
192
- push(msgs, "error", "accountReturnFirstLink must be a boolean.", "attributes.accountReturnFirstLink");
193
- }
194
- if (attrs?.accountSortAttribute !== undefined && typeof attrs.accountSortAttribute !== "string") {
195
- push(msgs, "error", "accountSortAttribute must be a string.", "attributes.accountSortAttribute");
233
+ push(msgs, "error", "accountReturnFirstLink must be a boolean. " +
234
+ "true = return the first sorted account's value even if null; false = skip nulls and return first non-null. Default is false.", "attributes.accountReturnFirstLink");
196
235
  }
197
- if (attrs?.accountFilter !== undefined && typeof attrs.accountFilter !== "string") {
198
- push(msgs, "error", "accountFilter must be a string.", "attributes.accountFilter");
236
+ // --- 9. accountFilter: type, non-empty, and searchable-fields hint ---
237
+ if (attrs?.accountFilter !== undefined) {
238
+ if (typeof attrs.accountFilter !== "string") {
239
+ push(msgs, "error", "accountFilter must be a string (sailpoint.object.Filter expression).", "attributes.accountFilter");
240
+ }
241
+ else if (attrs.accountFilter.trim() === "") {
242
+ push(msgs, "error", "accountFilter must not be empty if provided. Omit it to disable database-level filtering.", "attributes.accountFilter");
243
+ }
244
+ else {
245
+ push(msgs, "info", "accountFilter applies a database-level sailpoint.object.Filter. " +
246
+ "Only these fields are searchable at database level: nativeIdentity, displayName, entitlements. " +
247
+ "For other account attributes (e.g. custom fields, status), use accountPropertyFilter instead.", "attributes.accountFilter");
248
+ }
199
249
  }
200
- if (attrs?.accountPropertyFilter !== undefined && typeof attrs.accountPropertyFilter !== "string") {
201
- push(msgs, "error", "accountPropertyFilter must be a string.", "attributes.accountPropertyFilter");
250
+ // --- 10. accountPropertyFilter: type and non-empty ---
251
+ if (attrs?.accountPropertyFilter !== undefined) {
252
+ if (typeof attrs.accountPropertyFilter !== "string") {
253
+ push(msgs, "error", "accountPropertyFilter must be a string (sailpoint.object.Filter expression).", "attributes.accountPropertyFilter");
254
+ }
255
+ else if (attrs.accountPropertyFilter.trim() === "") {
256
+ push(msgs, "error", "accountPropertyFilter must not be empty if provided. Omit it to disable in-memory filtering.", "attributes.accountPropertyFilter");
257
+ }
202
258
  }
203
259
  return msgs;
204
260
  }
@@ -457,7 +513,10 @@ function lintDateCompare(attrs) {
457
513
  // ---------------------------------------------------------------------------
458
514
  function lintDateFormat(attrs) {
459
515
  const msgs = [];
460
- const NAMED_FORMATS = new Set(["ISO8601", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32", "LDAP_GENERALIZED_TIME"]);
516
+ // Exact set of named formats per SailPoint docs — no variants accepted.
517
+ const NAMED_FORMATS = new Set(["ISO8601", "LDAP", "PEOPLE_SOFT", "EPOCH_TIME_JAVA", "EPOCH_TIME_WIN32"]);
518
+ // A value that is ALL_CAPS_WITH_UNDERSCORES is clearly intended as a named constant, not a pattern.
519
+ const looksLikeNamedConstant = (s) => /^[A-Z][A-Z0-9_]+$/.test(s);
461
520
  const isLikelyPattern = (s) => /[yMdHhmsSZ]/.test(s);
462
521
  const checkFmt = (field) => {
463
522
  const raw = attrs?.[field];
@@ -468,15 +527,20 @@ function lintDateFormat(attrs) {
468
527
  return;
469
528
  }
470
529
  const t = raw.trim();
471
- const lower = t.toLowerCase();
472
- if (lower === "epoch" || lower === "unix" || lower === "unixtime" || lower === "javaepoch") {
473
- push(msgs, "error", `Unsupported ${field} '${t}'. Use a documented named format: ${Array.from(NAMED_FORMATS).join(", ")}.`, `attributes.${field}`);
530
+ // If the value looks like a named constant (ALL_CAPS_UNDERSCORES), validate strictly.
531
+ // Do NOT fall through to isLikelyPattern e.g. EPOCH_TIME_JAVA_IN_MILLIS contains
532
+ // 'H' and 'M' and would falsely pass the pattern check.
533
+ if (looksLikeNamedConstant(t)) {
534
+ if (!NAMED_FORMATS.has(t)) {
535
+ push(msgs, "error", `'${t}' is not a valid named format for ${field}. ` +
536
+ `Allowed named formats: ${Array.from(NAMED_FORMATS).join(", ")}. ` +
537
+ `Alternatively, use a Java SimpleDateFormat pattern (e.g. dd-MM-yyyy, yyyy-MM-dd'T'HH:mm:ssZ).`, `attributes.${field}`);
538
+ }
474
539
  return;
475
540
  }
476
- if (NAMED_FORMATS.has(t.toUpperCase()))
477
- return;
541
+ // Value looks like a date pattern — check it has at least one date token.
478
542
  if (!isLikelyPattern(t)) {
479
- push(msgs, "warn", `${field} '${t}' doesn't match a known named format and doesn't look like a date pattern (missing date tokens like y/M/d/H).`, `attributes.${field}`);
543
+ push(msgs, "warn", `${field} '${t}' doesn't match a known named format and doesn't look like a date pattern (missing tokens like y/M/d/H).`, `attributes.${field}`);
480
544
  }
481
545
  };
482
546
  checkFmt("inputFormat");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isc-transforms-mcp",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
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": {