@knowttl/mg-axi 0.1.1 → 0.1.3

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 (48) hide show
  1. package/README.md +251 -77
  2. package/dist/api.js +253 -8
  3. package/dist/bin/mg-axi-hook.js +31 -0
  4. package/dist/catalogue.js +693 -2
  5. package/dist/cli.js +155 -17
  6. package/dist/docs.js +175 -46
  7. package/dist/doctor.js +2 -2
  8. package/dist/entra-access-reviews.js +11 -4
  9. package/dist/entra-apps.js +171 -14
  10. package/dist/entra-audit-logs.js +101 -16
  11. package/dist/entra-auth-methods.js +8 -4
  12. package/dist/entra-certificate-auth.js +12 -8
  13. package/dist/entra-conditional-access.js +425 -15
  14. package/dist/entra-contacts.js +19 -12
  15. package/dist/entra-contracts.js +7 -4
  16. package/dist/entra-custom-security-attributes.js +19 -12
  17. package/dist/entra-data-policy-operations.js +7 -4
  18. package/dist/entra-delegated-admin.js +55 -6
  19. package/dist/entra-deleted-items.js +6 -4
  20. package/dist/entra-directory-objects.js +6 -4
  21. package/dist/entra-directory.js +19 -12
  22. package/dist/entra-domains.js +13 -8
  23. package/dist/entra-entitlement-management.js +12 -4
  24. package/dist/entra-federation-configurations.js +7 -4
  25. package/dist/entra-grants.js +8 -4
  26. package/dist/entra-group-lifecycle.js +13 -8
  27. package/dist/entra-group-pim.js +431 -0
  28. package/dist/entra-groups.js +13 -8
  29. package/dist/entra-identity-providers.js +7 -4
  30. package/dist/entra-lifecycle-workflows.js +152 -13
  31. package/dist/entra-multi-tenant-organization.js +7 -4
  32. package/dist/entra-on-premises-synchronization.js +6 -4
  33. package/dist/entra-organization.js +13 -8
  34. package/dist/entra-risk-prevention.js +442 -0
  35. package/dist/entra-risk.js +224 -9
  36. package/dist/entra-roles.js +204 -12
  37. package/dist/entra-subscriptions.js +6 -4
  38. package/dist/entra-terms-of-use.js +13 -8
  39. package/dist/entra-users.js +7 -4
  40. package/dist/graph-session.js +30 -5
  41. package/dist/hook.js +84 -0
  42. package/dist/list-totals.js +52 -0
  43. package/dist/setup.js +9 -1
  44. package/docs/coverage.md +743 -610
  45. package/inventory/operations.json +2 -2
  46. package/inventory/schema.json +1 -1
  47. package/package.json +4 -3
  48. package/skills/mg-axi/SKILL.md +86 -1
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 group lifecycle subfamily: the read mapping behind
3
4
  // `mg-axi entra group-lifecycle-policy list/show/count` and
4
5
  // `mg-axi entra group-setting-template list/show/count`. Operation
@@ -241,20 +242,22 @@ export async function listLifecyclePolicies(session, flags, profile, operation,
241
242
  if (!result.complete) {
242
243
  return {
243
244
  groupLifecyclePolicies: policies,
244
- count: { returned: policies.length, complete: false, reason: result.reason },
245
+ ...listTotals(policies.length, result.total, "lifecycle policies", false),
246
+ complete: false,
247
+ reason: result.reason,
245
248
  cursor: result.cursor,
246
249
  help: [...truncationHints, resumeHint(profileName), showHint],
247
250
  };
248
251
  }
249
- const count = { returned: policies.length, complete: true };
250
252
  if (!policies.length) {
251
253
  return {
252
254
  groupLifecyclePolicies: policies,
253
- count,
255
+ ...listTotals(policies.length, result.total, "lifecycle policies", true),
256
+ complete: true,
254
257
  help: ["0 lifecycle policies matched; the absence of results is the answer, not an error"],
255
258
  };
256
259
  }
257
- return { groupLifecyclePolicies: policies, count, help: [...truncationHints, showHint] };
260
+ return { groupLifecyclePolicies: policies, ...listTotals(policies.length, result.total, "lifecycle policies", true), complete: true, help: [...truncationHints, showHint] };
258
261
  }
