@cloud-copilot/iam-lens 0.1.11 → 0.1.13

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 (33) hide show
  1. package/dist/cjs/canWhat/canWhat.d.ts +10 -0
  2. package/dist/cjs/canWhat/canWhat.d.ts.map +1 -0
  3. package/dist/cjs/canWhat/canWhat.js +61 -0
  4. package/dist/cjs/canWhat/canWhat.js.map +1 -0
  5. package/dist/cjs/canWhat/permission.d.ts +54 -0
  6. package/dist/cjs/canWhat/permission.d.ts.map +1 -0
  7. package/dist/cjs/canWhat/permission.js +749 -0
  8. package/dist/cjs/canWhat/permission.js.map +1 -0
  9. package/dist/cjs/canWhat/permissionSet.d.ts +59 -0
  10. package/dist/cjs/canWhat/permissionSet.d.ts.map +1 -0
  11. package/dist/cjs/canWhat/permissionSet.js +296 -0
  12. package/dist/cjs/canWhat/permissionSet.js.map +1 -0
  13. package/dist/cjs/cli.js +28 -0
  14. package/dist/cjs/cli.js.map +1 -1
  15. package/dist/cjs/collect/client.js +1 -1
  16. package/dist/cjs/collect/client.js.map +1 -1
  17. package/dist/esm/canWhat/canWhat.d.ts +10 -0
  18. package/dist/esm/canWhat/canWhat.d.ts.map +1 -0
  19. package/dist/esm/canWhat/canWhat.js +58 -0
  20. package/dist/esm/canWhat/canWhat.js.map +1 -0
  21. package/dist/esm/canWhat/permission.d.ts +54 -0
  22. package/dist/esm/canWhat/permission.d.ts.map +1 -0
  23. package/dist/esm/canWhat/permission.js +737 -0
  24. package/dist/esm/canWhat/permission.js.map +1 -0
  25. package/dist/esm/canWhat/permissionSet.d.ts +59 -0
  26. package/dist/esm/canWhat/permissionSet.d.ts.map +1 -0
  27. package/dist/esm/canWhat/permissionSet.js +288 -0
  28. package/dist/esm/canWhat/permissionSet.js.map +1 -0
  29. package/dist/esm/cli.js +28 -0
  30. package/dist/esm/cli.js.map +1 -1
  31. package/dist/esm/collect/client.js +1 -1
  32. package/dist/esm/collect/client.js.map +1 -1
  33. package/package.json +4 -2
