isc-transforms-mcp 1.0.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 (63) hide show
  1. package/JSONS/authoritative-operation-catalog.json +280 -0
  2. package/JSONS/sailpoint.isc.transforms.accountAttribute.schema.json +164 -0
  3. package/JSONS/sailpoint.isc.transforms.base64Decode.schema.json +37 -0
  4. package/JSONS/sailpoint.isc.transforms.base64Encode.schema.json +32 -0
  5. package/JSONS/sailpoint.isc.transforms.concat.schema.json +109 -0
  6. package/JSONS/sailpoint.isc.transforms.conditional.schema.json +161 -0
  7. package/JSONS/sailpoint.isc.transforms.dateCompare.schema.json +159 -0
  8. package/JSONS/sailpoint.isc.transforms.dateFormat.schema.json +101 -0
  9. package/JSONS/sailpoint.isc.transforms.dateMath.schema.json +119 -0
  10. package/JSONS/sailpoint.isc.transforms.decomposeDiacriticalMarks.schema.json +92 -0
  11. package/JSONS/sailpoint.isc.transforms.displayName.schema.json +42 -0
  12. package/JSONS/sailpoint.isc.transforms.e164phone.schema.json +107 -0
  13. package/JSONS/sailpoint.isc.transforms.firstValid.schema.json +129 -0
  14. package/JSONS/sailpoint.isc.transforms.generateRandomString.schema.json +94 -0
  15. package/JSONS/sailpoint.isc.transforms.getEndOfString.schema.json +118 -0
  16. package/JSONS/sailpoint.isc.transforms.getReferenceIdentityAttribute.schema.json +79 -0
  17. package/JSONS/sailpoint.isc.transforms.identityAttribute.schema.json +104 -0
  18. package/JSONS/sailpoint.isc.transforms.index.schema.json +48 -0
  19. package/JSONS/sailpoint.isc.transforms.indexOf.schema.json +90 -0
  20. package/JSONS/sailpoint.isc.transforms.iso3166.schema.json +103 -0
  21. package/JSONS/sailpoint.isc.transforms.join.schema.json +113 -0
  22. package/JSONS/sailpoint.isc.transforms.lastIndexOf.schema.json +90 -0
  23. package/JSONS/sailpoint.isc.transforms.leftPad.schema.json +96 -0
  24. package/JSONS/sailpoint.isc.transforms.lookup.schema.json +100 -0
  25. package/JSONS/sailpoint.isc.transforms.lower.schema.json +80 -0
  26. package/JSONS/sailpoint.isc.transforms.normalizeNames.schema.json +79 -0
  27. package/JSONS/sailpoint.isc.transforms.randomAlphaNumeric.schema.json +53 -0
  28. package/JSONS/sailpoint.isc.transforms.randomNumeric.schema.json +53 -0
  29. package/JSONS/sailpoint.isc.transforms.reference.schema.json +90 -0
  30. package/JSONS/sailpoint.isc.transforms.replace.schema.json +96 -0
  31. package/JSONS/sailpoint.isc.transforms.replaceAll.schema.json +96 -0
  32. package/JSONS/sailpoint.isc.transforms.rfc5646.schema.json +79 -0
  33. package/JSONS/sailpoint.isc.transforms.rightPad.schema.json +96 -0
  34. package/JSONS/sailpoint.isc.transforms.rule.schema.json +106 -0
  35. package/JSONS/sailpoint.isc.transforms.split.schema.json +103 -0
  36. package/JSONS/sailpoint.isc.transforms.static.schema.json +131 -0
  37. package/JSONS/sailpoint.isc.transforms.substring.schema.json +167 -0
  38. package/JSONS/sailpoint.isc.transforms.trim.schema.json +93 -0
  39. package/JSONS/sailpoint.isc.transforms.upper.schema.json +80 -0
  40. package/JSONS/sailpoint.isc.transforms.usernameGenerator.schema.json +106 -0
  41. package/JSONS/sailpoint.isc.transforms.uuid.schema.json +32 -0
  42. package/LICENSE +21 -0
  43. package/README.md +221 -0
  44. package/bin/isc-transforms-mcp.mjs +3 -0
  45. package/dist/allowlist.js +37 -0
  46. package/dist/config.js +67 -0
  47. package/dist/http/errors.js +19 -0
  48. package/dist/http/iscAuth.js +45 -0
  49. package/dist/http/iscClient.js +73 -0
  50. package/dist/index.js +613 -0
  51. package/dist/logger.js +9 -0
  52. package/dist/redact.js +28 -0
  53. package/dist/transforms/catalog.js +566 -0
  54. package/dist/transforms/explain.js +266 -0
  55. package/dist/transforms/generate.js +551 -0
  56. package/dist/transforms/index.js +9 -0
  57. package/dist/transforms/lint.js +839 -0
  58. package/dist/transforms/normalize.js +96 -0
  59. package/dist/transforms/patterns.js +295 -0
  60. package/dist/transforms/testcases.js +350 -0
  61. package/dist/transforms/validate.js +250 -0
  62. package/dist/util/diff.js +23 -0
  63. package/package.json +76 -0
