@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,673 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ // EXT-02 lifecycle-workflows reads: the read-only workflow, workflow
3
+ // template, task definition and tenant-settings first part, the
4
+ // second-part top-level run and user/subject processing-result reads and
5
+ // the third-part task-report reads. See
6
+ // README.md for the supported commands and usage. Operation construction
7
+ // stays beside its command; the shared session owns URLs, credentials,
8
+ // paging, retries and error translation, and the SDK owns TOON rendering.
9
+ // This module only maps flags to session calls and projects rows for
10
+ // compact output.
11
+ //
12
+ // Reviewed against the v1.0 lifecycleworkflowscontainer-list-workflows,
13
+ // workflow-get, lifecycleworkflowscontainer-list-workflowtemplates,
14
+ // workflowtemplate-get, lifecycleworkflowscontainer-list-taskdefinitions,
15
+ // taskdefinition-get, lifecyclemanagementsettings-get, workflow-list-runs,
16
+ // run-get, workflow-list-userprocessingresults, userprocessingresult-get,
17
+ // run-list-subjectprocessingresults, subjectprocessingresult-get,
18
+ // workflow-list-taskreports and task-get operation documentation and the
19
+ // reviewed resource property sets for workflow, workflowTemplate,
20
+ // taskDefinition, lifecycleManagementSettings, run, userProcessingResult,
21
+ // subjectProcessingResult, taskReport and task, on 2026-10-06.
22
+ // Workflow list/show take D/A
23
+ // LifecycleWorkflows-Workflow.ReadBasic.All as the least privileged choice
24
+ // (LifecycleWorkflows-Workflow.Read.All or LifecycleWorkflows.Read.All for
25
+ // richer detail); template and top-level task-definition reads take D/A
26
+ // LifecycleWorkflows.Read.All; run, processing-result and task-report reads
27
+ // take D/A LifecycleWorkflows-Reports.Read.All as the least privileged
28
+ // choice (LifecycleWorkflows.Read.All or LifecycleWorkflows.ReadWrite.All
29
+ // for richer detail). The taskReport-nested task and taskDefinition singles
30
+ // have no operation-level documentation page, so they share the reporting
31
+ // family's Reports.Read.All default; the workflow-tasks task-get page
32
+ // documents LifecycleWorkflows-Workflow.Read.All for its own route and the
33
+ // top-level taskdefinition-get page documents LifecycleWorkflows.Read.All
34
+ // for its own route, and both stay named as richer alternatives on denial.
35
+ // Delegated personal Microsoft accounts are not supported.
36
+ // Delegated callers additionally need Global Reader or Lifecycle Workflows
37
+ // Administrator. The run and user-processing lists document $filter (and
38
+ // $select), so --filter passes through as plain $filter with no $count or
39
+ // ConsistencyLevel contract ($search/$orderby/$expand stay unreviewed and
40
+ // $top/$skip stay server-side paging concerns); run and user-processing
41
+ // singles document $select only. The subject list documents $filter but not
42
+ // $select and the subject get documents $expand only, so subject rows
43
+ // arrive whole and are projected locally with --fields (the template-show
44
+ // pattern); the subject navigation stays out everywhere. Task reports
45
+ // aggregate one task's results within one workflow; the task and
46
+ // taskDefinition navigations return the related objects whole (task
47
+ // arguments and definition parameters ride along as reviewed fields) while
48
+ // taskProcessingResults, the subject link and every $expand stay out.
49
+ // The taskReports list documents $select/$filter (with $orderby/$expand
50
+ // unreviewed); the taskReport get and the taskReport-nested task and
51
+ // taskDefinition singles have no operation-level page (the task-get page
52
+ // covers the workflow-tasks route only and documents no query parameters
53
+ // there), so those three singles arrive whole and are projected locally
54
+ // with --fields (--select is refused before credentials). Processing results
55
+ // are personal data: user and subject defaults carry status and counts
56
+ // only, never the subject link. Deeper execution detail (run-nested and
57
+ // third-level processing results, reprocessed runs, subjects, workflow and
58
+ // template tasks, createdBy, lastModifiedBy, previewScope, executionScope,
59
+ // versions, insights, deleted items, custom task extensions) needs its own
60
+ // sub-read and stays out, as do the summary functions (bracketed start/end
61
+ // arguments outside the session-guard function-binding allowlist).
62
+ // Lifecycle workflows need Microsoft Entra ID Governance or Microsoft Entra
63
+ // Suite (every governed user, not only administrators). No mutation (no
64
+ // workflow create/update/delete/activate/run, no settings update) and no
65
+ // beta.
66
+ // Workflows expose the configured joiner/mover/leaver automations. Only
67
+ // scalar properties carry a reviewed $select contract; tasks, runs,
68
+ // processing results, versions, scopes and createdBy/lastModifiedBy links
69
+ // are navigation properties and stay out (tasks ride expanded by default on
70
+ // the get and are dropped in local projection).
71
+ export const KNOWN_WORKFLOW_FIELDS = [
72
+ "id",
73
+ "category",
74
+ "createdDateTime",
75
+ "deletedDateTime",
76
+ "description",
77
+ "displayName",
78
+ "executionConditions",
79
+ "isEnabled",
80
+ "isSchedulingEnabled",
81
+ "lastModifiedDateTime",
82
+ "nextScheduleRunDateTime",
83
+ "version",
84
+ "quarantineDetails",
85
+ "settings",
86
+ ];
87
+ // Workflow templates expose the pre-configured Entra scenarios new
88
+ // workflows are built from. The tasks relationship is expanded by default
89
+ // on the wire and is dropped here: template tasks belong to a later slice.
90
+ export const KNOWN_WORKFLOW_TEMPLATE_FIELDS = [
91
+ "id",
92
+ "category",
93
+ "description",
94
+ "displayName",
95
+ "executionConditions",
96
+ ];
97
+ // Task definitions expose the built-in tasks workflows are constructed
98
+ // from. They carry no relationships.
99
+ export const KNOWN_TASK_DEFINITION_FIELDS = [
100
+ "id",
101
+ "category",
102
+ "continueOnError",
103
+ "description",
104
+ "displayName",
105
+ "parameters",
106
+ "version",
107
+ ];
108
+ // Tenant settings expose the workflow schedule interval and the email and
109
+ // quarantine configuration. They carry no relationships; the update action
110
+ // belongs to no read slice.
111
+ export const KNOWN_LIFECYCLE_SETTINGS_FIELDS = [
112
+ "workflowScheduleIntervalInHours",
113
+ "emailSettings",
114
+ "quarantineConfiguration",
115
+ ];
116
+ // Runs expose the execution instances of one workflow. activatedOnScope is
117
+ // a complex scope object (not a reviewed scalar) and the processing-result
118
+ // relationships need their own sub-reads, so neither is projected here.
119
+ // totalTasksCounts keeps the resource documentation's spelling.
120
+ export const KNOWN_RUN_FIELDS = [
121
+ "id",
122
+ "completedDateTime",
123
+ "failedTasksCount",
124
+ "failedUsersCount",
125
+ "lastUpdatedDateTime",
126
+ "processingStatus",
127
+ "scheduledDateTime",
128
+ "startedDateTime",
129
+ "successfulUsersCount",
130
+ "totalTasksCounts",
131
+ "totalUsersCount",
132
+ "totalUnprocessedTasksCount",
133
+ "workflowExecutionType",
134
+ ];
135
+ // User processing results aggregate one user's workflow execution. The
136
+ // subject relationship carries the targeted user identity: it needs
137
+ // $expand, stays off the reviewed set, and is never requested.
138
+ export const KNOWN_USER_PROCESSING_FIELDS = [
139
+ "id",
140
+ "completedDateTime",
141
+ "failedTasksCount",
142
+ "processingStatus",
143
+ "scheduledDateTime",
144
+ "startedDateTime",
145
+ "totalTasksCount",
146
+ "totalUnprocessedTasksCount",
147
+ "workflowExecutionType",
148
+ "workflowVersion",
149
+ ];
150
+ // Subject processing results mirror user results for non-user subjects.
151
+ // subjectType stays (it names the subject kind, not the subject); the
152
+ // workflowSubject-typed subject navigation stays out with $expand.
153
+ export const KNOWN_SUBJECT_PROCESSING_FIELDS = [
154
+ "id",
155
+ "completedDateTime",
156
+ "failedTasksCount",
157
+ "processingStatus",
158
+ "scheduledDateTime",
159
+ "startedDateTime",
160
+ "subjectType",
161
+ "totalTasksCount",
162
+ "totalUnprocessedTasksCount",
163
+ "workflowExecutionType",
164
+ "workflowVersion",
165
+ ];
166
+ // Task reports aggregate one workflow task's processing results within one
167
+ // run. runId names the owning run; the task, taskDefinition and
168
+ // taskProcessingResults relationships need their own sub-reads ($expand
169
+ // stays out), so only scalar report properties are reviewed here.
170
+ export const KNOWN_TASK_REPORT_FIELDS = [
171
+ "id",
172
+ "runId",
173
+ "processingStatus",
174
+ "successfulUsersCount",
175
+ "failedUsersCount",
176
+ "unprocessedUsersCount",
177
+ "totalUsersCount",
178
+ "startedDateTime",
179
+ "completedDateTime",
180
+ "lastUpdatedDateTime",
181
+ ];
182
+ // Tasks are the actions a workflow executes when triggered. arguments
183
+ // carries the configured key/value pairs (for example group IDs); the
184
+ // taskProcessingResults relationship needs its own sub-read, so it stays
185
+ // out with $expand.
186
+ export const KNOWN_TASK_FIELDS = [
187
+ "id",
188
+ "arguments",
189
+ "category",
190
+ "continueOnError",
191
+ "description",
192
+ "displayName",
193
+ "executionSequence",
194
+ "isEnabled",
195
+ "taskDefinitionId",
196
+ ];
197
+ const WORKFLOW_KNOWN = new Set(KNOWN_WORKFLOW_FIELDS);
198
+ const WORKFLOW_TEMPLATE_KNOWN = new Set(KNOWN_WORKFLOW_TEMPLATE_FIELDS);
199
+ const TASK_DEFINITION_KNOWN = new Set(KNOWN_TASK_DEFINITION_FIELDS);
200
+ const LIFECYCLE_SETTINGS_KNOWN = new Set(KNOWN_LIFECYCLE_SETTINGS_FIELDS);
201
+ const RUN_KNOWN = new Set(KNOWN_RUN_FIELDS);
202
+ const USER_PROCESSING_KNOWN = new Set(KNOWN_USER_PROCESSING_FIELDS);
203
+ const SUBJECT_PROCESSING_KNOWN = new Set(KNOWN_SUBJECT_PROCESSING_FIELDS);
204
+ const TASK_REPORT_KNOWN = new Set(KNOWN_TASK_REPORT_FIELDS);
205
+ const TASK_KNOWN = new Set(KNOWN_TASK_FIELDS);
206
+ // Compact rows: identifiers plus category and enablement state.
207
+ const DEFAULT_WORKFLOW_LIST_SELECT = ["id", "displayName", "category", "isEnabled", "isSchedulingEnabled"];
208
+ const DEFAULT_WORKFLOW_SHOW_SELECT = [...KNOWN_WORKFLOW_FIELDS];
209
+ const DEFAULT_WORKFLOW_TEMPLATE_LIST_SELECT = ["id", "displayName", "category"];
210
+ const DEFAULT_WORKFLOW_TEMPLATE_SHOW_SELECT = [...KNOWN_WORKFLOW_TEMPLATE_FIELDS];
211
+ const DEFAULT_TASK_DEFINITION_LIST_SELECT = ["id", "displayName", "category", "version"];
212
+ const DEFAULT_TASK_DEFINITION_SHOW_SELECT = [...KNOWN_TASK_DEFINITION_FIELDS];
213
+ const DEFAULT_LIFECYCLE_SETTINGS_SELECT = [...KNOWN_LIFECYCLE_SETTINGS_FIELDS];
214
+ // Compact run rows: identifiers plus execution status and user counts.
215
+ const DEFAULT_RUN_LIST_SELECT = ["id", "processingStatus", "totalUsersCount", "failedUsersCount", "successfulUsersCount"];
216
+ const DEFAULT_RUN_SHOW_SELECT = [...KNOWN_RUN_FIELDS];
217
+ // Processing results are personal data: minimal default fields (status and
218
+ // counts only) on both lists and singles; datetimes and versions need an
219
+ // explicit --select (user results) or --fields (subject results).
220
+ const DEFAULT_USER_PROCESSING_LIST_SELECT = ["id", "processingStatus", "failedTasksCount", "totalTasksCount", "totalUnprocessedTasksCount"];
221
+ const DEFAULT_USER_PROCESSING_SHOW_SELECT = [...DEFAULT_USER_PROCESSING_LIST_SELECT];
222
+ const DEFAULT_SUBJECT_PROCESSING_FIELDS = ["id", "subjectType", "processingStatus", "failedTasksCount", "totalTasksCount", "totalUnprocessedTasksCount"];
223
+ // Compact task-report rows: identifiers plus the owning run and user counts.
224
+ const DEFAULT_TASK_REPORT_LIST_SELECT = ["id", "runId", "processingStatus", "totalUsersCount", "failedUsersCount", "successfulUsersCount"];
225
+ const DEFAULT_TASK_REPORT_SHOW_SELECT = [...KNOWN_TASK_REPORT_FIELDS];
226
+ const DEFAULT_TASK_SHOW_SELECT = [...KNOWN_TASK_FIELDS];
227
+ export const DEFAULT_WORKFLOW_SCOPES = ["https://graph.microsoft.com/LifecycleWorkflows-Workflow.ReadBasic.All"];
228
+ export const DEFAULT_LIFECYCLE_SCOPES = ["https://graph.microsoft.com/LifecycleWorkflows.Read.All"];
229
+ export const DEFAULT_REPORTS_SCOPES = ["https://graph.microsoft.com/LifecycleWorkflows-Reports.Read.All"];
230
+ const TRUNCATE_AT = 500;
231
+ function fieldList(raw, flag, known, knownList, help) {
232
+ const parts = String(raw)
233
+ .split(",")
234
+ .map(part => part.trim())
235
+ .filter(part => part.length > 0);
236
+ if (!parts.length)
237
+ throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
238
+ const fields = [];
239
+ for (const part of parts) {
240
+ if (!known.has(part)) {
241
+ throw new AxiError(`Unknown property ${part} in --${flag}`, "VALIDATION_ERROR", [
242
+ help,
243
+ `Known properties: ${knownList.join(", ")}`,
244
+ ]);
245
+ }
246
+ if (!fields.includes(part))
247
+ fields.push(part);
248
+ }
249
+ return fields;
250
+ }
251
+ function scopesFor(flags, profile, defaults, help) {
252
+ if (profile.mode === "application") {
253
+ if (flags.scopes !== undefined) {
254
+ throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
255
+ }
256
+ return undefined;
257
+ }
258
+ if (flags.scopes === undefined)
259
+ return [...defaults];
260
+ const scopes = String(flags.scopes)
261
+ .split(",")
262
+ .map(scope => scope.trim())
263
+ .filter(scope => scope.length > 0);
264
+ return scopes;
265
+ }
266
+ function truncateValue(value, full) {
267
+ if (full)
268
+ return { value, truncated: false };
269
+ if (typeof value === "string" && value.length > TRUNCATE_AT) {
270
+ return { value: `${value.slice(0, TRUNCATE_AT)}... (truncated, ${value.length} chars total)`, truncated: true };
271
+ }
272
+ if (Array.isArray(value)) {
273
+ const results = value.map(item => truncateValue(item, full));
274
+ return { value: results.map(result => result.value), truncated: results.some(result => result.truncated) };
275
+ }
276
+ if (value !== null && typeof value === "object") {
277
+ let truncated = false;
278
+ const entries = Object.entries(value).map(([key, item]) => {
279
+ const result = truncateValue(item, full);
280
+ truncated = truncated || result.truncated;
281
+ return [key, result.value];
282
+ });
283
+ return { value: Object.fromEntries(entries), truncated };
284
+ }
285
+ return { value, truncated: false };
286
+ }
287
+ // Local projection preserves Graph's null/missing distinction: an explicit
288
+ // null stays null, an absent property stays absent and is never synthesized.
289
+ function project(row, fields, full) {
290
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
291
+ const projected = {};
292
+ let truncated = false;
293
+ for (const field of fields) {
294
+ if (!Object.hasOwn(source, field))
295
+ continue;
296
+ const result = truncateValue(source[field], full);
297
+ projected[field] = result.value;
298
+ truncated = truncated || result.truncated;
299
+ }
300
+ return { row: projected, truncated };
301
+ }
302
+ function selectedFields(flags, defaults, known, knownList, help) {
303
+ const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, "select", known, knownList, help);
304
+ const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, "fields", known, knownList, help);
305
+ const missing = fields.find(field => !select.includes(field));
306
+ if (missing) {
307
+ throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
308
+ }
309
+ return { select, fields };
310
+ }
311
+ function profileHint(profileName) {
312
+ return `--profile ${shellValue(profileName)}`;
313
+ }
314
+ function shellValue(value) {
315
+ return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
316
+ }
317
+ function fullHint(command, flags, profileName) {
318
+ const args = Object.entries({ ...flags, ...(flags.cursor === undefined ? {} : { cursor: "-" }), profile: profileName, full: true })
319
+ .map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
320
+ return `mg-axi ${command} ${args.join(" ")}`;
321
+ }
322
+ // Denials carry the session's generic grant/role/licence cause;
323
+ // lifecycle workflows adds the scope, roles and licensing that actually
324
+ // unlock these reads, because a 403 alone never says which prerequisite is
325
+ // missing.
326
+ function withGuidance(run, hints = LIFECYCLE_DENIAL_HINTS) {
327
+ return run().catch(error => {
328
+ if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
329
+ throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
330
+ }
331
+ throw error;
332
+ });
333
+ }
334
+ const LIFECYCLE_DENIAL_HINTS = [
335
+ "Lifecycle-workflow reads need LifecycleWorkflows.Read.All plus Global Reader or Lifecycle Workflows Administrator for delegated access, or admin-consented LifecycleWorkflows.Read.All for application access; delegated personal Microsoft accounts are not supported",
336
+ "Lifecycle workflows need Microsoft Entra ID Governance or Microsoft Entra Suite (every governed user, not only administrators)",
337
+ ];
338
+ // Workflow reads document their own least-privileged scope, so denials name
339
+ // ReadBasic first with the richer read alternatives beside it.
340
+ const WORKFLOW_DENIAL_HINTS = [
341
+ "Workflow reads need LifecycleWorkflows-Workflow.ReadBasic.All (least privileged; LifecycleWorkflows-Workflow.Read.All or LifecycleWorkflows.Read.All for richer detail) plus Global Reader or Lifecycle Workflows Administrator for delegated access, or admin-consented LifecycleWorkflows-Workflow.ReadBasic.All for application access; delegated personal Microsoft accounts are not supported",
342
+ "Lifecycle workflows need Microsoft Entra ID Governance or Microsoft Entra Suite (every governed user, not only administrators)",
343
+ ];
344
+ // Run and processing-result reads document the reports scope, so denials
345
+ // name Reports.Read.All first with the richer read alternatives beside it.
346
+ const REPORTS_DENIAL_HINTS = [
347
+ "Lifecycle-workflow reporting reads need LifecycleWorkflows-Reports.Read.All (least privileged; LifecycleWorkflows.Read.All or LifecycleWorkflows.ReadWrite.All for richer detail) plus Global Reader or Lifecycle Workflows Administrator for delegated access, or admin-consented LifecycleWorkflows-Reports.Read.All for application access; delegated personal Microsoft accounts are not supported",
348
+ "Lifecycle workflows need Microsoft Entra ID Governance or Microsoft Entra Suite (every governed user, not only administrators)",
349
+ ];
350
+ function workflowId(flags, help) {
351
+ const workflow = flags.workflow === undefined ? "" : String(flags.workflow);
352
+ if (!workflow.trim()) {
353
+ throw new AxiError("--workflow needs the parent lifecycle workflow ID", "VALIDATION_ERROR", [help]);
354
+ }
355
+ return workflow;
356
+ }
357
+ function reportId(flags, help) {
358
+ const report = flags.report === undefined ? "" : String(flags.report);
359
+ if (!report.trim()) {
360
+ throw new AxiError("--report needs the parent task report ID", "VALIDATION_ERROR", [help]);
361
+ }
362
+ return report;
363
+ }
364
+ const WORKFLOW_LIST = {
365
+ command: "entra lifecycle workflow list",
366
+ key: "workflows",
367
+ known: WORKFLOW_KNOWN,
368
+ knownList: KNOWN_WORKFLOW_FIELDS,
369
+ defaultSelect: DEFAULT_WORKFLOW_LIST_SELECT,
370
+ defaultScopes: DEFAULT_WORKFLOW_SCOPES,
371
+ hints: WORKFLOW_DENIAL_HINTS,
372
+ emptyNote: "0 workflows matched; the absence of results is the answer, not an error",
373
+ standing: profileName => [
374
+ "Workflows are the configured joiner/mover/leaver automations: a workflow row never carries its tasks, runs or processing results; those belong to later slices",
375
+ `Show one workflow: mg-axi entra lifecycle workflow show --id <workflow-id> ${profileHint(profileName)}`,
376
+ ],
377
+ };
378
+ const WORKFLOW_TEMPLATE_LIST = {
379
+ command: "entra lifecycle workflow-template list",
380
+ key: "workflowTemplates",
381
+ known: WORKFLOW_TEMPLATE_KNOWN,
382
+ knownList: KNOWN_WORKFLOW_TEMPLATE_FIELDS,
383
+ defaultSelect: DEFAULT_WORKFLOW_TEMPLATE_LIST_SELECT,
384
+ defaultScopes: DEFAULT_LIFECYCLE_SCOPES,
385
+ hints: LIFECYCLE_DENIAL_HINTS,
386
+ emptyNote: "0 workflow templates matched; the absence of results is the answer, not an error",
387
+ standing: profileName => [
388
+ "Workflow templates are the pre-configured Entra scenarios new workflows are built from; template tasks belong to a later slice",
389
+ `Show one workflow template: mg-axi entra lifecycle workflow-template show --id <template-id> ${profileHint(profileName)}`,
390
+ ],
391
+ };
392
+ const TASK_DEFINITION_LIST = {
393
+ command: "entra lifecycle task-definition list",
394
+ key: "taskDefinitions",
395
+ known: TASK_DEFINITION_KNOWN,
396
+ knownList: KNOWN_TASK_DEFINITION_FIELDS,
397
+ defaultSelect: DEFAULT_TASK_DEFINITION_LIST_SELECT,
398
+ defaultScopes: DEFAULT_LIFECYCLE_SCOPES,
399
+ hints: LIFECYCLE_DENIAL_HINTS,
400
+ emptyNote: "0 task definitions matched; the absence of results is the answer, not an error",
401
+ standing: profileName => [
402
+ "Task definitions are the built-in tasks workflows are constructed from; they carry no relationships",
403
+ `Show one task definition: mg-axi entra lifecycle task-definition show --id <task-definition-id> ${profileHint(profileName)}`,
404
+ ],
405
+ };
406
+ const RUN_LIST = {
407
+ command: "entra lifecycle run list",
408
+ key: "runs",
409
+ known: RUN_KNOWN,
410
+ knownList: KNOWN_RUN_FIELDS,
411
+ defaultSelect: DEFAULT_RUN_LIST_SELECT,
412
+ defaultScopes: DEFAULT_REPORTS_SCOPES,
413
+ hints: REPORTS_DENIAL_HINTS,
414
+ emptyNote: "0 runs matched; the absence of results is the answer, not an error",
415
+ standing: profileName => [
416
+ "Runs are the execution instances of one workflow: a run row never carries its user, subject or task processing results; those belong to later slices",
417
+ `Show one run: mg-axi entra lifecycle run show --workflow <workflow-id> --id <run-id> ${profileHint(profileName)}`,
418
+ ],
419
+ };
420
+ const USER_PROCESSING_LIST = {
421
+ command: "entra lifecycle user-processing-result list",
422
+ key: "userProcessingResults",
423
+ known: USER_PROCESSING_KNOWN,
424
+ knownList: KNOWN_USER_PROCESSING_FIELDS,
425
+ defaultSelect: DEFAULT_USER_PROCESSING_LIST_SELECT,
426
+ defaultScopes: DEFAULT_REPORTS_SCOPES,
427
+ hints: REPORTS_DENIAL_HINTS,
428
+ emptyNote: "0 user processing results matched; the absence of results is the answer, not an error",
429
+ standing: profileName => [
430
+ "User processing results aggregate one user's workflow execution and carry personal data: defaults stay minimal and the subject link is never requested",
431
+ `Show one user processing result: mg-axi entra lifecycle user-processing-result show --workflow <workflow-id> --id <result-id> ${profileHint(profileName)}`,
432
+ ],
433
+ };
434
+ const SUBJECT_PROCESSING_LIST = {
435
+ command: "entra lifecycle subject-processing-result list",
436
+ key: "subjectProcessingResults",
437
+ known: SUBJECT_PROCESSING_KNOWN,
438
+ knownList: KNOWN_SUBJECT_PROCESSING_FIELDS,
439
+ defaultSelect: DEFAULT_SUBJECT_PROCESSING_FIELDS,
440
+ defaultScopes: DEFAULT_REPORTS_SCOPES,
441
+ hints: REPORTS_DENIAL_HINTS,
442
+ emptyNote: "0 subject processing results matched; the absence of results is the answer, not an error",
443
+ queryless: true,
444
+ standing: profileName => [
445
+ "Subject processing results mirror user results for non-user subjects and carry personal data: rows arrive whole (Graph documents no $select here) and defaults stay minimal",
446
+ `Show one subject processing result: mg-axi entra lifecycle subject-processing-result show --workflow <workflow-id> --id <result-id> ${profileHint(profileName)}`,
447
+ ],
448
+ };
449
+ const TASK_REPORT_LIST = {
450
+ command: "entra lifecycle task-report list",
451
+ key: "taskReports",
452
+ known: TASK_REPORT_KNOWN,
453
+ knownList: KNOWN_TASK_REPORT_FIELDS,
454
+ defaultSelect: DEFAULT_TASK_REPORT_LIST_SELECT,
455
+ defaultScopes: DEFAULT_REPORTS_SCOPES,
456
+ hints: REPORTS_DENIAL_HINTS,
457
+ emptyNote: "0 task reports matched; the absence of results is the answer, not an error",
458
+ standing: profileName => [
459
+ "Task reports aggregate one workflow task's results within one run: a report row never carries its task, task definition or task processing results; those belong to later slices",
460
+ `Show one task report: mg-axi entra lifecycle task-report show --workflow <workflow-id> --id <report-id> ${profileHint(profileName)}`,
461
+ ],
462
+ };
463
+ async function listCollection(shape, session, flags, profile, operation, help, profileName, params = {}, resumeHint) {
464
+ const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
465
+ if (cursor !== undefined && !cursor.trim())
466
+ throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
467
+ if (shape.queryless === true && flags.select !== undefined) {
468
+ throw new AxiError("Graph documents no $select on this read; whole rows are returned and projected locally with --fields", "VALIDATION_ERROR", [help]);
469
+ }
470
+ const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
471
+ const savedSelect = saved?.$select;
472
+ const { select, fields } = shape.queryless === true
473
+ ? { select: [], fields: flags.fields === undefined ? [...shape.defaultSelect] : fieldList(flags.fields, "fields", shape.known, shape.knownList, help) }
474
+ : selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
475
+ const savedFilter = saved?.$filter;
476
+ const filter = flags.filter === undefined ? savedFilter : String(flags.filter);
477
+ const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
478
+ const full = flags.full === true;
479
+ // These lists document $filter (and $select, except the queryless
480
+ // subject list), so --filter passes through as plain $filter with no
481
+ // $count or ConsistencyLevel contract; $search/$orderby/$expand stay
482
+ // unreviewed and $top/$skip stay server-side paging concerns. Resumed
483
+ // queries replay the stored $select verbatim so resume never conflicts.
484
+ // Queryless lists never send $select: rows arrive whole and --fields
485
+ // projects locally.
486
+ let sendSelect = [];
487
+ if (shape.queryless !== true) {
488
+ sendSelect = cursor === undefined
489
+ ? select
490
+ : saved?.$select === undefined ? [] : fieldList(saved.$select, "select", shape.known, shape.knownList, help);
491
+ }
492
+ const query = {};
493
+ if (sendSelect.length)
494
+ query.$select = sendSelect.join(",");
495
+ if (filter !== undefined)
496
+ query.$filter = filter;
497
+ const args = { profile, operation, params, query, scopes };
498
+ if (cursor !== undefined)
499
+ args.cursor = cursor;
500
+ if (flags.all === true) {
501
+ if (flags.limit !== undefined)
502
+ throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
503
+ }
504
+ else {
505
+ args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
506
+ }
507
+ const result = await withGuidance(() => session.collect(args), shape.hints);
508
+ const effectiveFlags = { ...flags, ...(result.query.$select === undefined ? {} : { select: select.join(",") }) };
509
+ if (result.query.$filter !== undefined)
510
+ effectiveFlags.filter = result.query.$filter;
511
+ const rows = [];
512
+ let truncated = false;
513
+ for (const row of result.value) {
514
+ const projected = project(row, fields, full);
515
+ rows.push(projected.row);
516
+ truncated = truncated || projected.truncated;
517
+ }
518
+ const truncationHints = truncated ? [fullHint(shape.command, effectiveFlags, profileName)] : [];
519
+ if (truncated && cursor !== undefined)
520
+ truncationHints.push("Supply the original input cursor on stdin to replay this result with --full");
521
+ const resume = resumeHint ?? `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`;
522
+ const standing = [...shape.standing(profileName)];
523
+ if (!result.complete) {
524
+ return {
525
+ [shape.key]: rows,
526
+ count: { returned: rows.length, complete: false, reason: result.reason },
527
+ cursor: result.cursor,
528
+ help: [...truncationHints, resume, ...standing],
529
+ };
530
+ }
531
+ const count = { returned: rows.length, complete: true };
532
+ if (!rows.length) {
533
+ return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
534
+ }
535
+ return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
536
+ }
537
+ async function showOne(key, command, known, knownList, defaultSelect, defaultScopes, hints, session, flags, profile, operation, help, profileName, params, malformed, queryless = false) {
538
+ if (queryless && flags.select !== undefined) {
539
+ throw new AxiError("Graph documents no query parameters on this read; whole rows are always returned", "VALIDATION_ERROR", [help]);
540
+ }
541
+ const { select, fields } = queryless
542
+ ? { select: [...defaultSelect], fields: flags.fields === undefined ? [...defaultSelect] : fieldList(flags.fields, "fields", known, knownList, help) }
543
+ : selectedFields(flags, defaultSelect, known, knownList, help);
544
+ const scopes = scopesFor(flags, profile, defaultScopes, help);
545
+ const full = flags.full === true;
546
+ // Singles document $select only (the template get documents no query
547
+ // parameters at all and arrives whole); $expand stays out everywhere:
548
+ // tasks, createdBy, lastModifiedBy and previewScope belong to later
549
+ // slices, never to these reads.
550
+ const query = {};
551
+ if (!queryless && select.length)
552
+ query.$select = select.join(",");
553
+ const raw = await withGuidance(() => session.execute({
554
+ profile,
555
+ operation,
556
+ params,
557
+ query,
558
+ scopes,
559
+ }), hints);
560
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
561
+ throw new AxiError(malformed, "GRAPH_ERROR", [
562
+ "Single-object reads carry one object; treat anything else as unknown, not empty",
563
+ ]);
564
+ }
565
+ const { row, truncated } = project(raw, fields, full);
566
+ if (truncated)
567
+ return { [key]: row, help: [fullHint(command, flags, profileName)] };
568
+ return { [key]: row };
569
+ }
570
+ async function countCollection(key, emptyNote, defaultScopes, hints, session, flags, profile, operation, help, _profileName, params = {}) {
571
+ const scopes = scopesFor(flags, profile, defaultScopes, help);
572
+ // The $count route returns a text/plain integer scalar rather than a
573
+ // JSON collection, so the leaf reads it through session.execute with
574
+ // scalar mode and accepts only a non-negative integer. The $count route
575
+ // carries no operation-level documentation page and takes no
576
+ // --filter/--select/--limit/--cursor.
577
+ const raw = await withGuidance(() => session.execute({ profile, operation, params, scopes, scalar: true }), hints);
578
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
579
+ throw new AxiError(`Graph returned a malformed ${key} count body`, "GRAPH_ERROR", [
580
+ "Counts carry one non-negative integer scalar; treat anything else as unknown, not empty",
581
+ ]);
582
+ }
583
+ const count = { returned: raw, complete: true };
584
+ return raw === 0 ? { count, help: [emptyNote] } : { count };
585
+ }
586
+ export async function listWorkflows(session, flags, profile, operation, help, profileName) {
587
+ return listCollection(WORKFLOW_LIST, session, flags, profile, operation, help, profileName);
588
+ }
589
+ export async function showWorkflow(session, flags, profile, operation, help, profileName) {
590
+ return showOne("workflow", "entra lifecycle workflow show", WORKFLOW_KNOWN, KNOWN_WORKFLOW_FIELDS, DEFAULT_WORKFLOW_SHOW_SELECT, DEFAULT_WORKFLOW_SCOPES, WORKFLOW_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": String(flags.id) }, "Graph returned a malformed lifecycle workflow body");
591
+ }
592
+ export async function countWorkflows(session, flags, profile, operation, help, profileName) {
593
+ return countCollection("workflow", WORKFLOW_LIST.emptyNote, DEFAULT_WORKFLOW_SCOPES, WORKFLOW_DENIAL_HINTS, session, flags, profile, operation, help, profileName);
594
+ }
595
+ export async function listWorkflowTemplates(session, flags, profile, operation, help, profileName) {
596
+ return listCollection(WORKFLOW_TEMPLATE_LIST, session, flags, profile, operation, help, profileName);
597
+ }
598
+ export async function showWorkflowTemplate(session, flags, profile, operation, help, profileName) {
599
+ return showOne("workflowTemplate", "entra lifecycle workflow-template show", WORKFLOW_TEMPLATE_KNOWN, KNOWN_WORKFLOW_TEMPLATE_FIELDS, DEFAULT_WORKFLOW_TEMPLATE_SHOW_SELECT, DEFAULT_LIFECYCLE_SCOPES, LIFECYCLE_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflowTemplate-id": String(flags.id) }, "Graph returned a malformed workflow template body", true);
600
+ }
601
+ export async function countWorkflowTemplates(session, flags, profile, operation, help, profileName) {
602
+ return countCollection("workflow template", WORKFLOW_TEMPLATE_LIST.emptyNote, DEFAULT_LIFECYCLE_SCOPES, LIFECYCLE_DENIAL_HINTS, session, flags, profile, operation, help, profileName);
603
+ }
604
+ export async function listTaskDefinitions(session, flags, profile, operation, help, profileName) {
605
+ return listCollection(TASK_DEFINITION_LIST, session, flags, profile, operation, help, profileName);
606
+ }
607
+ export async function showTaskDefinition(session, flags, profile, operation, help, profileName) {
608
+ return showOne("taskDefinition", "entra lifecycle task-definition show", TASK_DEFINITION_KNOWN, KNOWN_TASK_DEFINITION_FIELDS, DEFAULT_TASK_DEFINITION_SHOW_SELECT, DEFAULT_LIFECYCLE_SCOPES, LIFECYCLE_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "taskDefinition-id": String(flags.id) }, "Graph returned a malformed task definition body");
609
+ }
610
+ export async function countTaskDefinitions(session, flags, profile, operation, help, profileName) {
611
+ return countCollection("task definition", TASK_DEFINITION_LIST.emptyNote, DEFAULT_LIFECYCLE_SCOPES, LIFECYCLE_DENIAL_HINTS, session, flags, profile, operation, help, profileName);
612
+ }
613
+ export async function showLifecycleSettings(session, flags, profile, operation, help, profileName) {
614
+ return showOne("settings", "entra lifecycle settings show", LIFECYCLE_SETTINGS_KNOWN, KNOWN_LIFECYCLE_SETTINGS_FIELDS, DEFAULT_LIFECYCLE_SETTINGS_SELECT, DEFAULT_LIFECYCLE_SCOPES, LIFECYCLE_DENIAL_HINTS, session, flags, profile, operation, help, profileName, {}, "Graph returned a malformed lifecycle settings body");
615
+ }
616
+ export async function listRuns(session, flags, profile, operation, help, profileName) {
617
+ const workflow = workflowId(flags, help);
618
+ return listCollection(RUN_LIST, session, flags, profile, operation, help, profileName, { "workflow-id": workflow }, `Resume losslessly with the same --workflow and flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
619
+ }
620
+ export async function showRun(session, flags, profile, operation, help, profileName) {
621
+ const workflow = workflowId(flags, help);
622
+ return showOne("run", "entra lifecycle run show", RUN_KNOWN, KNOWN_RUN_FIELDS, DEFAULT_RUN_SHOW_SELECT, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow, "run-id": String(flags.id) }, "Graph returned a malformed lifecycle run body");
623
+ }
624
+ export async function countRuns(session, flags, profile, operation, help, profileName) {
625
+ const workflow = workflowId(flags, help);
626
+ return countCollection("run", RUN_LIST.emptyNote, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow });
627
+ }
628
+ export async function listUserProcessingResults(session, flags, profile, operation, help, profileName) {
629
+ const workflow = workflowId(flags, help);
630
+ return listCollection(USER_PROCESSING_LIST, session, flags, profile, operation, help, profileName, { "workflow-id": workflow }, `Resume losslessly with the same --workflow and flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
631
+ }
632
+ export async function showUserProcessingResult(session, flags, profile, operation, help, profileName) {
633
+ const workflow = workflowId(flags, help);
634
+ return showOne("userProcessingResult", "entra lifecycle user-processing-result show", USER_PROCESSING_KNOWN, KNOWN_USER_PROCESSING_FIELDS, DEFAULT_USER_PROCESSING_SHOW_SELECT, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow, "userProcessingResult-id": String(flags.id) }, "Graph returned a malformed user processing result body");
635
+ }
636
+ export async function countUserProcessingResults(session, flags, profile, operation, help, profileName) {
637
+ const workflow = workflowId(flags, help);
638
+ return countCollection("user processing result", USER_PROCESSING_LIST.emptyNote, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow });
639
+ }
640
+ export async function listSubjectProcessingResults(session, flags, profile, operation, help, profileName) {
641
+ const workflow = workflowId(flags, help);
642
+ return listCollection(SUBJECT_PROCESSING_LIST, session, flags, profile, operation, help, profileName, { "workflow-id": workflow }, `Resume losslessly with the same --workflow and flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
643
+ }
644
+ export async function showSubjectProcessingResult(session, flags, profile, operation, help, profileName) {
645
+ const workflow = workflowId(flags, help);
646
+ return showOne("subjectProcessingResult", "entra lifecycle subject-processing-result show", SUBJECT_PROCESSING_KNOWN, KNOWN_SUBJECT_PROCESSING_FIELDS, DEFAULT_SUBJECT_PROCESSING_FIELDS, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow, "subjectProcessingResult-id": String(flags.id) }, "Graph returned a malformed subject processing result body", true);
647
+ }
648
+ export async function countSubjectProcessingResults(session, flags, profile, operation, help, profileName) {
649
+ const workflow = workflowId(flags, help);
650
+ return countCollection("subject processing result", SUBJECT_PROCESSING_LIST.emptyNote, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow });
651
+ }
652
+ export async function listTaskReports(session, flags, profile, operation, help, profileName) {
653
+ const workflow = workflowId(flags, help);
654
+ return listCollection(TASK_REPORT_LIST, session, flags, profile, operation, help, profileName, { "workflow-id": workflow }, `Resume losslessly with the same --workflow and flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`);
655
+ }
656
+ export async function showTaskReport(session, flags, profile, operation, help, profileName) {
657
+ const workflow = workflowId(flags, help);
658
+ return showOne("taskReport", "entra lifecycle task-report show", TASK_REPORT_KNOWN, KNOWN_TASK_REPORT_FIELDS, DEFAULT_TASK_REPORT_SHOW_SELECT, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow, "taskReport-id": String(flags.id) }, "Graph returned a malformed task report body", true);
659
+ }
660
+ export async function countTaskReports(session, flags, profile, operation, help, profileName) {
661
+ const workflow = workflowId(flags, help);
662
+ return countCollection("task report", TASK_REPORT_LIST.emptyNote, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow });
663
+ }
664
+ export async function showTaskReportTask(session, flags, profile, operation, help, profileName) {
665
+ const workflow = workflowId(flags, help);
666
+ const report = reportId(flags, help);
667
+ return showOne("task", "entra lifecycle task-report task show", TASK_KNOWN, KNOWN_TASK_FIELDS, DEFAULT_TASK_SHOW_SELECT, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow, "taskReport-id": report }, "Graph returned a malformed lifecycle task body", true);
668
+ }
669
+ export async function showTaskReportTaskDefinition(session, flags, profile, operation, help, profileName) {
670
+ const workflow = workflowId(flags, help);
671
+ const report = reportId(flags, help);
672
+ return showOne("taskDefinition", "entra lifecycle task-report task-definition show", TASK_DEFINITION_KNOWN, KNOWN_TASK_DEFINITION_FIELDS, DEFAULT_TASK_DEFINITION_SHOW_SELECT, DEFAULT_REPORTS_SCOPES, REPORTS_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "workflow-id": workflow, "taskReport-id": report }, "Graph returned a malformed task definition body", true);
673
+ }