@@ -0,0 +1,737 @@
1
+ /**
2
+ * Convert an AWS wildcard ARN pattern (e.g. "arn:aws:s3:::bucket/*") into a RegExp.
3
+ */
4
+ function wildcardToRegex(pattern) {
5
+ const parts = pattern.split('*').map((s) => s.replace(/[-/\\^$+?.()|[\]{}]/g, '\\$&'));
6
+ return new RegExp('^' + parts.join('.*') + '$');
7
+ }
8
+ /**
9
+ * An immutable representation of a single permission for a specific action.
10
+ *
11
+ * This will eventually have methods like "merge with another permission",
12
+ * "check if overlaps with another permission", "subtract a deny permission",
13
+ * etc and those will all return a new Permission instance.
14
+ */
15
+ export class Permission {
16
+ constructor(effect, service, action, resource, notResource, conditions) {
17
+ this.effect = effect;
18
+ this.service = service;
19
+ this.action = action;
20
+ this.resource = resource;
21
+ this.notResource = notResource;
22
+ this.conditions = conditions;
23
+ if (resource !== undefined && notResource !== undefined) {
24
+ throw new Error('Permission must have a resource or notResource, not both.');
25
+ }
26
+ else if (resource === undefined && notResource === undefined) {
27
+ throw new Error('Permission must have a resource or notResource, one must be defined.');
28
+ }
29
+ }
30
+ /**
31
+ * Returns true if this Permission completely includes the other Permission.
32
+ * Only supports merging of "Allow" permissions (same effect, service, action).
33
+ */
34
+ includes(other) {
35
+ // 1. Effects, service, and action must match
36
+ if (this.effect !== other.effect ||
37
+ this.service !== other.service ||
38
+ this.action !== other.action) {
39
+ return false;
40
+ }
41
+ // 2. Conditions: every condition in this must be implied by the other permission’s conditions
42
+ // That is, for each operator and context key in this.conditions, other.conditions must have it,
43
+ // and the values must satisfy inclusion logic per operator.
44
+ const condsA = normalizeConditionKeys(this.conditions || {});
45
+ const condsB = normalizeConditionKeys(other.conditions || {});
46
+ for (const op of Object.keys(condsA)) {
47
+ if (!(op in condsB))
48
+ return false;
49
+ const keysA = Object.keys(condsA[op]);
50
+ const keysB = Object.keys(condsB[op]);
51
+ // Every key in A must appear in B
52
+ for (const key of keysA) {
53
+ if (!keysB.includes(key))
54
+ return false;
55
+ const valsA = condsA[op][key];
56
+ const valsB = condsB[op][key];
57
+ const baseOp = conditionBaseOperator(op);
58
+ switch (baseOp) {
59
+ case 'stringequals':
60
+ case 'stringlike':
61
+ case 'arnequals':
62
+ case 'arnlike':
63
+ // other must be at least as restrictive: B_vals ⊆ A_vals
64
+ if (!valsB.every((v) => valsA.includes(v)))
65
+ return false;
66
+ break;
67
+ case 'stringnotequals':
68
+ case 'stringnotlike':
69
+ case 'arnnotequals':
70
+ case 'arnnotlike':
71
+ // other must exclude at least what A excludes: A_vals ⊆ B_vals
72
+ if (!valsA.every((v) => valsB.includes(v)))
73
+ return false;
74
+ break;
75
+ case 'numericlessthan':
76
+ case 'numericlessthanequals':
77
+ // other boundary <= this boundary
78
+ const numA = Number(valsA[0]);
79
+ const numB = Number(valsB[0]);
80
+ if (isNaN(numA) || isNaN(numB))
81
+ return false;
82
+ if (numB > numA)
83
+ return false;
84
+ break;
85
+ case 'numericgreaterthan':
86
+ case 'numericgreaterthanequals':
87
+ // other boundary >= this boundary
88
+ const ngA = Number(valsA[0]);
89
+ const ngB = Number(valsB[0]);
90
+ if (isNaN(ngA) || isNaN(ngB))
91
+ return false;
92
+ if (ngB < ngA)
93
+ return false;
94
+ break;
95
+ case 'bool':
96
+ // other must have the same boolean value
97
+ if (valsA[0] !== valsB[0])
98
+ return false;
99
+ break;
100
+ case 'ipaddress':
101
+ case 'notipaddress':
102
+ // every CIDR in B must be contained in some CIDR in A
103
+ for (const cidrB of valsB) {
104
+ if (!valsA.some((cidrA) => cidrA === cidrB)) {
105
+ return false;
106
+ }
107
+ }
108
+ break;
109
+ case 'datelessthan':
110
+ case 'datelessthanequals':
111
+ // other date <= this date lexically (ISO)
112
+ const dA = valsA[0];
113
+ const dB = valsB[0];
114
+ if (dB > dA)
115
+ return false;
116
+ break;
117
+ case 'dategreaterthan':
118
+ case 'dategreaterthanequals':
119
+ // other date >= this date
120
+ const dgA = valsA[0];
121
+ const dgB = valsB[0];
122
+ if (dgB < dgA)
123
+ return false;
124
+ break;
125
+ default:
126
+ return false;
127
+ }
128
+ }
129
+ }
130
+ // 3. Resources / NotResources
131
+ const thisResource = this.resource;
132
+ const thisNotResource = this.notResource;
133
+ const otherResource = other.resource;
134
+ const otherNotResource = other.notResource;
135
+ // 3a. If both have resource[]
136
+ if (thisResource !== undefined && otherResource !== undefined) {
137
+ return otherResource.every((r2) => thisResource.some((r1) => wildcardToRegex(r1).test(r2)));
138
+ }
139
+ // 3b. Both have notResource[]
140
+ if (thisNotResource !== undefined && otherNotResource !== undefined) {
141
+ return thisNotResource.every((n1) => otherNotResource.some((n2) => wildcardToRegex(n1).test(n2)));
142
+ }
143
+ // 3c. A.resource & B.notResource -> B allows almost all, A allows only R1 -> true iff every N2 is matched by some R1
144
+ if (thisResource !== undefined && otherNotResource !== undefined) {
145
+ return otherNotResource.every((n2) => thisResource.some((r1) => wildcardToRegex(r1).test(n2)));
146
+ }
147
+ // 3d. A.notResource & B.resource -> every r2 ∉ N1
148
+ if (thisNotResource !== undefined && otherResource !== undefined) {
149
+ return otherResource.every((r2) => !thisNotResource.some((n1) => wildcardToRegex(n1).test(r2)));
150
+ }
151
+ return false;
152
+ }
153
+ /**
154
+ * Returns the union of this Permission with another.
155
+ * If one includes the other, return the including Permission.
156
+ * Otherwise, attempt to merge conditions and resource/notResource.
157
+ * If merge yields a single Permission, return it; else return both.
158
+ */
159
+ union(other) {
160
+ // 1. Ensure same effect, service, and action
161
+ if (this.effect !== other.effect ||
162
+ this.service !== other.service ||
163
+ this.action !== other.action) {
164
+ return [this, other];
165
+ }
166
+ // 2. If one includes the other, return the including one
167
+ if (this.includes(other)) {
168
+ return [this];
169
+ }
170
+ if (other.includes(this)) {
171
+ return [other];
172
+ }
173
+ // 3. Attempt to combine conditions
174
+ const condsA = this.conditions || {};
175
+ const condsB = other.conditions || {};
176
+ const mergedConds = mergeConditions(condsA, condsB);
177
+ if (mergedConds === null) {
178
+ return [this, other];
179
+ }
180
+ // 4. Combine resource/notResource (constructor enforces exclusivity)
181
+ const thisResource = this.resource;
182
+ const thisNotResource = this.notResource;
183
+ const otherResource = other.resource;
184
+ const otherNotResource = other.notResource;
185
+ const eff = this.effect;
186
+ const svc = this.service;
187
+ const act = this.action;
188
+ const conds = Object.keys(mergedConds).length > 0 ? mergedConds : undefined;
189
+ // Both have resource[]
190
+ if (thisResource !== undefined && otherResource !== undefined) {
191
+ const union = Array.from(new Set([...thisResource, ...otherResource]));
192
+ return [new Permission(eff, svc, act, union, undefined, conds)];
193
+ }
194
+ // Both have notResource[]
195
+ if (thisNotResource !== undefined && otherNotResource !== undefined) {
196
+ // Intersection of both notResource arrays
197
+ const intersection = thisNotResource.filter((n) => otherNotResource.includes(n));
198
+ return [new Permission(eff, svc, act, undefined, intersection, conds)];
199
+ }
200
+ // One has resource, other has notResource
201
+ if (thisResource !== undefined && otherNotResource !== undefined) {
202
+ return [
203
+ new Permission(eff, svc, act, thisResource, undefined, conds),
204
+ new Permission(eff, svc, act, undefined, otherNotResource, conds)
205
+ ];
206
+ }
207
+ if (otherResource !== undefined && thisNotResource !== undefined) {
208
+ return [
209
+ new Permission(eff, svc, act, otherResource, undefined, conds),
210
+ new Permission(eff, svc, act, undefined, thisNotResource, conds)
211
+ ];
212
+ }
213
+ // Otherwise cannot combine, return both
214
+ return [this, other];
215
+ }
216
+ /**
217
+ * Returns the intersection of this Permission with another.
218
+ * Always returns exactly one Permission. If there is no overlap,
219
+ * returns undefined.
220
+ */
221
+ intersection(other) {
222
+ // 1. Must match effect, service, and action
223
+ if (this.effect !== other.effect ||
224
+ this.service !== other.service ||
225
+ this.action !== other.action) {
226
+ // No overlap at all—return a "zero-resource" permission
227
+ return undefined;
228
+ }
229
+ if (this.resource != undefined && other.resource != undefined) {
230
+ // 2. If one includes the other, return the narrower one unless both are NotResource
231
+ if (this.includes(other)) {
232
+ return other;
233
+ }
234
+ if (other.includes(this)) {
235
+ return this;
236
+ }
237
+ }
238
+ // 3. Attempt to intersect/merge conditions
239
+ const a = normalizeConditionKeys(this.conditions || {});
240
+ const b = normalizeConditionKeys(other.conditions || {});
241
+ const allOps = Array.from(new Set([...Object.keys(a), ...Object.keys(b)]));
242
+ const intersectedConds = {};
243
+ for (const op of allOps) {
244
+ const condA = a[op] || {};
245
+ const condB = b[op] || {};
246
+ const allKeys = Array.from(new Set([...Object.keys(condA), ...Object.keys(condB)]));
247
+ intersectedConds[op] = {};
248
+ for (const key of allKeys) {
249
+ const valsA = condA[key] || [];
250
+ const valsB = condB[key] || [];
251
+ // If key appears in both, intersect or combine based on operator
252
+ if (key in condA && key in condB) {
253
+ switch (conditionBaseOperator(op)) {
254
+ case 'stringequals':
255
+ case 'stringlike':
256
+ case 'arnequals':
257
+ case 'arnlike': {
258
+ // Intersection of string lists
259
+ const common = valsA.filter((v) => valsB.includes(v));
260
+ if (common.length === 0) {
261
+ return undefined;
262
+ }
263
+ intersectedConds[op][key] = common;
264
+ break;
265
+ }
266
+ case 'stringnotequals':
267
+ case 'stringnotlike':
268
+ case 'arnnotequals':
269
+ case 'arnnotlike': {
270
+ // Union of exclusions
271
+ intersectedConds[op][key] = Array.from(new Set([...valsA, ...valsB]));
272
+ break;
273
+ }
274
+ case 'numericlessthan':
275
+ case 'numericlessthanequals': {
276
+ const numA = Number(valsA[0]);
277
+ const numB = Number(valsB[0]);
278
+ if (isNaN(numA) || isNaN(numB)) {
279
+ return undefined;
280
+ }
281
+ const boundary = Math.min(numA, numB);
282
+ intersectedConds[op][key] = [String(boundary)];
283
+ break;
284
+ }
285
+ case 'numericgreaterthan':
286
+ case 'numericgreaterthanequals': {
287
+ const ngA = Number(valsA[0]);
288
+ const ngB = Number(valsB[0]);
289
+ if (isNaN(ngA) || isNaN(ngB)) {
290
+ return undefined;
291
+ }
292
+ const boundary = Math.max(ngA, ngB);
293
+ intersectedConds[op][key] = [String(boundary)];
294
+ break;
295
+ }
296
+ case 'bool': {
297
+ if (valsA[0] !== valsB[0]) {
298
+ return undefined;
299
+ }
300
+ intersectedConds[op][key] = [valsA[0]];
301
+ break;
302
+ }
303
+ case 'ipaddress':
304
+ case 'notipaddress': {
305
+ const common = valsA.filter((cidr) => valsB.includes(cidr));
306
+ if (common.length === 0) {
307
+ return undefined;
308
+ }
309
+ intersectedConds[op][key] = common;
310
+ break;
311
+ }
312
+ case 'datelessthan':
313
+ case 'datelessthanequals': {
314
+ const dA = valsA[0];
315
+ const dB = valsB[0];
316
+ intersectedConds[op][key] = [dA < dB ? dA : dB];
317
+ break;
318
+ }
319
+ case 'dategreaterthan':
320
+ case 'dategreaterthanequals': {
321
+ const dgA = valsA[0];
322
+ const dgB = valsB[0];
323
+ intersectedConds[op][key] = [dgA > dgB ? dgA : dgB];
324
+ break;
325
+ }
326
+ default:
327
+ return undefined;
328
+ }
329
+ }
330
+ else {
331
+ // Key only in one side: carry it through
332
+ intersectedConds[op][key] = key in condA ? Array.from(valsA) : Array.from(valsB);
333
+ }
334
+ }
335
+ }
336
+ // 4. Combine resource/notResource:
337
+ const thisResource = this.resource;
338
+ const thisNotResource = this.notResource;
339
+ const otherResource = other.resource;
340
+ const otherNotResource = other.notResource;
341
+ const eff = this.effect;
342
+ const svc = this.service;
343
+ const act = this.action;
344
+ const conds = Object.keys(intersectedConds).length > 0 ? intersectedConds : undefined;
345
+ // Both have resource[] => intersect patterns
346
+ if (thisResource !== undefined && otherResource !== undefined) {
347
+ // Keep any R1 that matches something in R2, and any R2 that matches something in R1
348
+ const part1 = thisResource.filter((r1) => otherResource.some((r2) => wildcardToRegex(r1).test(r2)));
349
+ const part2 = otherResource.filter((r2) => thisResource.some((r1) => wildcardToRegex(r2).test(r1)));
350
+ const intersectR = Array.from(new Set([...part1, ...part2]));
351
+ if (intersectR.length === 0) {
352
+ return undefined;
353
+ }
354
+ return new Permission(eff, svc, act, intersectR, undefined, conds);
355
+ }
356
+ // Both have notResource[] => union of exclusions (more restrictive), but remove subsumed patterns
357
+ if (thisNotResource !== undefined && otherNotResource !== undefined) {
358
+ // Compute union of both exclusion lists
359
+ const combined = Array.from(new Set([...thisNotResource, ...otherNotResource]));
360
+ // Remove any pattern that is subsumed by a more general pattern
361
+ const filtered = combined.filter((pat) => !combined.some((otherPat) => otherPat !== pat && wildcardToRegex(otherPat).test(pat)));
362
+ return new Permission(eff, svc, act, undefined, filtered, conds);
363
+ }
364
+ // One has resource, other has notResource
365
+ const resource = thisResource || otherResource;
366
+ const notResource = thisNotResource || otherNotResource;
367
+ if (resource !== undefined || notResource !== undefined) {
368
+ const filtered = resource.filter((r1) => !notResource.some((n2) => wildcardToRegex(n2).test(r1)));
369
+ if (filtered.length === 0) {
370
+ return undefined;
371
+ }
372
+ return new Permission(eff, svc, act, filtered, undefined, conds);
373
+ }
374
+ // This should never happen
375
+ return undefined;
376
+ }
377
+ /**
378
+ * Subtract a Deny permission from this Allow permission.
379
+ * Returns an array of resulting Allow permissions (may be empty if fully denied).
380
+ */
381
+ subtract(other) {
382
+ // Only subtract Deny from Allow for the same service/action
383
+ if (this.effect !== 'Allow' ||
384
+ other.effect !== 'Deny' ||
385
+ this.service !== other.service ||
386
+ this.action !== other.action) {
387
+ // No subtraction applies
388
+ return [this];
389
+ }
390
+ // Early exit: identical conditions and deny covers allow resources => fully denied
391
+ const allowCondsNorm = normalizeConditionKeys(this.conditions || {});
392
+ const denyCondsNorm = normalizeConditionKeys(other.conditions || {});
393
+ if (JSON.stringify(allowCondsNorm) === JSON.stringify(denyCondsNorm)) {
394
+ // If both have resource[] and deny resources include all allow resources
395
+ if (this.resource && other.resource) {
396
+ if (this.resource.every((a) => other.resource.some((d) => wildcardToRegex(d).test(a)))) {
397
+ return [];
398
+ }
399
+ }
400
+ // If both have notResource[] and deny.notResource excludes superset of allow.notResource
401
+ if (this.notResource && other.notResource) {
402
+ // Deny excludes everything allow excludes or more, so allow has no effective resources
403
+ if (this.notResource.every((n) => other.notResource.includes(n))) {
404
+ return [];
405
+ }
406
+ }
407
+ }
408
+ // 1. Invert Deny conditions
409
+ const inverted = invertConditions(other.conditions || {});
410
+ // 2. Merge conditions: original Allow ∧ inverted Deny
411
+ const allowConds = normalizeConditionKeys(this.conditions || {});
412
+ const mergedConds = mergeComplementaryConditions(mergeConditions(allowConds, inverted) || {
413
+ ...allowConds,
414
+ ...inverted
415
+ });
416
+ const allowResource = this.resource;
417
+ const allowNotResource = this.notResource;
418
+ const denyResource = other.resource;
419
+ const denyNotResource = other.notResource;
420
+ const eff = this.effect;
421
+ const svc = this.service;
422
+ const act = this.action;
423
+ const conds = Object.keys(mergedConds).length ? mergedConds : undefined;
424
+ // Case: Allow.resource & Deny.resource
425
+ if (allowResource !== undefined && denyResource !== undefined) {
426
+ // If Deny has no conditions, subtract resources normally
427
+ if (!other.conditions || Object.keys(other.conditions).length === 0) {
428
+ const remaining = allowResource.filter((a) => !denyResource.some((d) => wildcardToRegex(d).test(a)));
429
+ // we cannot express the subtraction in a single statement → keep both.
430
+ const denyIsSubset = denyResource.every((d) => allowResource.some((a) => wildcardToRegex(a).test(d)));
431
+ if (denyIsSubset && remaining.length === allowResource.length) {
432
+ return [this, other];
433
+ }
434
+ if (remaining.length === 0)
435
+ return [];
436
+ return [new Permission(eff, svc, act, remaining, undefined, conds)];
437
+ }
438
+ // Deny is conditional: do not remove resources, let condition inversion handle exclusion
439
+ return [new Permission(eff, svc, act, allowResource, undefined, conds)];
440
+ }
441
+ // Case: Allow.resource & Deny.notResource --> remaining = A ∩ DNR
442
+ if (allowResource !== undefined && denyNotResource !== undefined) {
443
+ // If Deny has conditions, skip list-based subtraction and rely on conditions only
444
+ if (other.conditions && Object.keys(other.conditions).length > 0) {
445
+ return [new Permission(eff, svc, act, allowResource, undefined, conds)];
446
+ }
447
+ const remaining = allowResource.filter((a) => denyNotResource.some((dnr) => wildcardToRegex(dnr).test(a)));
448
+ if (remaining.length === 0)
449
+ return [];
450
+ return [new Permission(eff, svc, act, remaining, undefined, conds)];
451
+ }
452
+ // Case: Allow.notResource & Deny.resource
453
+ if (allowNotResource !== undefined && denyResource !== undefined) {
454
+ // If Deny is conditional, let conditions handle; keep original notResource
455
+ if (other.conditions && Object.keys(other.conditions).length > 0) {
456
+ return [new Permission(eff, svc, act, undefined, allowNotResource, conds)];
457
+ }
458
+ // Check if every Deny resource is already excluded by allowNotResource
459
+ const denyCovered = denyResource.every((dr) => allowNotResource.some((anr) => wildcardToRegex(anr).test(dr)));
460
+ if (denyCovered) {
461
+ // Deny adds no new exclusions; keep original
462
+ return [new Permission(eff, svc, act, undefined, allowNotResource, conds)];
463
+ }
464
+ // Otherwise union the exclusions
465
+ const newNot = Array.from(new Set([...allowNotResource, ...denyResource]));
466
+ return [new Permission(eff, svc, act, undefined, newNot, conds)];
467
+ }
468
+ // Case: Allow.notResource & Deny.notResource --> newNot = ANR \ DNR
469
+ if (allowNotResource !== undefined && denyNotResource !== undefined) {
470
+ // If Deny has conditions, skip list-based subtraction and rely on conditions only
471
+ if (other.conditions && Object.keys(other.conditions).length > 0) {
472
+ return [new Permission(eff, svc, act, undefined, allowNotResource, conds)];
473
+ }
474
+ const remainingNot = allowNotResource.filter((n) => !denyNotResource.some((dnr) => wildcardToRegex(dnr).test(n)));
475
+ if (remainingNot.length === 0)
476
+ return [];
477
+ return [new Permission(eff, svc, act, undefined, remainingNot, conds)];
478
+ }
479
+ // This should never happen
480
+ throw new Error('Permission.subtract: This should never happen—invalid state.');
481
+ }
482
+ }
483
+ /**
484
+ * Attempt to merge two condition‐maps. If they can be expressed as a single IAM condition block,
485
+ * return that merged block. Otherwise, return null (indicating no single‐block merger is possible).
486
+ */
487
+ function mergeConditions(a, b) {
488
+ // 1. If the set of operators in 'a' differs from the set in 'b', return null.
489
+ a = normalizeConditionKeys(a);
490
+ b = normalizeConditionKeys(b);
491
+ const opsA = Object.keys(a).sort();
492
+ const opsB = Object.keys(b).sort();
493
+ if (JSON.stringify(opsA) !== JSON.stringify(opsB)) {
494
+ return null;
495
+ }
496
+ const merged = {};
497
+ // 2. For each operator op that appears in both:
498
+ for (const op of opsA) {
499
+ const keysA = Object.keys(a[op]).sort();
500
+ const keysB = Object.keys(b[op]).sort();
501
+ // If the set of context‐keys under this operator differs, we can't merge as one block
502
+ if (JSON.stringify(keysA) !== JSON.stringify(keysB)) {
503
+ return null;
504
+ }
505
+ // Now we know op and its context keys align. Build the merged set for this operator:
506
+ merged[op] = {};
507
+ for (const key of keysA) {
508
+ const valsA = a[op][key];
509
+ const valsB = b[op][key];
510
+ // How we combine depends on operator semantics:
511
+ switch (conditionBaseOperator(op)) {
512
+ case 'stringequals':
513
+ case 'stringlike':
514
+ case 'stringnotequals':
515
+ case 'stringnotlike':
516
+ case 'arnequals':
517
+ case 'arnlike':
518
+ case 'arnnotequals':
519
+ case 'arnnotlike':
520
+ // String‐based operators: just union the value arrays
521
+ merged[op][key] = Array.from(new Set([...valsA, ...valsB]));
522
+ break;
523
+ case 'numericlessthan':
524
+ case 'numericlessthanequals':
525
+ case 'numericgreaterthan':
526
+ case 'numericgreaterthanequals':
527
+ case 'numericequals':
528
+ case 'numericnotequals':
529
+ // Numeric operators: pick the “widest” comparison that still covers both sets
530
+ // For simplicity, convert all valsA/valsB to numbers; find the min or max
531
+ const numsA = valsA.map((v) => Number(v));
532
+ const numsB = valsB.map((v) => Number(v));
533
+ if (numsA.some(isNaN) || numsB.some(isNaN)) {
534
+ // Malformed number—cannot merge
535
+ return null;
536
+ }
537
+ if (op === 'numericlessthan' || op === 'numericlessthanequals') {
538
+ // We want the largest boundary
539
+ const candidate = Math.max(...numsA, ...numsB);
540
+ merged[op][key] = [String(candidate)];
541
+ }
542
+ else if (op === 'numericgreaterthan' || op === 'numericgreaterthanequals') {
543
+ // We want the smallest boundary
544
+ const candidate = Math.min(...numsA, ...numsB);
545
+ merged[op][key] = [String(candidate)];
546
+ }
547
+ else if (op === 'numericequals' || op === 'numericnotequals') {
548
+ // Union the sets of allowed/not‐allowed numbers
549
+ merged[op][key] = Array.from(new Set([...valsA.map(String), ...valsB.map(String)]));
550
+ }
551
+ break;
552
+ case 'datelessthan':
553
+ case 'datelessthanequals':
554
+ case 'dategreaterthan':
555
+ case 'dategreaterthanequals':
556
+ // Similar idea: choose the “widest” date limit
557
+ // Assume ISO‐8601 strings so lex‐compare works
558
+ if (op === 'datelessthan' || op === 'datelessthanequals') {
559
+ // pick the LARGEST date (latest) because “< latest” covers “< earlier”
560
+ const candidate = [...valsA, ...valsB].sort().reverse()[0];
561
+ merged[op][key] = [candidate];
562
+ }
563
+ else {
564
+ // "DateGreaterThan"/"DateGreaterThanEquals": pick the EARLIEST date
565
+ const candidate = [...valsA, ...valsB].sort()[0];
566
+ merged[op][key] = [candidate];
567
+ }
568
+ break;
569
+ case 'bool':
570
+ // Typically valsA and valsB are ["true"] or ["false"].
571
+ // If either contains "true", then the union is ["true","false"]? No—
572
+ // Bool doesn't make sense with an array. In IAM, Bool only works with a single value.
573
+ // If values differ (one says ["true"], the other says ["false"]), you cannot
574
+ // express (Bool==true OR Bool==false) as a single Bool. You’d need two separate
575
+ // statements. So bail out.
576
+ if (valsA[0] === valsB[0]) {
577
+ merged[op][key] = [valsA[0]];
578
+ }
579
+ else {
580
+ return null;
581
+ }
582
+ break;
583
+ case 'ipaddress':
584
+ case 'notipaddress':
585
+ // You can pass multiple CIDR blocks under a single IpAddress. So union them.
586
+ merged[op][key] = Array.from(new Set([...valsA, ...valsB]));
587
+ break;
588
+ // Any other operators (e.g., “ArnNotLike” etc.) behave similarly to their base type
589
+ default:
590
+ // If we don’t explicitly handle the operator, reject merging
591
+ return null;
592
+ }
593
+ }
594
+ }
595
+ return merged;
596
+ }
597
+ /**
598
+ * Checks if an IAM condition operator ends with "IfExists".
599
+ *
600
+ * @param op the IAM condition operator, e.g., "StringEqualsIfExists"
601
+ * @returns true if the operator ends with "IfExists", false otherwise.
602
+ */
603
+ function isIfExists(op) {
604
+ // Check if the operator ends with "IfExists"
605
+ return op.toLowerCase().endsWith('ifexists');
606
+ }
607
+ /**
608
+ * Get the set operator from an IAM condition operator such as "ForAllValues" or "ForAnyValue".
609
+ *
610
+ * @param op the IAM condition operator, e.g., "ForAllValues:StringEquals"
611
+ * @returns the set operator, e.g., "forallvalues" or "foranyvalue", or undefined if no set operator is present.
612
+ */
613
+ function conditionSetOperator(op) {
614
+ return op.includes(':') ? op.split(':')[0].toLowerCase() : undefined;
615
+ }
616
+ /**
617
+ * Gets the base operator name from an IAM condition operator. Removes any set operator prefix or
618
+ * "IfExists" suffix.
619
+ *
620
+ * @param op the IAM condition operator, e.g., "ForAllValues:StringEqualsIfExists"
621
+ * @returns the base operator name, e.g., "stringequals" or "arnequals".
622
+ */
623
+ function conditionBaseOperator(op) {
624
+ // Return the base operator name for IAM condition operators
625
+ return op
626
+ .split(':')
627
+ .at(-1)
628
+ .toLowerCase()
629
+ .replace(/ifexists$/, '');
630
+ }
631
+ /**
632
+ * Returns a new PermissionConditions object with all operator and context keys lowercased.
633
+ */
634
+ export function normalizeConditionKeys(conds) {
635
+ const result = {};
636
+ for (const [op, keyMap] of Object.entries(conds)) {
637
+ const lowerOp = op.toLowerCase();
638
+ result[lowerOp] = {};
639
+ for (const [contextKey, values] of Object.entries(keyMap)) {
640
+ const lowerContextKey = contextKey.toLowerCase();
641
+ result[lowerOp][lowerContextKey] = Array.from(values);
642
+ }
643
+ }
644
+ return result;
645
+ }
646
+ const invertOperatorMap = {
647
+ stringequals: 'StringNotEquals',
648
+ stringlike: 'StringNotLike',
649
+ arnequals: 'ArnNotEquals',
650
+ arnlike: 'ArnNotLike',
651
+ stringnotequals: 'StringEquals',
652
+ stringnotlike: 'StringLike',
653
+ arnnotequals: 'ArnEquals',
654
+ arnnotlike: 'ArnLike',
655
+ numericlessthan: 'NumericGreaterThanEquals',
656
+ numericlessthanequals: 'NumericGreaterThan',
657
+ numericgreaterthan: 'NumericLessThanEquals',
658
+ numericgreaterthanequals: 'NumericLessThan',
659
+ numericequals: 'NumericNotEquals',
660
+ numericnotequals: 'NumericEquals',
661
+ datelessthan: 'DateGreaterThanEquals',
662
+ datelessthanequals: 'DateGreaterThan',
663
+ dategreaterthan: 'DateLessThanEquals',
664
+ dategreaterthanequals: 'DateLessThan',
665
+ bool: 'Bool',
666
+ ipaddress: 'NotIpAddress',
667
+ notipaddress: 'IpAddress'
668
+ };
669
+ const invertedSetOperatorMap = {
670
+ forallvalues: 'ForAnyValue',
671
+ foranyvalue: 'ForAllValues'
672
+ };
673
+ /**
674
+ * Invert a set of IAM condition clauses for Deny → allow inversion.
675
+ * Preserves ForAllValues:/ForAnyValue: prefixes and IfExists suffixes.
676
+ *
677
+ * @param conds the condition clauses to invert
678
+ * @return a new set of inverted conditions
679
+ */
680
+ export function invertConditions(conds) {
681
+ const normalized = normalizeConditionKeys(conds);
682
+ const inverted = {};
683
+ for (const [op, keyMap] of Object.entries(normalized)) {
684
+ const setOperator = conditionSetOperator(op) || undefined;
685
+ const setOperatorPrefix = setOperator ? invertedSetOperatorMap[setOperator] + ':' : '';
686
+ const hasIfExists = isIfExists(op);
687
+ const coreOp = conditionBaseOperator(op);
688
+ const invertedCore = invertOperatorMap[coreOp] || coreOp;
689
+ const invertedOp = `${setOperatorPrefix}${invertedCore}${hasIfExists ? 'IfExists' : ''}`.toLowerCase();
690
+ inverted[invertedOp] = {};
691
+ for (const [key, vals] of Object.entries(keyMap)) {
692
+ if (coreOp === 'bool' || coreOp === 'null') {
693
+ inverted[invertedOp][key] = vals.map((v) => (v.toLowerCase() === 'true' ? 'false' : 'true'));
694
+ }
695
+ else {
696
+ inverted[invertedOp][key] = Array.from(vals);
697
+ }
698
+ }
699
+ }
700
+ return inverted;
701
+ }
702
+ function mergeComplementaryConditions(c) {
703
+ const complement = {
704
+ stringequals: 'stringnotequals',
705
+ stringlike: 'stringnotlike',
706
+ arnequals: 'arnnotequals',
707
+ arnlike: 'arnnotlike',
708
+ numericequals: 'numericnotequals',
709
+ numericnotequals: 'numericequals',
710
+ numericlessthan: 'numericgreaterthanequals',
711
+ numericgreaterthanequals: 'numericlessthan',
712
+ numericlessthanequals: 'numericgreaterthan',
713
+ numericgreaterthan: 'numericlessthanequals',
714
+ datelessthan: 'dategreaterthanequals',
715
+ dategreaterthanequals: 'datelessthan',
716
+ datelessthanequals: 'dategreaterthan',
717
+ dategreaterthan: 'datelessthanequals',
718
+ ipaddress: 'notipaddress',
719
+ notipaddress: 'ipaddress',
720
+ bool: 'bool'
721
+ };
722
+ const out = JSON.parse(JSON.stringify(c));
723
+ for (const [base, comp] of Object.entries(complement)) {
724
+ if (out[base] && out[comp]) {
725
+ for (const key of Object.keys(out[base])) {
726
+ if (key in out[comp]) {
727
+ out[base][key] = out[base][key].filter((v) => !out[comp][key].includes(v));
728
+ delete out[comp][key];
729
+ }
730
+ }
731
+ if (out[comp] && Object.keys(out[comp]).length === 0)
732
+ delete out[comp];
733
+ }
734
+ }
735
+ return out;
736
+ }
737
+ //# sourceMappingURL=permission.js.map