@knowttl/mg-axi 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +1138 -0
  2. package/dist/api.js +953 -0
  3. package/dist/app-auth.js +47 -0
  4. package/dist/auth.js +59 -0
  5. package/dist/bin/mg-axi.js +7 -0
  6. package/dist/catalogue.js +2191 -0
  7. package/dist/cli.js +1314 -0
  8. package/dist/docs.js +559 -0
  9. package/dist/doctor.js +75 -0
  10. package/dist/entra-access-reviews.js +425 -0
  11. package/dist/entra-apps.js +374 -0
  12. package/dist/entra-audit-logs.js +381 -0
  13. package/dist/entra-auth-methods.js +296 -0
  14. package/dist/entra-ca-policy-update.js +353 -0
  15. package/dist/entra-certificate-auth.js +384 -0
  16. package/dist/entra-conditional-access.js +317 -0
  17. package/dist/entra-contacts.js +827 -0
  18. package/dist/entra-contracts.js +252 -0
  19. package/dist/entra-custom-security-attributes.js +464 -0
  20. package/dist/entra-data-policy-operations.js +274 -0
  21. package/dist/entra-delegated-admin.js +468 -0
  22. package/dist/entra-deleted-items.js +461 -0
  23. package/dist/entra-directory-objects.js +274 -0
  24. package/dist/entra-directory.js +411 -0
  25. package/dist/entra-domains.js +399 -0
  26. package/dist/entra-entitlement-management.js +645 -0
  27. package/dist/entra-federation-configurations.js +290 -0
  28. package/dist/entra-grants.js +272 -0
  29. package/dist/entra-group-lifecycle.js +349 -0
  30. package/dist/entra-group-member-add.js +278 -0
  31. package/dist/entra-groups.js +336 -0
  32. package/dist/entra-identity-providers.js +297 -0
  33. package/dist/entra-lifecycle-workflows.js +673 -0
  34. package/dist/entra-multi-tenant-organization.js +309 -0
  35. package/dist/entra-on-premises-synchronization.js +231 -0
  36. package/dist/entra-organization.js +398 -0
  37. package/dist/entra-risk-dismiss.js +191 -0
  38. package/dist/entra-risk.js +404 -0
  39. package/dist/entra-roles.js +310 -0
  40. package/dist/entra-subscriptions.js +289 -0
  41. package/dist/entra-tenant-information.js +152 -0
  42. package/dist/entra-terms-of-use.js +380 -0
  43. package/dist/entra-user-revoke-sessions.js +170 -0
  44. package/dist/entra-user-update.js +170 -0
  45. package/dist/entra-users.js +211 -0
  46. package/dist/graph-session.js +1034 -0
  47. package/dist/msal-app-provider.js +42 -0
  48. package/dist/msal-provider.js +82 -0
  49. package/dist/mutations.js +529 -0
  50. package/dist/profiles.js +139 -0
  51. package/dist/setup.js +116 -0
  52. package/dist/version.js +2 -0
  53. package/docs/coverage.md +1028 -0
  54. package/inventory/operations.json +4 -0
  55. package/inventory/schema.json +71 -0
  56. package/package.json +32 -0
  57. package/skills/mg-axi/SKILL.md +293 -0
