@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,464 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ // EXT-01 custom-security-attributes subfamily: the read mapping behind
3
+ // `mg-axi entra attribute-set list/show/count`,
4
+ // `mg-axi entra custom-security-attribute-definition list/show/count` and
5
+ // `mg-axi entra allowed-value list/show/count`. Operation construction stays
6
+ // beside its command; the shared session owns URLs, credentials, paging,
7
+ // retries and error translation, and the SDK owns TOON rendering. This
8
+ // module only maps flags to session calls and projects rows for compact
9
+ // output.
10
+ //
11
+ // Reviewed against the v1.0 directory-list-attributesets, attributeset-get,
12
+ // directory-list-customsecurityattributedefinitions,
13
+ // customsecurityattributedefinition-get,
14
+ // customsecurityattributedefinition-list-allowedvalues and allowedvalue-get
15
+ // operation documentation and the attributeSet,
16
+ // customSecurityAttributeDefinition and allowedValue resource references on
17
+ // 2026-10-05. Every read takes D/A CustomSecAttributeDefinition.Read.All;
18
+ // delegated callers additionally need a custom-security-attribute role, and
19
+ // even Global Administrators have no custom-security-attribute access by
20
+ // default (Attribute Definition Reader or Attribute Definition
21
+ // Administrator work for every read in this slice). Personal Microsoft
22
+ // accounts are not supported. No P1/P2 prerequisite is stated for these
23
+ // reads. The attribute-set list documents $select, $top and $orderby only
24
+ // ($filter is not supported), so it offers no --filter; the definition
25
+ // list documents $select, $top and $filter (eq), so --filter passes through
26
+ // as plain $filter with no $count or ConsistencyLevel contract. The
27
+ // allowed-values list documents $select only, so it offers no --filter.
28
+ // Definition $expand (inline allowedValues) is not reviewed here: allowed
29
+ // values have their own list/show commands. The $count routes carry no
30
+ // operation-level documentation page; only the definition count accepts
31
+ // --filter (its collection documents $filter), the other two take no
32
+ // --filter/--select/--limit/--cursor. Beta attribute sets, definitions and
33
+ // allowed values and every mutation stay out. No mutation lives here.
34
+ // Every attribute-set property this slice may request or display, matching
35
+ // the reviewed resource. Anything else fails before credentials.
36
+ export const KNOWN_ATTRIBUTE_SET_FIELDS = [
37
+ "id",
38
+ "description",
39
+ "maxAttributesPerSet",
40
+ ];
41
+ const KNOWN_ATTRIBUTE_SETS = new Set(KNOWN_ATTRIBUTE_SET_FIELDS);
42
+ // Every definition property this slice may request or display, matching the
43
+ // reviewed resource. The allowedValues navigation property is never
44
+ // projected here; it has its own list/show commands. Anything else fails
45
+ // before credentials.
46
+ export const KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS = [
47
+ "attributeSet",
48
+ "description",
49
+ "id",
50
+ "isCollection",
51
+ "isSearchable",
52
+ "name",
53
+ "status",
54
+ "type",
55
+ "usePreDefinedValuesOnly",
56
+ ];
57
+ const KNOWN_DEFINITIONS = new Set(KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS);
58
+ // Every allowed-value property this slice may request or display, matching
59
+ // the reviewed resource. Anything else fails before credentials.
60
+ export const KNOWN_ALLOWED_VALUE_FIELDS = [
61
+ "id",
62
+ "isActive",
63
+ ];
64
+ const KNOWN_ALLOWED_VALUES = new Set(KNOWN_ALLOWED_VALUE_FIELDS);
65
+ // Attribute sets carry only three short properties, so lists and shows
66
+ // share the full reviewed set.
67
+ const DEFAULT_ATTRIBUTE_SET_LIST_SELECT = [...KNOWN_ATTRIBUTE_SET_FIELDS];
68
+ const DEFAULT_ATTRIBUTE_SET_SHOW_SELECT = [...KNOWN_ATTRIBUTE_SET_FIELDS];
69
+ // Compact definition rows: the identifier, the owning set and name, the
70
+ // availability status and the data type that decide whether the definition
71
+ // is usable.
72
+ const DEFAULT_DEFINITION_LIST_SELECT = ["id", "attributeSet", "name", "status", "type"];
73
+ // Show rows: the full reviewed definition set.
74
+ const DEFAULT_DEFINITION_SHOW_SELECT = [...KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS];
75
+ // Allowed values carry only two short properties, so lists and shows share
76
+ // the full reviewed set.
77
+ const DEFAULT_ALLOWED_VALUE_LIST_SELECT = [...KNOWN_ALLOWED_VALUE_FIELDS];
78
+ const DEFAULT_ALLOWED_VALUE_SHOW_SELECT = [...KNOWN_ALLOWED_VALUE_FIELDS];
79
+ // Delegated defaults are operation-specific; application profiles use their
80
+ // configured .default audience and reject --scopes.
81
+ export const DEFAULT_CUSTOM_SECURITY_SCOPES = ["https://graph.microsoft.com/CustomSecAttributeDefinition.Read.All"];
82
+ const TRUNCATE_AT = 500;
83
+ function fieldList(raw, known, knownList, flag, help, noun) {
84
+ const parts = String(raw)
85
+ .split(",")
86
+ .map(part => part.trim())
87
+ .filter(part => part.length > 0);
88
+ if (!parts.length)
89
+ throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
90
+ const fields = [];
91
+ for (const part of parts) {
92
+ if (!known.has(part)) {
93
+ throw new AxiError(`Unknown custom-security-attribute property ${part} in --${flag}`, "VALIDATION_ERROR", [
94
+ help,
95
+ `Known ${noun} properties: ${knownList.join(", ")}`,
96
+ ]);
97
+ }
98
+ if (!fields.includes(part))
99
+ fields.push(part);
100
+ }
101
+ return fields;
102
+ }
103
+ function scopesFor(flags, defaults, profile, help) {
104
+ if (profile.mode === "application") {
105
+ if (flags.scopes !== undefined) {
106
+ throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
107
+ }
108
+ return undefined;
109
+ }
110
+ if (flags.scopes === undefined)
111
+ return [...defaults];
112
+ const scopes = String(flags.scopes)
113
+ .split(",")
114
+ .map(scope => scope.trim())
115
+ .filter(scope => scope.length > 0);
116
+ return scopes;
117
+ }
118
+ function truncateValue(value, full) {
119
+ const truncate = (text) => full || text.length <= TRUNCATE_AT
120
+ ? { value: text, truncated: false }
121
+ : { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
122
+ if (typeof value === "string")
123
+ return truncate(value);
124
+ if (Array.isArray(value) && value.every(entry => typeof entry === "string")) {
125
+ const projected = value.map(truncate);
126
+ return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
127
+ }
128
+ return { value, truncated: false };
129
+ }
130
+ // Local projection preserves Graph's null/missing distinction: an explicit
131
+ // null stays null, an absent property stays absent and is never synthesized.
132
+ function project(row, fields, full) {
133
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
134
+ const projected = {};
135
+ let truncated = false;
136
+ for (const field of fields) {
137
+ if (!Object.hasOwn(source, field))
138
+ continue;
139
+ const result = truncateValue(source[field], full);
140
+ projected[field] = result.value;
141
+ truncated = truncated || result.truncated;
142
+ }
143
+ return { row: projected, truncated };
144
+ }
145
+ function selectedFields(flags, known, knownList, defaults, help, noun) {
146
+ const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, known, knownList, "select", help, noun);
147
+ const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, known, knownList, "fields", help, noun);
148
+ const missing = fields.find(field => !select.includes(field));
149
+ if (missing) {
150
+ throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
151
+ }
152
+ return { select, fields };
153
+ }
154
+ function profileHint(profileName) {
155
+ return `--profile ${shellValue(profileName)}`;
156
+ }
157
+ function shellValue(value) {
158
+ return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
159
+ }
160
+ function fullHint(command, flags, profileName) {
161
+ const args = Object.entries({ ...flags, profile: profileName, full: true })
162
+ .map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
163
+ return `mg-axi ${command} ${args.join(" ")}`;
164
+ }
165
+ // Denials carry the session's generic grant/role/licence cause; each
166
+ // operation adds the scope, roles and licensing that actually unlock it,
167
+ // because a 403 alone never says which prerequisite is missing. Custom
168
+ // security attributes are unusual: even Global Administrators have no
169
+ // access by default, so the hints name the attribute roles plainly.
170
+ function withGuidance(hints, run) {
171
+ return run().catch(error => {
172
+ if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
173
+ throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
174
+ }
175
+ throw error;
176
+ });
177
+ }
178
+ const CUSTOM_SECURITY_DENIAL_HINTS = [
179
+ "Custom-security-attribute reads need CustomSecAttributeDefinition.Read.All for delegated or application access; delegated callers pass it as --scopes",
180
+ "Delegated reads additionally need a custom-security-attribute role even for Global Administrators: Attribute Definition Reader or Attribute Definition Administrator work for every read in this slice (some reads also accept Attribute Assignment Reader or Attribute Assignment Administrator); by default Global Administrator and other administrator roles have no custom-security-attribute access",
181
+ "Personal Microsoft accounts are not supported for custom-security-attribute reads",
182
+ "No P1/P2 prerequisite is stated for custom-security-attribute reads; never diagnose licence solely from HTTP 403",
183
+ ];
184
+ // Restoring the saved select/filter keeps cursor resumes lossless when
185
+ // --select or --filter is omitted. Only the definition list documents
186
+ // $filter, so only it passes --filter through as plain $filter with no
187
+ // $count or ConsistencyLevel contract; strict input validation refuses the
188
+ // flag on the other lists before credentials instead of forwarding a
189
+ // misleading request.
190
+ function collectionCommon(session, flags, known, knownList, defaults, noun, operation, help, profile, withFilter) {
191
+ const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
192
+ if (cursor !== undefined && !cursor.trim())
193
+ throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
194
+ const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
195
+ const { select, fields } = selectedFields(flags, known, knownList, saved?.["$select"] === undefined ? defaults : fieldList(saved["$select"], known, knownList, "select", help, noun), help, noun);
196
+ const filter = withFilter
197
+ ? (flags.filter === undefined ? saved?.["$filter"] : String(flags.filter))
198
+ : undefined;
199
+ return { cursor, select, fields, scopes: scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help), full: flags.full === true, filter };
200
+ }
201
+ function collectArgs(profile, operation, common, flags, help, params) {
202
+ const query = { $select: common.select.join(",") };
203
+ if (common.filter !== undefined)
204
+ query.$filter = common.filter;
205
+ const args = { profile, operation, query, scopes: common.scopes };
206
+ if (params !== undefined)
207
+ args.params = params;
208
+ if (common.cursor !== undefined)
209
+ args.cursor = common.cursor;
210
+ if (flags.all === true) {
211
+ if (flags.limit !== undefined)
212
+ throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
213
+ }
214
+ else {
215
+ args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
216
+ }
217
+ return args;
218
+ }
219
+ function resumeHint(profileName) {
220
+ return `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`;
221
+ }
222
+ function attributeSetId(flags, help) {
223
+ const id = String(flags.id);
224
+ if (!id.trim())
225
+ throw new AxiError("--id needs the attribute-set identifier", "VALIDATION_ERROR", [help]);
226
+ return id;
227
+ }
228
+ function definitionId(flags, help, flag = "id") {
229
+ const id = String(flags[flag]);
230
+ if (!id.trim())
231
+ throw new AxiError(`--${flag} needs the custom-security-attribute-definition identifier`, "VALIDATION_ERROR", [help]);
232
+ return id;
233
+ }
234
+ function allowedValueId(flags, help) {
235
+ const id = String(flags.id);
236
+ if (!id.trim())
237
+ throw new AxiError("--id needs the allowed-value identifier", "VALIDATION_ERROR", [help]);
238
+ return id;
239
+ }
240
+ function definitionParams(flags, help) {
241
+ return { "customSecurityAttributeDefinition-id": definitionId(flags, help, "definition") };
242
+ }
243
+ function singleResult(raw, fields, full, kind) {
244
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
245
+ throw new AxiError(`Graph returned a malformed ${kind} body`, "GRAPH_ERROR", [
246
+ `Single-${kind} reads carry one ${kind} object; treat anything else as unknown, not empty`,
247
+ ]);
248
+ }
249
+ return project(raw, fields, full);
250
+ }
251
+ export async function listAttributeSets(session, flags, profile, operation, help, profileName) {
252
+ const common = collectionCommon(session, flags, KNOWN_ATTRIBUTE_SETS, KNOWN_ATTRIBUTE_SET_FIELDS, DEFAULT_ATTRIBUTE_SET_LIST_SELECT, "attribute-set", operation, help, profile, false);
253
+ const result = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
254
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_ATTRIBUTE_SET_LIST_SELECT.join(",") };
255
+ const sets = [];
256
+ let truncated = false;
257
+ for (const row of result.value) {
258
+ const projected = project(row, common.fields, common.full);
259
+ sets.push(projected.row);
260
+ truncated = truncated || projected.truncated;
261
+ }
262
+ const showHint = `mg-axi entra attribute-set show --id <set-id> ${profileHint(profileName)}`;
263
+ const filterNote = "Attribute-set lists offer no --filter: Graph documents $select, $top and $orderby only for /directory/attributeSets";
264
+ const truncationHints = truncated ? [fullHint("entra attribute-set list", effectiveFlags, profileName)] : [];
265
+ if (!result.complete) {
266
+ return {
267
+ attributeSets: sets,
268
+ count: { returned: sets.length, complete: false, reason: result.reason },
269
+ cursor: result.cursor,
270
+ help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
271
+ };
272
+ }
273
+ const count = { returned: sets.length, complete: true };
274
+ if (!sets.length) {
275
+ return {
276
+ attributeSets: sets,
277
+ count,
278
+ help: [filterNote, "0 attribute sets matched; the absence of results is the answer, not an error"],
279
+ };
280
+ }
281
+ return { attributeSets: sets, count, help: [...truncationHints, showHint, filterNote] };
282
+ }
283
+ export async function showAttributeSet(session, flags, profile, operation, help, profileName) {
284
+ const { select, fields } = selectedFields(flags, KNOWN_ATTRIBUTE_SETS, KNOWN_ATTRIBUTE_SET_FIELDS, DEFAULT_ATTRIBUTE_SET_SHOW_SELECT, help, "attribute-set");
285
+ const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
286
+ const full = flags.full === true;
287
+ const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
288
+ profile,
289
+ operation,
290
+ params: { "attributeSet-id": attributeSetId(flags, help) },
291
+ query: { $select: select.join(",") },
292
+ scopes,
293
+ }));
294
+ const { row, truncated } = singleResult(raw, fields, full, "attribute set");
295
+ if (truncated)
296
+ return { attributeSet: row, help: [fullHint("entra attribute-set show", flags, profileName)] };
297
+ return { attributeSet: row };
298
+ }
299
+ // The $count route returns a text/plain integer scalar rather than a JSON
300
+ // collection, so the leaf reads it through session.execute with scalar mode
301
+ // and accepts only a non-negative integer. There is no
302
+ // --filter/--select/--limit contract on the count: the catalogue declares
303
+ // no such flags and strict input validation refuses them before
304
+ // credentials.
305
+ export async function countAttributeSets(session, flags, profile, operation, help, profileName) {
306
+ void profileName;
307
+ const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
308
+ const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({ profile, operation, scopes, scalar: true }));
309
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
310
+ throw new AxiError("Graph returned a malformed attribute-set count body", "GRAPH_ERROR", [
311
+ "Attribute-set counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
312
+ ]);
313
+ }
314
+ return { count: { returned: raw, complete: true } };
315
+ }
316
+ export async function listCustomSecurityAttributeDefinitions(session, flags, profile, operation, help, profileName) {
317
+ const common = collectionCommon(session, flags, KNOWN_DEFINITIONS, KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS, DEFAULT_DEFINITION_LIST_SELECT, "definition", operation, help, profile, true);
318
+ const result = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
319
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_DEFINITION_LIST_SELECT.join(",") };
320
+ if (result.query.$filter !== undefined)
321
+ effectiveFlags.filter = result.query.$filter;
322
+ const definitions = [];
323
+ let truncated = false;
324
+ for (const row of result.value) {
325
+ const projected = project(row, common.fields, common.full);
326
+ definitions.push(projected.row);
327
+ truncated = truncated || projected.truncated;
328
+ }
329
+ const showHint = `mg-axi entra custom-security-attribute-definition show --id <definition-id> ${profileHint(profileName)}`;
330
+ const truncationHints = truncated ? [fullHint("entra custom-security-attribute-definition list", effectiveFlags, profileName)] : [];
331
+ if (!result.complete) {
332
+ return {
333
+ customSecurityAttributeDefinitions: definitions,
334
+ count: { returned: definitions.length, complete: false, reason: result.reason },
335
+ cursor: result.cursor,
336
+ help: [...truncationHints, resumeHint(profileName), showHint],
337
+ };
338
+ }
339
+ const count = { returned: definitions.length, complete: true };
340
+ if (!definitions.length) {
341
+ return {
342
+ customSecurityAttributeDefinitions: definitions,
343
+ count,
344
+ help: ["0 custom security attribute definitions matched; the absence of results is the answer, not an error"],
345
+ };
346
+ }
347
+ return { customSecurityAttributeDefinitions: definitions, count, help: [...truncationHints, showHint] };
348
+ }
349
+ export async function showCustomSecurityAttributeDefinition(session, flags, profile, operation, help, profileName) {
350
+ const { select, fields } = selectedFields(flags, KNOWN_DEFINITIONS, KNOWN_CUSTOM_SECURITY_DEFINITION_FIELDS, DEFAULT_DEFINITION_SHOW_SELECT, help, "definition");
351
+ const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
352
+ const full = flags.full === true;
353
+ const id = definitionId(flags, help);
354
+ const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
355
+ profile,
356
+ operation,
357
+ params: { "customSecurityAttributeDefinition-id": id },
358
+ query: { $select: select.join(",") },
359
+ scopes,
360
+ }));
361
+ const { row, truncated } = singleResult(raw, fields, full, "custom security attribute definition");
362
+ const helpHints = [
363
+ ...(truncated ? [fullHint("entra custom-security-attribute-definition show", flags, profileName)] : []),
364
+ `mg-axi entra allowed-value list --definition ${shellValue(id)} ${profileHint(profileName)}`,
365
+ ];
366
+ return { customSecurityAttributeDefinition: row, help: helpHints };
367
+ }
368
+ // The $count route returns a text/plain integer scalar rather than a JSON
369
+ // collection, so the leaf reads it through session.execute with scalar mode
370
+ // and accepts only a non-negative integer. --filter narrows the count
371
+ // server-side; there is no --select/--limit/--cursor contract on the count.
372
+ export async function countCustomSecurityAttributeDefinitions(session, flags, profile, operation, help, profileName) {
373
+ void profileName;
374
+ const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
375
+ const query = {};
376
+ if (flags.filter !== undefined)
377
+ query.$filter = String(flags.filter);
378
+ const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({ profile, operation, query, scopes, scalar: true }));
379
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
380
+ throw new AxiError("Graph returned a malformed definition count body", "GRAPH_ERROR", [
381
+ "Definition counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
382
+ ]);
383
+ }
384
+ if (flags.filter !== undefined) {
385
+ return {
386
+ count: { returned: raw, complete: true },
387
+ help: [`Count reflects --filter ${shellValue(String(flags.filter))}; drop --filter for the tenant total`],
388
+ };
389
+ }
390
+ return { count: { returned: raw, complete: true } };
391
+ }
392
+ export async function listAllowedValues(session, flags, profile, operation, help, profileName) {
393
+ const common = collectionCommon(session, flags, KNOWN_ALLOWED_VALUES, KNOWN_ALLOWED_VALUE_FIELDS, DEFAULT_ALLOWED_VALUE_LIST_SELECT, "allowed-value", operation, help, profile, false);
394
+ const definition = definitionId(flags, help, "definition");
395
+ const result = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help, definitionParams(flags, help))));
396
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_ALLOWED_VALUE_LIST_SELECT.join(",") };
397
+ const values = [];
398
+ let truncated = false;
399
+ for (const row of result.value) {
400
+ const projected = project(row, common.fields, common.full);
401
+ values.push(projected.row);
402
+ truncated = truncated || projected.truncated;
403
+ }
404
+ const showHint = `mg-axi entra allowed-value show --definition ${shellValue(definition)} --id <value-id> ${profileHint(profileName)}`;
405
+ const filterNote = "Allowed-value lists offer no --filter: Graph documents $select only for allowedValues";
406
+ const truncationHints = truncated ? [fullHint("entra allowed-value list", effectiveFlags, profileName)] : [];
407
+ if (!result.complete) {
408
+ return {
409
+ allowedValues: values,
410
+ count: { returned: values.length, complete: false, reason: result.reason },
411
+ cursor: result.cursor,
412
+ help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
413
+ };
414
+ }
415
+ const count = { returned: values.length, complete: true };
416
+ if (!values.length) {
417
+ return {
418
+ allowedValues: values,
419
+ count,
420
+ help: [filterNote, "0 allowed values matched; the definition may allow free-form values instead of predefined ones"],
421
+ };
422
+ }
423
+ return { allowedValues: values, count, help: [...truncationHints, showHint, filterNote] };
424
+ }
425
+ export async function showAllowedValue(session, flags, profile, operation, help, profileName) {
426
+ const { select, fields } = selectedFields(flags, KNOWN_ALLOWED_VALUES, KNOWN_ALLOWED_VALUE_FIELDS, DEFAULT_ALLOWED_VALUE_SHOW_SELECT, help, "allowed-value");
427
+ const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
428
+ const full = flags.full === true;
429
+ const params = { ...definitionParams(flags, help), "allowedValue-id": allowedValueId(flags, help) };
430
+ const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
431
+ profile,
432
+ operation,
433
+ params,
434
+ query: { $select: select.join(",") },
435
+ scopes,
436
+ }));
437
+ const { row, truncated } = singleResult(raw, fields, full, "allowed value");
438
+ if (truncated)
439
+ return { allowedValue: row, help: [fullHint("entra allowed-value show", flags, profileName)] };
440
+ return { allowedValue: row };
441
+ }
442
+ // The $count route returns a text/plain integer scalar rather than a JSON
443
+ // collection, so the leaf reads it through session.execute with scalar mode
444
+ // and accepts only a non-negative integer. There is no
445
+ // --filter/--select/--limit contract on the count: the catalogue declares
446
+ // no such flags and strict input validation refuses them before
447
+ // credentials.
448
+ export async function countAllowedValues(session, flags, profile, operation, help, profileName) {
449
+ void profileName;
450
+ const scopes = scopesFor(flags, DEFAULT_CUSTOM_SECURITY_SCOPES, profile, help);
451
+ const raw = await withGuidance(CUSTOM_SECURITY_DENIAL_HINTS, () => session.execute({
452
+ profile,
453
+ operation,
454
+ params: definitionParams(flags, help),
455
+ scopes,
456
+ scalar: true,
457
+ }));
458
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
459
+ throw new AxiError("Graph returned a malformed allowed-value count body", "GRAPH_ERROR", [
460
+ "Allowed-value counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
461
+ ]);
462
+ }
463
+ return { count: { returned: raw, complete: true } };
464
+ }