259
262
  export async function showLifecyclePolicy(session, flags, profile, operation, help, profileName) {
260
263
  const { select, fields } = selectedFields(flags, KNOWN_LIFECYCLE, KNOWN_LIFECYCLE_FIELDS, DEFAULT_LIFECYCLE_SHOW_SELECT, help);
@@ -305,20 +308,22 @@ export async function listSettingTemplates(session, flags, profile, operation, h
305
308
  if (!result.complete) {
306
309
  return {
307
310
  groupSettingTemplates: templates,
308
- count: { returned: templates.length, complete: false, reason: result.reason },
311
+ ...listTotals(templates.length, result.total, "setting templates", false),
312
+ complete: false,
313
+ reason: result.reason,
309
314
  cursor: result.cursor,
310
315
  help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
311
316
  };
312
317
  }
313
- const count = { returned: templates.length, complete: true };
314
318
  if (!templates.length) {
315
319
  return {
316
320
  groupSettingTemplates: templates,
317
- count,
321
+ ...listTotals(templates.length, result.total, "setting templates", true),
322
+ complete: true,
318
323
  help: [filterNote, "0 setting templates matched; the absence of results is the answer, not an error"],
319
324
  };
320
325
  }
321
- return { groupSettingTemplates: templates, count, help: [...truncationHints, showHint, filterNote] };
326
+ return { groupSettingTemplates: templates, ...listTotals(templates.length, result.total, "setting templates", true), complete: true, help: [...truncationHints, showHint, filterNote] };
322
327
  }
323
328
  export async function showSettingTemplate(session, flags, profile, operation, help, profileName) {
324
329
  const { select, fields } = selectedFields(flags, KNOWN_TEMPLATES, KNOWN_TEMPLATE_FIELDS, DEFAULT_TEMPLATE_SHOW_SELECT, help);
@@ -0,0 +1,431 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
3
+ // EXT-02 group PIM schedule reads: the read-only schedule and instance
4
+ // mapping behind `mg-axi entra pim group-assignment-schedule list/show`,
5
+ // `mg-axi entra pim group-assignment-instance list/show`,
6
+ // `mg-axi entra pim group-eligibility-schedule list/show`,
7
+ // `mg-axi entra pim group-eligibility-instance list/show` and
8
+ // `mg-axi entra pim group-eligibility-request list/show`.
9
+ // Operation construction stays beside its command; the shared session owns
10
+ // URLs, credentials, paging, retries and error translation, and the SDK owns
11
+ // TOON rendering. This module only maps flags to session calls and projects
12
+ // rows for compact output.
13
+ //
14
+ // Reviewed against the v1.0 privilegedaccessgroup-list-assignmentschedules,
15
+ // privilegedaccessgroupassignmentschedule-get,
16
+ // privilegedaccessgroup-list-assignmentscheduleinstances,
17
+ // privilegedaccessgroupassignmentscheduleinstance-get,
18
+ // privilegedaccessgroup-list-eligibilityschedules,
19
+ // privilegedaccessgroupeligibilityschedule-get,
20
+ // privilegedaccessgroup-list-eligibilityscheduleinstances,
21
+ // privilegedaccessgroupeligibilityscheduleinstance-get,
22
+ // privilegedaccessgroup-list-eligibilityschedulerequests and
23
+ // privilegedaccessgroupeligibilityschedulerequest-get operation
24
+ // documentation and the privilegedAccessGroupAssignmentSchedule,
25
+ // privilegedAccessGroupAssignmentScheduleInstance,
26
+ // privilegedAccessGroupEligibilitySchedule,
27
+ // privilegedAccessGroupEligibilityScheduleInstance and
28
+ // privilegedAccessGroupEligibilityScheduleRequest resource contracts on
29
+ // 2026-10-06, with a second check against the Graph permissions reference
30
+ // and the ID Governance licensing fundamentals. Assignment reads take D/A
31
+ // PrivilegedAssignmentSchedule.Read.AzureADGroup; eligibility reads take
32
+ // D/A PrivilegedEligibilitySchedule.Read.AzureADGroup. Delegated callers
33
+ // additionally need owner/member of the group or a supported directory
34
+ // role (Global Reader or Privileged Role Administrator for role-assignable
35
+ // groups; Global Reader, Directory Writer, Groups Administrator, Identity
36
+ // Governance Administrator or User Administrator otherwise), scoped at
37
+ // directory level; delegated personal Microsoft accounts are not
38
+ // supported. PIM for Groups needs P2 or ID Governance.
39
+ //
40
+ // Every list requires $filter (eq) scoped to a groupId or a principalId,
41
+ // so --filter is required and must name one of them; the requirement is
42
+ // validated before credentials. Lists document $select/$filter/$expand, so
43
+ // --filter passes through as plain $filter with no $count or
44
+ // ConsistencyLevel attached; singles document $select with $expand.
45
+ // These collections document no $count contract, so totals stay honest as
46
+ // count { returned, complete } like the extended role PIM reads rather
47
+ // than the shared list-totals helper, which applies where $count=true is
48
+ // sent. Deferred with reason, never built on a write scope:
49
+ // assignmentScheduleRequests list/get take
50
+ // PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup as least privilege.
51
+ // Out of scope: approvals, filterByCurrentUser functions, nested
52
+ // group/principal/activatedUsing/targetSchedule navigation (scalar linkage
53
+ // ids such as assignmentScheduleId stay in), resources, every
54
+ // count/ref/cast tail, all mutations and beta.
55
+ // Schedules carry the full lifecycle (scheduleInfo, status, audit stamps);
56
+ // instances carry one provisioned window plus its parent schedule linkage;
57
+ // requests carry the ask (action, justification, scheduleInfo, ticketInfo)
58
+ // plus its outcome (status, approvalId, targetScheduleId).
59
+ export const KNOWN_ASSIGNMENT_SCHEDULE_FIELDS = [
60
+ "id",
61
+ "accessId",
62
+ "assignmentType",
63
+ "memberType",
64
+ "principalId",
65
+ "groupId",
66
+ "status",
67
+ "scheduleInfo",
68
+ "createdDateTime",
69
+ "modifiedDateTime",
70
+ "createdUsing",
71
+ ];
72
+ export const KNOWN_ASSIGNMENT_INSTANCE_FIELDS = [
73
+ "id",
74
+ "accessId",
75
+ "assignmentType",
76
+ "memberType",
77
+ "principalId",
78
+ "groupId",
79
+ "assignmentScheduleId",
80
+ "startDateTime",
81
+ "endDateTime",
82
+ ];
83
+ export const KNOWN_ELIGIBILITY_SCHEDULE_FIELDS = [
84
+ "id",
85
+ "accessId",
86
+ "memberType",
87
+ "principalId",
88
+ "groupId",
89
+ "status",
90
+ "scheduleInfo",
91
+ "createdDateTime",
92
+ "modifiedDateTime",
93
+ "createdUsing",
94
+ ];
95
+ export const KNOWN_ELIGIBILITY_INSTANCE_FIELDS = [
96
+ "id",
97
+ "accessId",
98
+ "memberType",
99
+ "principalId",
100
+ "groupId",
101
+ "eligibilityScheduleId",
102
+ "startDateTime",
103
+ "endDateTime",
104
+ ];
105
+ // Requests narrow to the ask plus its outcome: createdBy is an identitySet
106
+ // for a later slice and customData is documented as unused, so neither is
107
+ // selectable here. Justification is free text and rides only behind an
108
+ // explicit --select, never in a default.
109
+ export const KNOWN_ELIGIBILITY_REQUEST_FIELDS = [
110
+ "id",
111
+ "accessId",
112
+ "action",
113
+ "status",
114
+ "principalId",
115
+ "groupId",
116
+ "justification",
117
+ "scheduleInfo",
118
+ "ticketInfo",
119
+ "createdDateTime",
120
+ "completedDateTime",
121
+ "approvalId",
122
+ "targetScheduleId",
123
+ "isValidationOnly",
124
+ ];
125
+ const ASSIGNMENT_SCHEDULE_KNOWN = new Set(KNOWN_ASSIGNMENT_SCHEDULE_FIELDS);
126
+ const ASSIGNMENT_INSTANCE_KNOWN = new Set(KNOWN_ASSIGNMENT_INSTANCE_FIELDS);
127
+ const ELIGIBILITY_SCHEDULE_KNOWN = new Set(KNOWN_ELIGIBILITY_SCHEDULE_FIELDS);
128
+ const ELIGIBILITY_INSTANCE_KNOWN = new Set(KNOWN_ELIGIBILITY_INSTANCE_FIELDS);
129
+ const ELIGIBILITY_REQUEST_KNOWN = new Set(KNOWN_ELIGIBILITY_REQUEST_FIELDS);
130
+ // Compact rows: identifiers plus the correlation keys. groupId and
131
+ // principalId are the required scoping keys; accessId names member versus
132
+ // owner; assignmentType names Assigned versus Activated; memberType names
133
+ // how the row reaches the principal (for example Direct).
134
+ const DEFAULT_ASSIGNMENT_SCHEDULE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "assignmentType"];
135
+ const DEFAULT_ASSIGNMENT_INSTANCE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "assignmentType"];
136
+ const DEFAULT_ELIGIBILITY_SCHEDULE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "memberType"];
137
+ const DEFAULT_ELIGIBILITY_INSTANCE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "memberType"];
138
+ const DEFAULT_ELIGIBILITY_REQUEST_LIST_SELECT = ["id", "action", "status", "principalId", "groupId", "accessId"];
139
+ const DEFAULT_ELIGIBILITY_REQUEST_SHOW_SELECT = KNOWN_ELIGIBILITY_REQUEST_FIELDS.filter(field => field !== "justification");
140
+ export const DEFAULT_ASSIGNMENT_SCOPES = ["https://graph.microsoft.com/PrivilegedAssignmentSchedule.Read.AzureADGroup"];
141
+ export const DEFAULT_ELIGIBILITY_SCOPES = ["https://graph.microsoft.com/PrivilegedEligibilitySchedule.Read.AzureADGroup"];
142
+ const TRUNCATE_AT = 500;
143
+ function fieldList(raw, flag, known, knownList, help) {
144
+ const parts = String(raw)
145
+ .split(",")
146
+ .map(part => part.trim())
147
+ .filter(part => part.length > 0);
148
+ if (!parts.length)
149
+ throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
150
+ const fields = [];
151
+ for (const part of parts) {
152
+ if (!known.has(part)) {
153
+ throw new AxiError(`Unknown property ${part} in --${flag}`, "VALIDATION_ERROR", [
154
+ help,
155
+ `Known properties: ${knownList.join(", ")}`,
156
+ ]);
157
+ }
158
+ if (!fields.includes(part))
159
+ fields.push(part);
160
+ }
161
+ return fields;
162
+ }
163
+ function scopesFor(flags, profile, defaults, help) {
164
+ if (profile.mode === "application") {
165
+ if (flags.scopes !== undefined) {
166
+ throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
167
+ }
168
+ return undefined;
169
+ }
170
+ if (flags.scopes === undefined)
171
+ return [...defaults];
172
+ const scopes = String(flags.scopes)
173
+ .split(",")
174
+ .map(scope => scope.trim())
175
+ .filter(scope => scope.length > 0);
176
+ return scopes;
177
+ }
178
+ // Request justification is free text and scheduleInfo/ticketInfo are nested
179
+ // objects, so truncation walks values recursively; --full returns them whole.
180
+ function truncateValue(value, full) {
181
+ if (full)
182
+ return { value, truncated: false };
183
+ if (typeof value === "string" && value.length > TRUNCATE_AT) {
184
+ return { value: `${value.slice(0, TRUNCATE_AT)}... (truncated, ${value.length} chars total)`, truncated: true };
185
+ }
186
+ if (Array.isArray(value)) {
187
+ const results = value.map(item => truncateValue(item, full));
188
+ return { value: results.map(result => result.value), truncated: results.some(result => result.truncated) };
189
+ }
190
+ if (value !== null && typeof value === "object") {
191
+ let truncated = false;
192
+ const entries = Object.entries(value).map(([key, item]) => {
193
+ const result = truncateValue(item, full);
194
+ truncated = truncated || result.truncated;
195
+ return [key, result.value];
196
+ });
197
+ return { value: Object.fromEntries(entries), truncated };
198
+ }
199
+ return { value, truncated: false };
200
+ }
201
+ // Local projection preserves Graph's null/missing distinction: an explicit
202
+ // null stays null, an absent property stays absent and is never synthesized.
203
+ function project(row, fields, full) {
204
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
205
+ const projected = {};
206
+ let truncated = false;
207
+ for (const field of fields) {
208
+ if (!Object.hasOwn(source, field))
209
+ continue;
210
+ const result = truncateValue(source[field], full);
211
+ projected[field] = result.value;
212
+ truncated = truncated || result.truncated;
213
+ }
214
+ return { row: projected, truncated };
215
+ }
216
+ function selectedFields(flags, defaults, known, knownList, help) {
217
+ const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, "select", known, knownList, help);
218
+ const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, "fields", known, knownList, help);
219
+ const missing = fields.find(field => !select.includes(field));
220
+ if (missing) {
221
+ throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
222
+ }
223
+ return { select, fields };
224
+ }
225
+ function profileHint(profileName) {
226
+ return `--profile ${shellValue(profileName)}`;
227
+ }
228
+ function shellValue(value) {
229
+ return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
230
+ }
231
+ function fullHint(command, flags, profileName) {
232
+ const args = Object.entries({ ...flags, profile: profileName, full: true })
233
+ .map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
234
+ return `mg-axi ${command} ${args.join(" ")}`;
235
+ }
236
+ // Denials carry the session's generic grant/role/licence cause; each
237
+ // family adds the scope, roles and licensing that actually unlock it,
238
+ // because a 403 alone never says which prerequisite is missing.
239
+ function withGuidance(hints, run) {
240
+ return run().catch(error => {
241
+ if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
242
+ throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
243
+ }
244
+ throw error;
245
+ });
246
+ }
247
+ const ASSIGNMENT_DENIAL_HINTS = [
248
+ "Group PIM assignment reads need PrivilegedAssignmentSchedule.Read.AzureADGroup plus owner/member of the group or a supported directory role (Global Reader or Privileged Role Administrator for role-assignable groups; Global Reader, Directory Writer, Groups Administrator, Identity Governance Administrator or User Administrator otherwise, scoped at directory level) for delegated access, or admin-consented PrivilegedAssignmentSchedule.Read.AzureADGroup for application access; delegated personal Microsoft accounts are not supported",
249
+ "PIM for Groups needs P2 or ID Governance, not only P2",
250
+ ];
251
+ const ELIGIBILITY_DENIAL_HINTS = [
252
+ "Group PIM eligibility reads need PrivilegedEligibilitySchedule.Read.AzureADGroup plus owner/member of the group or a supported directory role (Global Reader or Privileged Role Administrator for role-assignable groups; Global Reader, Directory Writer, Groups Administrator, Identity Governance Administrator or User Administrator otherwise, scoped at directory level) for delegated access, or admin-consented PrivilegedEligibilitySchedule.Read.AzureADGroup for application access; delegated personal Microsoft accounts are not supported",
253
+ "PIM for Groups needs P2 or ID Governance, not only P2",
254
+ ];
255
+ const ASSIGNMENT_SCHEDULE_LIST = {
256
+ command: "entra pim group-assignment-schedule list",
257
+ key: "assignmentSchedules",
258
+ noun: "assignment schedules",
259
+ known: ASSIGNMENT_SCHEDULE_KNOWN,
260
+ knownList: KNOWN_ASSIGNMENT_SCHEDULE_FIELDS,
261
+ defaultSelect: DEFAULT_ASSIGNMENT_SCHEDULE_LIST_SELECT,
262
+ defaultScopes: DEFAULT_ASSIGNMENT_SCOPES,
263
+ denialHints: ASSIGNMENT_DENIAL_HINTS,
264
+ scopeNote: "Assignment schedules govern group membership or ownership over time; instances are the provisioned windows, never the schedule itself",
265
+ emptyNote: "0 assignment schedules matched; the absence of results is the answer, not an error",
266
+ };
267
+ const ASSIGNMENT_INSTANCE_LIST = {
268
+ command: "entra pim group-assignment-instance list",
269
+ key: "assignmentScheduleInstances",
270
+ noun: "assignment instances",
271
+ known: ASSIGNMENT_INSTANCE_KNOWN,
272
+ knownList: KNOWN_ASSIGNMENT_INSTANCE_FIELDS,
273
+ defaultSelect: DEFAULT_ASSIGNMENT_INSTANCE_LIST_SELECT,
274
+ defaultScopes: DEFAULT_ASSIGNMENT_SCOPES,
275
+ denialHints: ASSIGNMENT_DENIAL_HINTS,
276
+ scopeNote: "Assignment instances are provisioned membership or ownership windows; assignmentType Assigned names direct grants and Activated names activated eligible grants",
277
+ emptyNote: "0 assignment instances matched; the absence of results is the answer, not an error",
278
+ };
279
+ const ELIGIBILITY_SCHEDULE_LIST = {
280
+ command: "entra pim group-eligibility-schedule list",
281
+ key: "eligibilitySchedules",
282
+ noun: "eligibility schedules",
283
+ known: ELIGIBILITY_SCHEDULE_KNOWN,
284
+ knownList: KNOWN_ELIGIBILITY_SCHEDULE_FIELDS,
285
+ defaultSelect: DEFAULT_ELIGIBILITY_SCHEDULE_LIST_SELECT,
286
+ defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
287
+ denialHints: ELIGIBILITY_DENIAL_HINTS,
288
+ scopeNote: "Eligibility schedules govern who may activate group membership or ownership; they are not active grants",
289
+ emptyNote: "0 eligibility schedules matched; the absence of results is the answer, not an error",
290
+ };
291
+ const ELIGIBILITY_INSTANCE_LIST = {
292
+ command: "entra pim group-eligibility-instance list",
293
+ key: "eligibilityScheduleInstances",
294
+ noun: "eligibility instances",
295
+ known: ELIGIBILITY_INSTANCE_KNOWN,
296
+ knownList: KNOWN_ELIGIBILITY_INSTANCE_FIELDS,
297
+ defaultSelect: DEFAULT_ELIGIBILITY_INSTANCE_LIST_SELECT,
298
+ defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
299
+ denialHints: ELIGIBILITY_DENIAL_HINTS,
300
+ scopeNote: "Eligibility instances are provisioned eligibility windows; activation itself is a PIM workflow outside these reads",
301
+ emptyNote: "0 eligibility instances matched; the absence of results is the answer, not an error",
302
+ };
303
+ const ELIGIBILITY_REQUEST_LIST = {
304
+ command: "entra pim group-eligibility-request list",
305
+ key: "eligibilityScheduleRequests",
306
+ noun: "eligibility requests",
307
+ known: ELIGIBILITY_REQUEST_KNOWN,
308
+ knownList: KNOWN_ELIGIBILITY_REQUEST_FIELDS,
309
+ defaultSelect: DEFAULT_ELIGIBILITY_REQUEST_LIST_SELECT,
310
+ defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
311
+ denialHints: ELIGIBILITY_DENIAL_HINTS,
312
+ scopeNote: "Eligibility requests carry the ask and its outcome; justification text rides only behind an explicit --select",
313
+ emptyNote: "0 eligibility requests matched; the absence of results is the answer, not an error",
314
+ };
315
+ // Graph requires every one of these lists to be scoped to a groupId or a
316
+ // principalId, so --filter must name one of them; validated before any
317
+ // credential or transport is touched, with cursor resumes replaying the
318
+ // stored filter.
319
+ function requiredFilter(flags, savedFilter, help) {
320
+ const filter = flags.filter === undefined ? savedFilter : String(flags.filter);
321
+ if (filter === undefined || (!filter.includes("groupId") && !filter.includes("principalId"))) {
322
+ throw new AxiError("--filter needs a groupId or principalId eq clause scoping the list", "VALIDATION_ERROR", [
323
+ help,
324
+ "For example --filter \"groupId eq '<group-id>'\" or --filter \"principalId eq '<principal-id>'\"",
325
+ ]);
326
+ }
327
+ return filter;
328
+ }
329
+ async function listCollection(shape, session, flags, profile, operation, help, profileName) {
330
+ const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
331
+ if (cursor !== undefined && !cursor.trim())
332
+ throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
333
+ const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
334
+ const savedSelect = saved?.$select;
335
+ const { select, fields } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
336
+ const filter = requiredFilter(flags, saved?.$filter, help);
337
+ const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
338
+ const full = flags.full === true;
339
+ // These lists document $select/$filter/$expand, so --filter passes
340
+ // through as plain $filter with no $count or ConsistencyLevel attached;
341
+ // $expand stays unreviewed and is never sent.
342
+ const query = { $select: select.join(","), $filter: filter };
343
+ const args = { profile, operation, query, scopes };
344
+ if (cursor !== undefined)
345
+ args.cursor = cursor;
346
+ if (flags.all === true) {
347
+ if (flags.limit !== undefined)
348
+ throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
349
+ }
350
+ else {
351
+ args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
352
+ }
353
+ const result = await withGuidance(shape.denialHints, () => session.collect(args));
354
+ const effectiveFlags = { ...flags, select: result.query.$select ?? shape.defaultSelect.join(",") };
355
+ if (result.query.$filter !== undefined)
356
+ effectiveFlags.filter = result.query.$filter;
357
+ const rows = [];
358
+ let truncated = false;
359
+ for (const row of result.value) {
360
+ const projected = project(row, fields, full);
361
+ rows.push(projected.row);
362
+ truncated = truncated || projected.truncated;
363
+ }
364
+ const truncationHints = truncated ? [fullHint(shape.command, effectiveFlags, profileName)] : [];
365
+ const standing = [shape.scopeNote];
366
+ if (!result.complete) {
367
+ return {
368
+ [shape.key]: rows,
369
+ ...listTotals(rows.length, result.total, shape.noun, false),
370
+ complete: false,
371
+ reason: result.reason,
372
+ cursor: result.cursor,
373
+ help: [...truncationHints, `Resume losslessly with the same --filter and flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
374
+ };
375
+ }
376
+ if (!rows.length) {
377
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
378
+ }
379
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
380
+ }
381
+ async function showOne(key, command, known, knownList, defaultSelect, defaultScopes, denialHints, session, flags, profile, operation, help, profileName, params) {
382
+ const { select, fields } = selectedFields(flags, defaultSelect, known, knownList, help);
383
+ const scopes = scopesFor(flags, profile, defaultScopes, help);
384
+ const full = flags.full === true;
385
+ const raw = await withGuidance(denialHints, () => session.execute({
386
+ profile,
387
+ operation,
388
+ params,
389
+ query: { $select: select.join(",") },
390
+ scopes,
391
+ }));
392
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
393
+ throw new AxiError(`Graph returned a malformed ${key} body`, "GRAPH_ERROR", [
394
+ "Single-object reads carry one object; treat anything else as unknown, not empty",
395
+ ]);
396
+ }
397
+ const { row, truncated } = project(raw, fields, full);
398
+ if (truncated)
399
+ return { [key]: row, help: [fullHint(command, flags, profileName)] };
400
+ return { [key]: row };
401
+ }
402
+ export async function listGroupAssignmentSchedules(session, flags, profile, operation, help, profileName) {
403
+ return listCollection(ASSIGNMENT_SCHEDULE_LIST, session, flags, profile, operation, help, profileName);
404
+ }
405
+ export async function showGroupAssignmentSchedule(session, flags, profile, operation, help, profileName) {
406
+ return showOne("assignmentSchedule", "entra pim group-assignment-schedule show", ASSIGNMENT_SCHEDULE_KNOWN, KNOWN_ASSIGNMENT_SCHEDULE_FIELDS, [...KNOWN_ASSIGNMENT_SCHEDULE_FIELDS], DEFAULT_ASSIGNMENT_SCOPES, ASSIGNMENT_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupAssignmentSchedule-id": String(flags.id) });
407
+ }
408
+ export async function listGroupAssignmentInstances(session, flags, profile, operation, help, profileName) {
409
+ return listCollection(ASSIGNMENT_INSTANCE_LIST, session, flags, profile, operation, help, profileName);
410
+ }
411
+ export async function showGroupAssignmentInstance(session, flags, profile, operation, help, profileName) {
412
+ return showOne("assignmentScheduleInstance", "entra pim group-assignment-instance show", ASSIGNMENT_INSTANCE_KNOWN, KNOWN_ASSIGNMENT_INSTANCE_FIELDS, [...KNOWN_ASSIGNMENT_INSTANCE_FIELDS], DEFAULT_ASSIGNMENT_SCOPES, ASSIGNMENT_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupAssignmentScheduleInstance-id": String(flags.id) });
413
+ }
414
+ export async function listGroupEligibilitySchedules(session, flags, profile, operation, help, profileName) {
415
+ return listCollection(ELIGIBILITY_SCHEDULE_LIST, session, flags, profile, operation, help, profileName);
416
+ }
417
+ export async function showGroupEligibilitySchedule(session, flags, profile, operation, help, profileName) {
418
+ return showOne("eligibilitySchedule", "entra pim group-eligibility-schedule show", ELIGIBILITY_SCHEDULE_KNOWN, KNOWN_ELIGIBILITY_SCHEDULE_FIELDS, [...KNOWN_ELIGIBILITY_SCHEDULE_FIELDS], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilitySchedule-id": String(flags.id) });
419
+ }
420
+ export async function listGroupEligibilityInstances(session, flags, profile, operation, help, profileName) {
421
+ return listCollection(ELIGIBILITY_INSTANCE_LIST, session, flags, profile, operation, help, profileName);
422
+ }
423
+ export async function showGroupEligibilityInstance(session, flags, profile, operation, help, profileName) {
424
+ return showOne("eligibilityScheduleInstance", "entra pim group-eligibility-instance show", ELIGIBILITY_INSTANCE_KNOWN, KNOWN_ELIGIBILITY_INSTANCE_FIELDS, [...KNOWN_ELIGIBILITY_INSTANCE_FIELDS], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilityScheduleInstance-id": String(flags.id) });
425
+ }
426
+ export async function listGroupEligibilityRequests(session, flags, profile, operation, help, profileName) {
427
+ return listCollection(ELIGIBILITY_REQUEST_LIST, session, flags, profile, operation, help, profileName);
428
+ }
429
+ export async function showGroupEligibilityRequest(session, flags, profile, operation, help, profileName) {
430
+ return showOne("eligibilityScheduleRequest", "entra pim group-eligibility-request show", ELIGIBILITY_REQUEST_KNOWN, KNOWN_ELIGIBILITY_REQUEST_FIELDS, [...DEFAULT_ELIGIBILITY_REQUEST_SHOW_SELECT], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilityScheduleRequest-id": String(flags.id) });
431
+ }
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // READ-02: the Entra group read mapping behind `mg-axi entra group list/show`
3
4
  // and `mg-axi entra group member list` / `member-of list` (each with an
4
5
  // opt-in `--transitive` flat view). Operation construction stays beside its
@@ -226,23 +227,25 @@ export async function listGroups(session, flags, profile, operation, help, profi
226
227
  if (!result.complete) {
227
228
  return {
228
229
  groups,
229
- count: { returned: groups.length, complete: false, reason: result.reason },
230
+ ...listTotals(groups.length, result.total, "groups", false),
231
+ complete: false,
232
+ reason: result.reason,
230
233
  cursor: result.cursor,
231
234
  help: [...truncationHints, resumeHint(profileName), showHint],
232
235
  };
233
236
  }
234
- const count = { returned: groups.length, complete: true };
235
237
  if (!groups.length) {
236
238
  return {
237
239
  groups,
238
- count,
240
+ ...listTotals(groups.length, result.total, "groups", true),
241
+ complete: true,
239
242
  help: [
240
243
  `mg-axi entra group list --filter <odata-filter> ${profileHint(profileName)}`,
241
244
  "0 groups matched; the absence of results is the answer, not an error",
242
245
  ],
243
246
  };
244
247
  }
245
- return { groups, count, help: [...truncationHints, showHint] };
248
+ return { groups, ...listTotals(groups.length, result.total, "groups", true), complete: true, help: [...truncationHints, showHint] };
246
249
  }
247
250
  export async function showGroup(session, flags, profile, operation, help, profileName) {
248
251
  const { select, fields } = selectedFields(flags, KNOWN_GROUPS, KNOWN_GROUP_FIELDS, DEFAULT_SHOW_SELECT, help);
@@ -302,17 +305,19 @@ async function listRelationship(session, flags, profile, operation, help, profil
302
305
  if (!result.complete) {
303
306
  return {
304
307
  [key]: rows,
305
- count: { returned: rows.length, complete: false, reason: result.reason },
308
+ ...listTotals(rows.length, result.total, emptyNoun, false),
309
+ complete: false,
310
+ reason: result.reason,
306
311
  cursor: result.cursor,
307
312
  ...warnings,
308
313
  help: [...truncationHints, ...limitedHints, hiddenHint, resumeHint(profileName), scopeHint],
309
314
  };
310
315
  }
311
- const count = { returned: rows.length, complete: true };
312
316
  if (!rows.length) {
313
317
  return {
314
318
  [key]: rows,
315
- count,
319
+ ...listTotals(rows.length, result.total, emptyNoun, true),
320
+ complete: true,
316
321
  ...warnings,
317
322
  help: [
318
323
  ...limitedHints,
@@ -322,7 +327,7 @@ async function listRelationship(session, flags, profile, operation, help, profil
322
327
  ],
323
328
  };
324
329
  }
325
- return { [key]: rows, count, ...warnings, help: [...truncationHints, ...limitedHints, hiddenHint, scopeHint] };
330
+ return { [key]: rows, ...listTotals(rows.length, result.total, emptyNoun, true), complete: true, ...warnings, help: [...truncationHints, ...limitedHints, hiddenHint, scopeHint] };
326
331
  }
327
332
  export async function listGroupMembers(session, flags, profile, operation, help, profileName) {
328
333
  const transitive = flags.transitive === true;
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-03 workforce identity-providers subfamily: the read mapping behind
3
4
  // `mg-axi entra identity-provider list/show/count/available-types`. Operation construction
4
5
  // stays beside its command; the shared session owns URLs, credentials,
@@ -217,20 +218,22 @@ export async function listIdentityProviders(session, flags, profile, operation,
217
218
  if (!result.complete) {
218
219
  return {
219
220
  identityProviders: providers,
220
- count: { returned: providers.length, complete: false, reason: result.reason },
221
+ ...listTotals(providers.length, result.total, "identity providers", false),
222
+ complete: false,
223
+ reason: result.reason,
221
224
  cursor: result.cursor,
222
225
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, showHint, WORKFORCE_NOTE],
223
226
  };
224
227
  }
225
- const count = { returned: providers.length, complete: true };
226
228
  if (!providers.length) {
227
229
  return {
228
230
  identityProviders: providers,
229
- count,
231
+ ...listTotals(providers.length, result.total, "identity providers", true),
232
+ complete: true,
230
233
  help: [WORKFORCE_NOTE, "0 identity providers matched; the absence of results is the answer, not an error"],
231
234
  };
232
235
  }
233
- return { identityProviders: providers, count, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
236
+ return { identityProviders: providers, ...listTotals(providers.length, result.total, "identity providers", true), complete: true, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
234
237
  }
235
238
  export async function showIdentityProvider(session, flags, profile, operation, help, profileName) {
236
239
  const { select, fields } = selectedFields(flags, DEFAULT_SHOW_SELECT, help);