@@ -0,0 +1,411 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ // READ-10: the directory-device and administrative-unit read mapping behind
3
+ // `mg-axi entra device list/show`, `mg-axi entra administrative-unit
4
+ // list/show` and `mg-axi entra administrative-unit member list`. Operation
5
+ // construction stays beside its command; the shared session owns URLs,
6
+ // credentials, paging, retries and error translation, and the SDK owns TOON
7
+ // rendering. This module only maps flags to session calls and projects rows
8
+ // for compact output.
9
+ //
10
+ // Reviewed against the v1.0 device-list, device-get,
11
+ // directory-list-administrativeunits and administrativeunit-list-members
12
+ // operation documentation on 2026-10-04. Device list/get read D/A
13
+ // Device.Read.All; delegated callers additionally need a supported directory
14
+ // role (for example Global Reader, Security Reader or Device Managers).
15
+ // Administrative-unit list and member reads take D/A
16
+ // AdministrativeUnit.Read.All; delegated list callers need a member-user
17
+ // account or a supported role with Privileged Role Administrator the
18
+ // least-privileged role, while member reads distinguish Directory Readers
19
+ // (basic properties and members) from Global Reader (all properties
20
+ // including members). Hidden AU memberships additionally need
21
+ // Member.Read.Hidden and the server omits what the caller cannot see rather
22
+ // than failing. Application callers with narrow consent receive
23
+ // limited-information rows carrying only @odata.type and id with other
24
+ // properties null; those rows are preserved, never reinterpreted as empty.
25
+ // Directory devices are Entra directory objects: Intune managed devices and
26
+ // device actions are a separately authorized surface and never this command.
27
+ // Base device reads carry no P1/P2 prerequisite; scoped AU administration
28
+ // needs P1, members are Free, and dynamic membership needs additional P1
29
+ // licensing. No device or AU mutation exists in this slice.
30
+ // Every device property this slice may request or display, matching the
31
+ // reviewed raw surface. Anything else fails before credentials.
32
+ export const KNOWN_DEVICE_FIELDS = [
33
+ "id",
34
+ "deviceId",
35
+ "displayName",
36
+ "operatingSystem",
37
+ "operatingSystemVersion",
38
+ "trustType",
39
+ "isCompliant",
40
+ "isManaged",
41
+ "accountEnabled",
42
+ "createdDateTime",
43
+ "approximateLastSignInDateTime",
44
+ "manufacturer",
45
+ "model",
46
+ ];
47
+ const KNOWN_DEVICES = new Set(KNOWN_DEVICE_FIELDS);
48
+ // Every administrative-unit property this slice may request or display,
49
+ // matching the reviewed raw surface. Anything else fails before credentials.
50
+ export const KNOWN_AU_FIELDS = [
51
+ "id",
52
+ "displayName",
53
+ "description",
54
+ "visibility",
55
+ "membershipType",
56
+ "membershipRule",
57
+ ];
58
+ const KNOWN_AUS = new Set(KNOWN_AU_FIELDS);
59
+ // AU members are directoryObjects of mixed types (users, groups, devices);
60
+ // richer per-type fields need single-object reads, so only these stay
61
+ // selectable.
62
+ export const KNOWN_AU_MEMBER_FIELDS = ["id", "displayName", "mail"];
63
+ const KNOWN_AU_MEMBERS = new Set(KNOWN_AU_MEMBER_FIELDS);
64
+ // @odata.type is preserved on member rows without being selectable: it names
65
+ // the member kind and marks limited-information rows.
66
+ const MEMBER_TYPE_PROPERTY = "@odata.type";
67
+ // Compact device rows: identifier, display name, platform and enabled state.
68
+ const DEFAULT_DEVICE_LIST_SELECT = ["id", "displayName", "operatingSystem", "accountEnabled"];
69
+ // Show rows: the full reviewed device set, including the directory deviceId
70
+ // that is distinct from the object id.
71
+ const DEFAULT_DEVICE_SHOW_SELECT = [...KNOWN_DEVICE_FIELDS];
72
+ // Compact AU rows: identifier, display name, scope visibility and kind.
73
+ const DEFAULT_AU_LIST_SELECT = ["id", "displayName", "visibility", "membershipType"];
74
+ // Show rows: the full reviewed AU set, including the membership rule.
75
+ const DEFAULT_AU_SHOW_SELECT = [...KNOWN_AU_FIELDS];
76
+ // Compact member rows: identifier and display name.
77
+ const DEFAULT_AU_MEMBER_SELECT = ["id", "displayName"];
78
+ // Delegated defaults are operation-specific; hidden AU members need an
79
+ // explicit --scopes set including Member.Read.Hidden. Application profiles
80
+ // use their configured .default audience and reject --scopes.
81
+ export const DEFAULT_DEVICE_SCOPES = ["https://graph.microsoft.com/Device.Read.All"];
82
+ export const DEFAULT_AU_SCOPES = ["https://graph.microsoft.com/AdministrativeUnit.Read.All"];
83
+ const TRUNCATE_AT = 500;
84
+ function fieldList(raw, known, knownList, flag, help) {
85
+ const parts = String(raw)
86
+ .split(",")
87
+ .map(part => part.trim())
88
+ .filter(part => part.length > 0);
89
+ if (!parts.length)
90
+ throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
91
+ const fields = [];
92
+ for (const part of parts) {
93
+ if (!known.has(part)) {
94
+ throw new AxiError(`Unknown directory property ${part} in --${flag}`, "VALIDATION_ERROR", [
95
+ help,
96
+ `Known properties: ${knownList.join(", ")}`,
97
+ ]);
98
+ }
99
+ if (!fields.includes(part))
100
+ fields.push(part);
101
+ }
102
+ return fields;
103
+ }
104
+ function scopesFor(flags, defaults, profile, help) {
105
+ if (profile.mode === "application") {
106
+ if (flags.scopes !== undefined) {
107
+ throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
108
+ }
109
+ return undefined;
110
+ }
111
+ if (flags.scopes === undefined)
112
+ return [...defaults];
113
+ const scopes = String(flags.scopes)
114
+ .split(",")
115
+ .map(scope => scope.trim())
116
+ .filter(scope => scope.length > 0);
117
+ return scopes;
118
+ }
119
+ function truncateValue(value, full) {
120
+ const truncate = (text) => full || text.length <= TRUNCATE_AT
121
+ ? { value: text, truncated: false }
122
+ : { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
123
+ if (typeof value === "string")
124
+ return truncate(value);
125
+ if (Array.isArray(value) && value.every(entry => typeof entry === "string")) {
126
+ const projected = value.map(truncate);
127
+ return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
128
+ }
129
+ return { value, truncated: false };
130
+ }
131
+ // Local projection preserves Graph's null/missing distinction: an explicit
132
+ // null stays null, an absent property stays absent and is never synthesized.
133
+ function project(row, fields, full) {
134
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
135
+ const projected = {};
136
+ let truncated = false;
137
+ for (const field of fields) {
138
+ if (!Object.hasOwn(source, field))
139
+ continue;
140
+ const result = truncateValue(source[field], full);
141
+ projected[field] = result.value;
142
+ truncated = truncated || result.truncated;
143
+ }
144
+ return { row: projected, truncated };
145
+ }
146
+ // Member projection additionally preserves @odata.type so the member kind
147
+ // and limited-information rows survive local projection.
148
+ function projectMember(row, select, fields, full) {
149
+ const { row: projected, truncated } = project(row, fields, full);
150
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
151
+ if (typeof source[MEMBER_TYPE_PROPERTY] === "string")
152
+ projected[MEMBER_TYPE_PROPERTY] = source[MEMBER_TYPE_PROPERTY];
153
+ const descriptive = [...new Set([...select, ...Object.keys(source)])]
154
+ .filter(field => field !== "id" && !field.startsWith("@"));
155
+ const limitedInfo = Object.hasOwn(source, "id") && descriptive.length > 0
156
+ && descriptive.every(field => source[field] === null || source[field] === undefined || !Object.hasOwn(source, field));
157
+ return { row: projected, truncated, limitedInfo };
158
+ }
159
+ function selectedFields(flags, known, knownList, defaults, help) {
160
+ const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, known, knownList, "select", help);
161
+ const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, known, knownList, "fields", help);
162
+ const missing = fields.find(field => !select.includes(field));
163
+ if (missing) {
164
+ throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
165
+ }
166
+ return { select, fields };
167
+ }
168
+ function profileHint(profileName) {
169
+ return `--profile ${shellValue(profileName)}`;
170
+ }
171
+ function shellValue(value) {
172
+ return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
173
+ }
174
+ function fullHint(command, flags, profileName) {
175
+ const args = Object.entries({ ...flags, profile: profileName, full: true })
176
+ .map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
177
+ return `mg-axi ${command} ${args.join(" ")}`;
178
+ }
179
+ // Denials carry the session's generic grant/role/licence cause; each
180
+ // operation adds the scope, roles and licensing that actually unlock it,
181
+ // because a 403 alone never says which prerequisite is missing.
182
+ function withGuidance(hints, run) {
183
+ return run().catch(error => {
184
+ if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
185
+ throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
186
+ }
187
+ throw error;
188
+ });
189
+ }
190
+ const DEVICE_DENIAL_HINTS = [
191
+ "Device reads need Device.Read.All plus a supported directory role for delegated access (for example Global Reader, Security Reader or Device Managers), or admin-consented Device.Read.All for application access",
192
+ "Directory devices are Entra directory objects; Intune managed devices and device actions are a separately authorized surface, never this command",
193
+ "Base device reads carry no P1/P2 prerequisite; never diagnose licence solely from HTTP 403",
194
+ ];
195
+ const AU_LIST_DENIAL_HINTS = [
196
+ "Administrative-unit lists need AdministrativeUnit.Read.All plus a member-user account or a supported directory role for delegated access (Privileged Role Administrator is the least-privileged role), or admin-consented AdministrativeUnit.Read.All for application access",
197
+ "Scoped administration needs P1, members are Free, and dynamic membership needs additional P1 licensing; never diagnose licence solely from HTTP 403",
198
+ ];
199
+ const AU_SHOW_DENIAL_HINTS = [
200
+ "Administrative-unit reads need AdministrativeUnit.Read.All plus a member-user account or a supported directory role for delegated access (Directory Readers for basic properties, Global Reader for all properties), or admin-consented AdministrativeUnit.Read.All for application access",
201
+ "Scoped administration needs P1, members are Free, and dynamic membership needs additional P1 licensing; never diagnose licence solely from HTTP 403",
202
+ ];
203
+ const AU_MEMBER_DENIAL_HINTS = [
204
+ "Administrative-unit member reads need AdministrativeUnit.Read.All (Directory Readers for basic properties and members, Global Reader for all properties including members), or admin-consented AdministrativeUnit.Read.All for application access",
205
+ "Hidden memberships additionally need Member.Read.Hidden; the server omits what the caller cannot see rather than failing",
206
+ "Scoped administration needs P1, members are Free, and dynamic membership needs additional P1 licensing; never diagnose licence solely from HTTP 403",
207
+ ];
208
+ // $filter on directory collections runs through advanced query: an effective
209
+ // filter always carries $count=true with ConsistencyLevel eventual.
210
+ // Restoring the saved filter keeps cursor resumes lossless when --filter is
211
+ // omitted.
212
+ function collectionCommon(session, flags, known, knownList, defaults, defaultScopes, operation, help, profile) {
213
+ const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
214
+ if (cursor !== undefined && !cursor.trim())
215
+ throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
216
+ const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
217
+ const { select, fields } = selectedFields(flags, known, knownList, saved?.["$select"] === undefined ? defaults : fieldList(saved["$select"], known, knownList, "select", help), help);
218
+ const filter = flags.filter === undefined ? saved?.["$filter"] : String(flags.filter);
219
+ return { cursor, select, fields, scopes: scopesFor(flags, defaultScopes, profile, help), full: flags.full === true, filter };
220
+ }
221
+ function collectArgs(profile, operation, common, flags, help) {
222
+ const query = { $select: common.select.join(",") };
223
+ if (common.filter !== undefined) {
224
+ query.$filter = common.filter;
225
+ query.$count = "true";
226
+ }
227
+ const args = { profile, operation, query, scopes: common.scopes };
228
+ if (common.filter !== undefined)
229
+ args.consistencyLevel = "eventual";
230
+ if (common.cursor !== undefined)
231
+ args.cursor = common.cursor;
232
+ if (flags.all === true) {
233
+ if (flags.limit !== undefined)
234
+ throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
235
+ }
236
+ else {
237
+ args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
238
+ }
239
+ return args;
240
+ }
241
+ function resumeHint(profileName) {
242
+ return `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`;
243
+ }
244
+ export async function listDevices(session, flags, profile, operation, help, profileName) {
245
+ const common = collectionCommon(session, flags, KNOWN_DEVICES, KNOWN_DEVICE_FIELDS, DEFAULT_DEVICE_LIST_SELECT, DEFAULT_DEVICE_SCOPES, operation, help, profile);
246
+ const result = await withGuidance(DEVICE_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
247
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_DEVICE_LIST_SELECT.join(",") };
248
+ if (result.query.$filter !== undefined)
249
+ effectiveFlags.filter = result.query.$filter;
250
+ const devices = [];
251
+ let truncated = false;
252
+ for (const row of result.value) {
253
+ const projected = project(row, common.fields, common.full);
254
+ devices.push(projected.row);
255
+ truncated = truncated || projected.truncated;
256
+ }
257
+ const showHint = `mg-axi entra device show --id <device-id> ${profileHint(profileName)}`;
258
+ const scopeHint = "Directory devices only; Intune managed devices are a separately authorized surface";
259
+ const truncationHints = truncated ? [fullHint("entra device list", effectiveFlags, profileName)] : [];
260
+ if (!result.complete) {
261
+ return {
262
+ devices,
263
+ count: { returned: devices.length, complete: false, reason: result.reason },
264
+ cursor: result.cursor,
265
+ help: [...truncationHints, resumeHint(profileName), showHint, scopeHint],
266
+ };
267
+ }
268
+ const count = { returned: devices.length, complete: true };
269
+ if (!devices.length) {
270
+ return {
271
+ devices,
272
+ count,
273
+ help: [
274
+ `mg-axi entra device list --filter <odata-filter> ${profileHint(profileName)}`,
275
+ "0 devices matched; the absence of results is the answer, not an error",
276
+ scopeHint,
277
+ ],
278
+ };
279
+ }
280
+ return { devices, count, help: [...truncationHints, showHint, scopeHint] };
281
+ }
282
+ export async function showDevice(session, flags, profile, operation, help, profileName) {
283
+ const { select, fields } = selectedFields(flags, KNOWN_DEVICES, KNOWN_DEVICE_FIELDS, DEFAULT_DEVICE_SHOW_SELECT, help);
284
+ const scopes = scopesFor(flags, DEFAULT_DEVICE_SCOPES, profile, help);
285
+ const full = flags.full === true;
286
+ const raw = await withGuidance(DEVICE_DENIAL_HINTS, () => session.execute({
287
+ profile,
288
+ operation,
289
+ params: { "device-id": String(flags.id) },
290
+ query: { $select: select.join(",") },
291
+ scopes,
292
+ }));
293
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
294
+ throw new AxiError("Graph returned a malformed device body", "GRAPH_ERROR", [
295
+ "Single-device reads carry one device object; treat anything else as unknown, not empty",
296
+ ]);
297
+ }
298
+ const { row, truncated } = project(raw, fields, full);
299
+ if (truncated)
300
+ return { device: row, help: [fullHint("entra device show", flags, profileName)] };
301
+ return { device: row };
302
+ }
303
+ export async function listAdministrativeUnits(session, flags, profile, operation, help, profileName) {
304
+ const common = collectionCommon(session, flags, KNOWN_AUS, KNOWN_AU_FIELDS, DEFAULT_AU_LIST_SELECT, DEFAULT_AU_SCOPES, operation, help, profile);
305
+ const result = await withGuidance(AU_LIST_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
306
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_AU_LIST_SELECT.join(",") };
307
+ if (result.query.$filter !== undefined)
308
+ effectiveFlags.filter = result.query.$filter;
309
+ const units = [];
310
+ let truncated = false;
311
+ for (const row of result.value) {
312
+ const projected = project(row, common.fields, common.full);
313
+ units.push(projected.row);
314
+ truncated = truncated || projected.truncated;
315
+ }
316
+ const showHint = `mg-axi entra administrative-unit show --id <administrative-unit-id> ${profileHint(profileName)}`;
317
+ const truncationHints = truncated ? [fullHint("entra administrative-unit list", effectiveFlags, profileName)] : [];
318
+ if (!result.complete) {
319
+ return {
320
+ administrativeUnits: units,
321
+ count: { returned: units.length, complete: false, reason: result.reason },
322
+ cursor: result.cursor,
323
+ help: [...truncationHints, resumeHint(profileName), showHint],
324
+ };
325
+ }
326
+ const count = { returned: units.length, complete: true };
327
+ if (!units.length) {
328
+ return {
329
+ administrativeUnits: units,
330
+ count,
331
+ help: [
332
+ `mg-axi entra administrative-unit list --filter <odata-filter> ${profileHint(profileName)}`,
333
+ "0 administrative units matched; the absence of results is the answer, not an error",
334
+ ],
335
+ };
336
+ }
337
+ return { administrativeUnits: units, count, help: [...truncationHints, showHint] };
338
+ }
339
+ export async function showAdministrativeUnit(session, flags, profile, operation, help, profileName) {
340
+ const { select, fields } = selectedFields(flags, KNOWN_AUS, KNOWN_AU_FIELDS, DEFAULT_AU_SHOW_SELECT, help);
341
+ const scopes = scopesFor(flags, DEFAULT_AU_SCOPES, profile, help);
342
+ const full = flags.full === true;
343
+ const raw = await withGuidance(AU_SHOW_DENIAL_HINTS, () => session.execute({
344
+ profile,
345
+ operation,
346
+ params: { "administrativeUnit-id": String(flags.id) },
347
+ query: { $select: select.join(",") },
348
+ scopes,
349
+ }));
350
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
351
+ throw new AxiError("Graph returned a malformed administrative-unit body", "GRAPH_ERROR", [
352
+ "Single-unit reads carry one administrativeUnit object; treat anything else as unknown, not empty",
353
+ ]);
354
+ }
355
+ const { row, truncated } = project(raw, fields, full);
356
+ const helpHints = truncated ? [fullHint("entra administrative-unit show", flags, profileName)] : [];
357
+ if (row["membershipType"] === "Dynamic") {
358
+ helpHints.push("Dynamic membership needs additional P1 licensing; scoped administration needs P1 while members are Free");
359
+ }
360
+ if (helpHints.length)
361
+ return { administrativeUnit: row, help: helpHints };
362
+ return { administrativeUnit: row };
363
+ }
364
+ export async function listAdministrativeUnitMembers(session, flags, profile, operation, help, profileName) {
365
+ const unit = String(flags["administrative-unit"]);
366
+ if (!unit.trim())
367
+ throw new AxiError("--administrative-unit needs the administrative-unit object ID", "VALIDATION_ERROR", [help]);
368
+ const common = collectionCommon(session, flags, KNOWN_AU_MEMBERS, KNOWN_AU_MEMBER_FIELDS, DEFAULT_AU_MEMBER_SELECT, DEFAULT_AU_SCOPES, operation, help, profile);
369
+ const args = collectArgs(profile, operation, common, flags, help);
370
+ args.params = { "administrativeUnit-id": unit };
371
+ const result = await withGuidance(AU_MEMBER_DENIAL_HINTS, () => session.collect(args));
372
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_AU_MEMBER_SELECT.join(",") };
373
+ if (result.query.$filter !== undefined)
374
+ effectiveFlags.filter = result.query.$filter;
375
+ const rows = [];
376
+ let truncated = false;
377
+ let limitedInfo = 0;
378
+ for (const row of result.value) {
379
+ const projected = projectMember(row, common.select, common.fields, common.full);
380
+ rows.push(projected.row);
381
+ truncated = truncated || projected.truncated;
382
+ if (projected.limitedInfo)
383
+ limitedInfo += 1;
384
+ }
385
+ const truncationHints = truncated ? [fullHint("entra administrative-unit member list", effectiveFlags, profileName)] : [];
386
+ const limitedHints = limitedInfo > 0
387
+ ? [`${limitedInfo} of ${rows.length} rows have no non-null selected descriptive properties; this may reflect limited read consent or unset properties`]
388
+ : [];
389
+ const hiddenHint = "Hidden memberships are omitted without Member.Read.Hidden; completion describes pagination, not visibility";
390
+ if (!result.complete) {
391
+ return {
392
+ members: rows,
393
+ count: { returned: rows.length, complete: false, reason: result.reason },
394
+ cursor: result.cursor,
395
+ help: [...truncationHints, ...limitedHints, hiddenHint, resumeHint(profileName)],
396
+ };
397
+ }
398
+ const count = { returned: rows.length, complete: true };
399
+ if (!rows.length) {
400
+ return {
401
+ members: rows,
402
+ count,
403
+ help: [
404
+ ...limitedHints,
405
+ hiddenHint,
406
+ "0 members matched; the absence of results is the answer, not an error",
407
+ ],
408
+ };
409
+ }
410
+ return { members: rows, count, help: [...truncationHints, ...limitedHints, hiddenHint] };
411
+ }