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,96 @@
1
+ import { getTransformSpec, toCanonicalType } from "./catalog.js";
2
+ export function normalizeTransform(input, opts = {}) {
3
+ if (!input || typeof input !== "object") {
4
+ throw new Error("Transform must be a JSON object.");
5
+ }
6
+ const requestedType = toCanonicalType(String(input.type || ""));
7
+ if (!requestedType) {
8
+ throw new Error(`Unknown transform type: ${String(input.type)}`);
9
+ }
10
+ const spec = getTransformSpec(requestedType);
11
+ if (!spec) {
12
+ throw new Error(`No transform spec found for type: ${requestedType}`);
13
+ }
14
+ // NOTE: top-level transform objects require a name. Nested transforms typically do not.
15
+ const name = String(input.name || input.id || (opts.nested ? "" : `Transform-${requestedType}`));
16
+ // Ensure attributes object exists where required.
17
+ const incomingAttrs = input.attributes && typeof input.attributes === "object" ? { ...input.attributes } : {};
18
+ // Rule-backed operation keys are emitted as `{ type: "rule", attributes: { name, operation, ... } }`
19
+ if (requestedType === "generateRandomString" || requestedType === "getEndOfString" || requestedType === "getReferenceIdentityAttribute") {
20
+ const injected = spec.injectedAttributes || {};
21
+ const lockedName = injected.name;
22
+ const lockedOp = injected.operation;
23
+ // Allow caller to provide other keys, but lock name/operation to the doc-required values.
24
+ const merged = { ...incomingAttrs, ...injected };
25
+ if (lockedName)
26
+ merged.name = lockedName;
27
+ if (lockedOp)
28
+ merged.operation = lockedOp;
29
+ const out = {
30
+ // payload type
31
+ type: "rule",
32
+ ...(name ? { name } : {}),
33
+ // only include internal when explicitly provided for nested; default false for top-level
34
+ ...(opts.nested ? (input.internal !== undefined ? { internal: input.internal } : {}) : { internal: input.internal ?? false }),
35
+ attributes: merged,
36
+ };
37
+ // If this is a top-level transform and name is missing, enforce.
38
+ if (!opts.nested && !out.name) {
39
+ throw new Error("Transform.name is required.");
40
+ }
41
+ return out;
42
+ }
43
+ // Standard transform payload
44
+ const normalized = {
45
+ type: requestedType,
46
+ ...(name ? { name } : {}),
47
+ ...(opts.nested ? (input.internal !== undefined ? { internal: input.internal } : {}) : { internal: input.internal ?? false }),
48
+ };
49
+ if (!opts.nested && !normalized.name) {
50
+ throw new Error("Transform.name is required.");
51
+ }
52
+ if (!spec.attributesOptional) {
53
+ normalized.attributes = incomingAttrs;
54
+ }
55
+ else if (Object.keys(incomingAttrs).length > 0) {
56
+ normalized.attributes = incomingAttrs;
57
+ }
58
+ return normalized;
59
+ }
60
+ /**
61
+ * Best-effort normalization for nested transform objects inside attributes:
62
+ * - If an attribute value looks like a nested transform `{ type, attributes }`, normalize it recursively.
63
+ */
64
+ export function deepNormalizeTransform(input) {
65
+ // Top-level normalization.
66
+ const normalized = normalizeTransform(input, { nested: false });
67
+ if (!normalized.attributes || typeof normalized.attributes !== "object")
68
+ return normalized;
69
+ const walk = (v) => {
70
+ if (Array.isArray(v))
71
+ return v.map(walk);
72
+ if (v && typeof v === "object") {
73
+ if (typeof v.type === "string") {
74
+ try {
75
+ // Nested normalization: do not inject synthetic name/internal.
76
+ const n = normalizeTransform(v, { nested: true });
77
+ if (!n.attributes || typeof n.attributes !== "object")
78
+ return n;
79
+ // Recurse into nested attributes.
80
+ n.attributes = walk(n.attributes);
81
+ return n;
82
+ }
83
+ catch {
84
+ // not a known transform object; fall through
85
+ }
86
+ }
87
+ const out = {};
88
+ for (const [k, val] of Object.entries(v))
89
+ out[k] = walk(val);
90
+ return out;
91
+ }
92
+ return v;
93
+ };
94
+ normalized.attributes = walk(normalized.attributes);
95
+ return normalized;
96
+ }
@@ -0,0 +1,295 @@
1
+ // src/transforms/patterns.ts
2
+ // Named nested-transform pattern library for common SailPoint ISC use cases.
3
+ // All patterns are fully offline — no tenant access required.
4
+ // ---------------------------------------------------------------------------
5
+ // Pattern library
6
+ // ---------------------------------------------------------------------------
7
+ const PATTERNS = [
8
+ // ── 1. Fallback email chain ─────────────────────────────────────────────
9
+ {
10
+ pattern_name: "Fallback email chain",
11
+ description: "Returns the first non-null email from a priority chain: work email → personal email → a generated placeholder. Uses firstValid with accountAttribute / identityAttribute sources and a static fallback.",
12
+ keywords: ["email", "fallback", "first valid", "first non-null", "preferred email", "work email", "personal email"],
13
+ example_transform: {
14
+ type: "firstValid",
15
+ name: "Fallback Email Chain",
16
+ attributes: {
17
+ values: [
18
+ {
19
+ type: "accountAttribute",
20
+ attributes: { sourceName: "HR Source", attributeName: "workEmail" },
21
+ },
22
+ {
23
+ type: "accountAttribute",
24
+ attributes: { sourceName: "Directory", attributeName: "personalEmail" },
25
+ },
26
+ {
27
+ type: "static",
28
+ attributes: { value: "noemail@placeholder.com" },
29
+ },
30
+ ],
31
+ },
32
+ },
33
+ },
34
+ // ── 2. Conditional department → building code ───────────────────────────
35
+ {
36
+ pattern_name: "Conditional department to building code",
37
+ description: "Maps a department attribute to a building code. If department equals 'Engineering', returns 'BLDG-E'; otherwise returns 'BLDG-A'. Uses conditional with an accountAttribute variable.",
38
+ keywords: ["conditional", "department", "building", "location", "if equals", "if department", "map value", "conditional mapping"],
39
+ example_transform: {
40
+ type: "conditional",
41
+ name: "Department to Building Code",
42
+ attributes: {
43
+ department: {
44
+ type: "accountAttribute",
45
+ attributes: { sourceName: "HR Source", attributeName: "department" },
46
+ },
47
+ expression: "$department eq Engineering",
48
+ positiveCondition: "BLDG-E",
49
+ negativeCondition: "BLDG-A",
50
+ },
51
+ },
52
+ },
53
+ // ── 3. Username: first initial + last name + uniqueCounter ──────────────
54
+ {
55
+ pattern_name: "Username first initial last name uniqueCounter",
56
+ description: "Generates a unique username using the first letter of givenName plus the full sn (surname). Falls back to a counter-suffixed variant for conflict resolution. Uses usernameGenerator with dynamic variables.",
57
+ keywords: ["username", "login", "unique", "first initial", "last name", "uniquecounter", "user id", "account name"],
58
+ example_transform: {
59
+ type: "usernameGenerator",
60
+ name: "Username Generator",
61
+ attributes: {
62
+ patterns: [
63
+ "${fi}${ln}",
64
+ "${fi}${ln}${uniqueCounter}",
65
+ ],
66
+ fi: {
67
+ type: "substring",
68
+ attributes: {
69
+ begin: 0,
70
+ end: 1,
71
+ input: {
72
+ type: "identityAttribute",
73
+ attributes: { name: "givenName" },
74
+ },
75
+ },
76
+ },
77
+ ln: {
78
+ type: "lower",
79
+ attributes: {
80
+ input: {
81
+ type: "identityAttribute",
82
+ attributes: { name: "sn" },
83
+ },
84
+ },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ // ── 4. Epoch timestamp → ISO8601 ────────────────────────────────────────
90
+ {
91
+ pattern_name: "Date format EPOCH to ISO8601",
92
+ description: "Converts a Java-epoch (milliseconds since 1970-01-01T00:00:00Z) timestamp from an HR source into an ISO8601 date string. Uses dateFormat with inputFormat=EPOCH_TIME_JAVA and outputFormat=ISO8601.",
93
+ keywords: ["epoch", "iso8601", "date format", "convert date", "timestamp", "java epoch", "epoch_time_java"],
94
+ example_transform: {
95
+ type: "dateFormat",
96
+ name: "Epoch to ISO8601",
97
+ attributes: {
98
+ inputFormat: "EPOCH_TIME_JAVA",
99
+ outputFormat: "ISO8601",
100
+ input: {
101
+ type: "accountAttribute",
102
+ attributes: { sourceName: "HR Source", attributeName: "hireDate" },
103
+ },
104
+ },
105
+ },
106
+ },
107
+ // ── 5. Normalize + lowercase name ───────────────────────────────────────
108
+ {
109
+ pattern_name: "Normalize and lowercase name",
110
+ description: "Normalizes a name (handles Mc/Mac, de/von, Roman numerals, etc.) then lowercases it. Useful for email prefix generation from display names. Uses concat around lower(normalizeNames(givenName)) and lower(normalizeNames(sn)).",
111
+ keywords: ["normalize name", "lowercase name", "email prefix", "first last", "decompose", "diacritic", "normalize lower"],
112
+ example_transform: {
113
+ type: "concat",
114
+ name: "Normalized Lowercase Full Name",
115
+ attributes: {
116
+ values: [
117
+ {
118
+ type: "lower",
119
+ attributes: {
120
+ input: {
121
+ type: "normalizeNames",
122
+ attributes: {
123
+ input: {
124
+ type: "identityAttribute",
125
+ attributes: { name: "givenName" },
126
+ },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ ".",
132
+ {
133
+ type: "lower",
134
+ attributes: {
135
+ input: {
136
+ type: "normalizeNames",
137
+ attributes: {
138
+ input: {
139
+ type: "identityAttribute",
140
+ attributes: { name: "sn" },
141
+ },
142
+ },
143
+ },
144
+ },
145
+ },
146
+ ],
147
+ },
148
+ },
149
+ },
150
+ // ── 6. Lookup country code → region label ───────────────────────────────
151
+ {
152
+ pattern_name: "Lookup country code to region label",
153
+ description: "Maps a 2-letter ISO country code from an HR source to a human-readable region label (AMER, EMEA, APAC, etc.). Uses lookup with a table of known codes and a 'default' fallback.",
154
+ keywords: ["lookup", "country", "region", "amer", "emea", "apac", "map code", "country code to region"],
155
+ example_transform: {
156
+ type: "lookup",
157
+ name: "Country to Region",
158
+ attributes: {
159
+ table: {
160
+ US: "AMER", CA: "AMER", MX: "AMER",
161
+ GB: "EMEA", DE: "EMEA", FR: "EMEA", IN: "EMEA",
162
+ AU: "APAC", JP: "APAC", SG: "APAC",
163
+ default: "UNKNOWN",
164
+ },
165
+ input: {
166
+ type: "accountAttribute",
167
+ attributes: { sourceName: "HR Source", attributeName: "countryCode" },
168
+ },
169
+ },
170
+ },
171
+ },
172
+ // ── 7. Email from first.last@domain ─────────────────────────────────────
173
+ {
174
+ pattern_name: "Email from first dot last at domain",
175
+ description: "Builds an email address from givenName + '.' + sn + '@domain.com'. Uses concat with identityAttribute sources and a static domain literal. Lowercases both name parts.",
176
+ keywords: ["email", "concat", "first name last name", "firstname.lastname", "email address", "build email"],
177
+ example_transform: {
178
+ type: "concat",
179
+ name: "First Last Email Address",
180
+ attributes: {
181
+ values: [
182
+ {
183
+ type: "lower",
184
+ attributes: {
185
+ input: { type: "identityAttribute", attributes: { name: "givenName" } },
186
+ },
187
+ },
188
+ ".",
189
+ {
190
+ type: "lower",
191
+ attributes: {
192
+ input: { type: "identityAttribute", attributes: { name: "sn" } },
193
+ },
194
+ },
195
+ "@",
196
+ {
197
+ type: "static",
198
+ attributes: { value: "example.com" },
199
+ },
200
+ ],
201
+ },
202
+ },
203
+ },
204
+ // ── 8. Date compare lifecycle state ─────────────────────────────────────
205
+ {
206
+ pattern_name: "Date compare lifecycle state",
207
+ description: "Compares today ('now') against an identity's termination date. If today is before the termination date, the identity is active; otherwise it is terminated. Uses dateCompare with operator LT.",
208
+ keywords: ["date compare", "lifecycle", "termination", "expiry", "active", "terminated", "before after date"],
209
+ example_transform: {
210
+ type: "dateCompare",
211
+ name: "Lifecycle State from Termination Date",
212
+ attributes: {
213
+ firstDate: "now",
214
+ secondDate: {
215
+ type: "dateFormat",
216
+ attributes: {
217
+ inputFormat: "EPOCH_TIME_JAVA",
218
+ outputFormat: "ISO8601",
219
+ input: {
220
+ type: "accountAttribute",
221
+ attributes: { sourceName: "HR Source", attributeName: "terminationDate" },
222
+ },
223
+ },
224
+ },
225
+ operator: "LT",
226
+ positiveCondition: "active",
227
+ negativeCondition: "terminated",
228
+ },
229
+ },
230
+ },
231
+ // ── 9. Phone number E.164 normalisation ─────────────────────────────────
232
+ {
233
+ pattern_name: "Phone number E164 normalisation",
234
+ description: "Normalises a phone number from an account attribute into E.164 international format (e.g. +12125551234). The defaultRegion ensures country-code insertion for numbers without an explicit country prefix.",
235
+ keywords: ["phone", "e164", "e.164", "international", "normalize phone", "phone format", "mobile"],
236
+ example_transform: {
237
+ type: "e164phone",
238
+ name: "Normalise Phone Number",
239
+ attributes: {
240
+ defaultRegion: "US",
241
+ input: {
242
+ type: "accountAttribute",
243
+ attributes: { sourceName: "HR Source", attributeName: "mobilePhone" },
244
+ },
245
+ },
246
+ },
247
+ },
248
+ // ── 10. Split and extract domain from email ──────────────────────────────
249
+ {
250
+ pattern_name: "Split extract domain from email",
251
+ description: "Extracts the domain part from an email address by splitting on '@' and returning the second segment (index 1). Uses split with delimiter='@' and index=1.",
252
+ keywords: ["split", "extract domain", "email domain", "domain from email", "after @"],
253
+ example_transform: {
254
+ type: "split",
255
+ name: "Extract Email Domain",
256
+ attributes: {
257
+ delimiter: "@",
258
+ index: 1,
259
+ input: {
260
+ type: "identityAttribute",
261
+ attributes: { name: "email" },
262
+ },
263
+ },
264
+ },
265
+ },
266
+ ];
267
+ // ---------------------------------------------------------------------------
268
+ // Exports
269
+ // ---------------------------------------------------------------------------
270
+ export function suggestPattern(description) {
271
+ const lower = description.toLowerCase();
272
+ const scored = PATTERNS.map((p) => {
273
+ const hit = p.keywords.filter((kw) => lower.includes(kw.toLowerCase())).length;
274
+ return { pattern: p, score: hit };
275
+ })
276
+ .filter((r) => r.score > 0)
277
+ .sort((a, b) => b.score - a.score);
278
+ const best = scored[0]?.pattern ?? PATTERNS[0];
279
+ const others = scored.slice(1, 4).map((r) => r.pattern.pattern_name);
280
+ return {
281
+ pattern_name: best.pattern_name,
282
+ description: best.description,
283
+ example_transform: best.example_transform,
284
+ other_matches: others,
285
+ };
286
+ }
287
+ export function listPatterns() {
288
+ return PATTERNS.map((p) => ({
289
+ pattern_name: p.pattern_name,
290
+ description: p.description,
291
+ }));
292
+ }
293
+ export function getPattern(name) {
294
+ return PATTERNS.find((p) => p.pattern_name.toLowerCase() === name.toLowerCase());
295
+ }