@@ -0,0 +1,266 @@
1
+ // src/transforms/explain.ts
2
+ // Human-readable error explanation + best-effort auto-correction for SailPoint ISC transforms.
3
+ // Fully offline — no tenant access required.
4
+ import { validateTransform } from "./validate.js";
5
+ import { lintTransform } from "./lint.js";
6
+ const RECIPES = [
7
+ // --- Required field missing ---
8
+ {
9
+ match: (e) => e.message.includes("Missing required") || e.message.includes("required field"),
10
+ explain: (e) => {
11
+ const field = e.message.match(/['`]([^'`]+)['`]/)?.[1] ?? "field";
12
+ return (`The field '${field}' is required by SailPoint and must be present in the transform JSON. ` +
13
+ `Check the official docs for this operation to see what value is expected.`);
14
+ },
15
+ },
16
+ // --- Unknown / additional property ---
17
+ {
18
+ match: (e) => e.message.toLowerCase().includes("unknown") || e.message.toLowerCase().includes("additional"),
19
+ explain: (e) => {
20
+ const field = e.message.match(/['`"]([^'`"]+)['`"]/)?.[1] ?? "property";
21
+ return (`The field '${field}' is not a recognised attribute for this transform type. ` +
22
+ `SailPoint's JSON Schema for this operation uses additionalProperties:false, ` +
23
+ `meaning only documented attributes are permitted. Remove or rename this field.`);
24
+ },
25
+ correct: (transform, e) => {
26
+ const field = e.message.match(/['`"]([^'`"]+)['`"]/)?.[1];
27
+ if (field && transform.attributes && Object.prototype.hasOwnProperty.call(transform.attributes, field)) {
28
+ delete transform.attributes[field];
29
+ }
30
+ else if (field && Object.prototype.hasOwnProperty.call(transform, field) && !["type", "name", "attributes", "requiresPeriodicRefresh", "internal"].includes(field)) {
31
+ delete transform[field];
32
+ }
33
+ },
34
+ },
35
+ // --- Type mismatch (boolean) ---
36
+ {
37
+ match: (e) => e.message.toLowerCase().includes("type mismatch") && e.message.toLowerCase().includes("boolean"),
38
+ explain: (e) => {
39
+ const path = e.path ?? "";
40
+ return (`The field at '${path}' must be a boolean (true or false), not a string like "true". ` +
41
+ `SailPoint's schema enforces the native JSON boolean type for this attribute.`);
42
+ },
43
+ correct: (transform, e) => {
44
+ const path = (e.path ?? "").replace(/^\//, "").split("/");
45
+ let obj = transform;
46
+ for (let i = 0; i < path.length - 1; i++)
47
+ obj = obj?.[path[i]];
48
+ const key = path[path.length - 1];
49
+ if (key && obj && typeof obj[key] === "string") {
50
+ obj[key] = obj[key].toLowerCase() === "true";
51
+ }
52
+ },
53
+ },
54
+ // --- Type mismatch (string) ---
55
+ {
56
+ match: (e) => e.message.toLowerCase().includes("type mismatch") && e.message.toLowerCase().includes("string"),
57
+ explain: (e) => {
58
+ const path = e.path ?? "";
59
+ const rawData = e.raw?.data;
60
+ const rawType = rawData !== undefined ? typeof rawData : "different type";
61
+ return `The field at '${path}' must be a string, not a ${rawType}. Wrap the value in double quotes.`;
62
+ },
63
+ },
64
+ // --- conditional expression operator ---
65
+ {
66
+ match: (e) => e.message.toLowerCase().includes("eq") && e.message.toLowerCase().includes("expression"),
67
+ explain: () => `SailPoint's conditional transform only supports the 'eq' (equals) comparator in expressions. ` +
68
+ `Operators like '!=', '>', '<', '>=', '<=', or others are NOT supported. ` +
69
+ `Rewrite your expression using: '<ValueA> eq <ValueB>'.`,
70
+ },
71
+ // --- dateFormat named format ---
72
+ {
73
+ match: (e) => e.message.includes("EPOCH_TIME_JAVA") || (e.message.toLowerCase().includes("epoch") && e.message.toLowerCase().includes("format")),
74
+ explain: () => `SailPoint's dateFormat transform does not accept 'epoch' as a format token. ` +
75
+ `Use the named constant 'EPOCH_TIME_JAVA' for Java-epoch milliseconds, ` +
76
+ `'EPOCH_TIME_WIN32' for Windows FILETIME, or 'ISO8601' for standard ISO datetime strings.`,
77
+ },
78
+ // --- week rounding in dateMath ---
79
+ {
80
+ match: (e) => e.message.includes("Rounding with 'w'"),
81
+ explain: () => `SailPoint's dateMath transform does not support rounding by week ('w'). ` +
82
+ `You can add/subtract weeks (e.g. 'now+1w') but cannot round to the week boundary. ` +
83
+ `Use day ('d'), month ('M'), or year ('y') rounding instead.`,
84
+ },
85
+ // --- accountAttribute multiple sources ---
86
+ {
87
+ match: (e) => e.message.toLowerCase().includes("exactly one source"),
88
+ explain: () => `The accountAttribute transform requires exactly ONE of: sourceName, applicationId, or applicationName. ` +
89
+ `Providing more than one creates an ambiguous source reference. ` +
90
+ `Remove all but the one you intend to use: prefer 'sourceName' for human-readable references, ` +
91
+ `or 'applicationName' for immutable source references.`,
92
+ correct: (transform) => {
93
+ const attrs = transform.attributes;
94
+ if (!attrs)
95
+ return;
96
+ const sourceFields = ["sourceName", "applicationId", "applicationName"];
97
+ const present = sourceFields.filter((f) => attrs[f] !== undefined);
98
+ // Keep sourceName if present; otherwise keep first found
99
+ const keep = present.includes("sourceName") ? "sourceName" : present[0];
100
+ for (const f of present) {
101
+ if (f !== keep)
102
+ delete attrs[f];
103
+ }
104
+ },
105
+ },
106
+ // --- replace regex invalid ---
107
+ {
108
+ match: (e) => e.message.toLowerCase().includes("valid regular expression") || e.message.toLowerCase().includes("invalid regex"),
109
+ explain: (e) => {
110
+ const regexMatch = e.message.match(/regex '([^']+)'/);
111
+ const regex = regexMatch?.[1] ?? "<pattern>";
112
+ return (`The regex '${regex}' is not a valid regular expression. ` +
113
+ `Check for: unmatched brackets, invalid escape sequences (use \\\\ for literal backslash), ` +
114
+ `or unsupported regex syntax. Test your regex at regex101.com before using it here.`);
115
+ },
116
+ },
117
+ // --- join delimiter → separator ---
118
+ {
119
+ match: (e) => e.message.includes("delimiter") && e.message.includes("separator"),
120
+ explain: () => `The join transform uses 'separator' (not 'delimiter') as the attribute name. ` +
121
+ `Rename your attribute from 'delimiter' to 'separator'.`,
122
+ correct: (transform) => {
123
+ if (transform.attributes?.delimiter !== undefined) {
124
+ transform.attributes.separator = transform.attributes.delimiter;
125
+ delete transform.attributes.delimiter;
126
+ }
127
+ },
128
+ },
129
+ // --- getEndOfString uses numChars not length ---
130
+ {
131
+ match: (e) => e.message.includes("numChars") && e.message.includes("length"),
132
+ explain: () => `The getEndOfString transform uses 'numChars' (not 'length') to specify how many characters ` +
133
+ `to return from the end of the string. Rename 'length' to 'numChars'.`,
134
+ correct: (transform) => {
135
+ if (transform.attributes?.length !== undefined && transform.attributes?.numChars === undefined) {
136
+ transform.attributes.numChars = transform.attributes.length;
137
+ delete transform.attributes.length;
138
+ }
139
+ },
140
+ },
141
+ // --- requiresPeriodicRefresh type ---
142
+ {
143
+ match: (e) => e.message.includes("requiresPeriodicRefresh"),
144
+ explain: () => `'requiresPeriodicRefresh' must be a native JSON boolean (true or false). ` +
145
+ `If you wrote "true" (a string), change it to true (no quotes). ` +
146
+ `This flag controls whether ISC re-evaluates the transform during the nightly identity refresh.`,
147
+ correct: (transform) => {
148
+ if (typeof transform.requiresPeriodicRefresh === "string") {
149
+ transform.requiresPeriodicRefresh = transform.requiresPeriodicRefresh.toLowerCase() === "true";
150
+ }
151
+ },
152
+ },
153
+ // --- lookup missing default ---
154
+ {
155
+ match: (e) => e.message.toLowerCase().includes("default") && e.message.toLowerCase().includes("lookup"),
156
+ explain: () => `The lookup table is missing a 'default' key. Without it, the transform throws an error ` +
157
+ `whenever the input value doesn't match any key in the table. ` +
158
+ `Add: "default": "<fallback value>" to handle unmatched inputs gracefully.`,
159
+ correct: (transform) => {
160
+ if (transform.attributes?.table && typeof transform.attributes.table === "object" && !transform.attributes.table.default) {
161
+ transform.attributes.table.default = "UNKNOWN";
162
+ }
163
+ },
164
+ },
165
+ // --- usernameGenerator sourceCheck type ---
166
+ {
167
+ match: (e) => e.message.includes("sourceCheck") && e.message.toLowerCase().includes("boolean"),
168
+ explain: () => `'sourceCheck' must be a boolean (true or false). ` +
169
+ `When true, the username generator checks uniqueness against the target system directly; ` +
170
+ `when false (default), it checks only the ISC database.`,
171
+ correct: (transform) => {
172
+ if (typeof transform.attributes?.sourceCheck === "string") {
173
+ transform.attributes.sourceCheck = transform.attributes.sourceCheck.toLowerCase() === "true";
174
+ }
175
+ },
176
+ },
177
+ ];
178
+ export function explainTransformErrors(transformJson, externalError) {
179
+ if (!transformJson || typeof transformJson !== "object") {
180
+ return {
181
+ explanation: "The input is not a valid JSON object.",
182
+ issues: [{ message: "Not a JSON object.", suggestion: "Wrap your transform in braces: { ... }" }],
183
+ corrected_json: null,
184
+ };
185
+ }
186
+ // Collect all validation + lint errors
187
+ const validation = validateTransform(transformJson);
188
+ const lintResult = lintTransform(transformJson);
189
+ const allErrors = [
190
+ ...validation.errors,
191
+ ...lintResult.messages.filter((m) => m.level === "error"),
192
+ ];
193
+ // Add external error as a synthetic lint message
194
+ if (externalError) {
195
+ allErrors.push({ level: "error", message: externalError, path: "<external>" });
196
+ }
197
+ if (allErrors.length === 0 && !externalError) {
198
+ return {
199
+ explanation: "No errors detected. The transform passed both schema validation and semantic lint.",
200
+ issues: [],
201
+ corrected_json: null,
202
+ };
203
+ }
204
+ // Deep-clone for correction attempts
205
+ let corrected = JSON.parse(JSON.stringify(transformJson));
206
+ let correctionApplied = false;
207
+ const issues = [];
208
+ const seen = new Set();
209
+ for (const err of allErrors) {
210
+ const key = err.message.slice(0, 80);
211
+ if (seen.has(key))
212
+ continue;
213
+ seen.add(key);
214
+ let matched = false;
215
+ for (const recipe of RECIPES) {
216
+ if (recipe.match(err)) {
217
+ const suggestion = recipe.explain(err);
218
+ issues.push({ message: err.message, suggestion });
219
+ if (recipe.correct) {
220
+ try {
221
+ recipe.correct(corrected, err);
222
+ correctionApplied = true;
223
+ }
224
+ catch {
225
+ // best-effort; skip silently
226
+ }
227
+ }
228
+ matched = true;
229
+ break;
230
+ }
231
+ }
232
+ if (!matched) {
233
+ issues.push({
234
+ message: err.message,
235
+ suggestion: `Refer to the official SailPoint docs for this transform type and verify that ` +
236
+ `all required fields are present and correctly typed. ` +
237
+ `Path: ${err.path ?? "unknown"}.`,
238
+ });
239
+ }
240
+ }
241
+ // Verify corrected JSON actually improved (re-validate)
242
+ let finalCorrected = null;
243
+ if (correctionApplied) {
244
+ const reValidated = validateTransform(corrected);
245
+ const reLinted = lintTransform(corrected);
246
+ const reErrors = [
247
+ ...reValidated.errors,
248
+ ...reLinted.messages.filter((m) => m.level === "error"),
249
+ ];
250
+ if (reErrors.length < allErrors.length) {
251
+ finalCorrected = corrected;
252
+ }
253
+ }
254
+ const countFixed = correctionApplied && finalCorrected
255
+ ? allErrors.length - (validateTransform(finalCorrected).errors.length + lintTransform(finalCorrected).messages.filter((m) => m.level === "error").length)
256
+ : 0;
257
+ const explanation = issues.length === 1
258
+ ? `Found 1 error in the transform.`
259
+ : `Found ${issues.length} error(s) in the transform.` +
260
+ (countFixed > 0 ? ` Auto-correction fixed ${countFixed} of them.` : "");
261
+ return {
262
+ explanation,
263
+ issues,
264
+ corrected_json: finalCorrected,
265
+ };
266
+ }