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