@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,152 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { resolveSessionOperation } from "./graph-session.js";
3
+ // EXT-04e tenant-lookup subfamily: the read mapping behind
4
+ // `mg-axi entra tenant-information show --domain-name <domain>` and
5
+ // `mg-axi entra tenant-information show --tenant-id <tenant-guid>`.
6
+ // Operation construction stays beside its command; the shared session owns
7
+ // URLs, credentials, retries and error translation, and the SDK owns TOON
8
+ // rendering. This module only maps flags to session calls and projects rows
9
+ // for compact output.
10
+ //
11
+ // Reviewed against the v1.0 findTenantInformationByDomainName,
12
+ // findTenantInformationByTenantId and tenantInformation resource
13
+ // documentation on 2026-10-05. Every read takes D/A
14
+ // CrossTenantInformation.ReadBasic.All as least privilege; delegated
15
+ // callers pass it as --scopes while application callers need it
16
+ // admin-consented on the configured .default audience. The operation pages
17
+ // state no Entra role prerequisite and personal Microsoft accounts are not
18
+ // supported. No P1/P2 prerequisite is stated for these reads. The pages
19
+ // document no query parameters, so the leaf offers no --select/--fields/--
20
+ // filter/--limit/--cursor and always returns the full reviewed
21
+ // tenantInformation set. The addresses are function segments, so the key
22
+ // reaches the session only as an explicit CLI flag and the allowlisted
23
+ // session binding validates, OData-quotes and encodes it; any other value
24
+ // fails before credentials. Beta stays out. No mutation lives here.
25
+ // Every tenant-information property this slice may display, matching the
26
+ // reviewed tenantInformation resource. Anything else fails before
27
+ // credentials.
28
+ export const KNOWN_TENANT_INFORMATION_FIELDS = [
29
+ "defaultDomainName",
30
+ "displayName",
31
+ "federationBrandName",
32
+ "tenantId",
33
+ ];
34
+ // Delegated defaults are operation-specific; application profiles use their
35
+ // configured .default audience and reject --scopes.
36
+ export const DEFAULT_TENANT_INFORMATION_SCOPES = ["https://graph.microsoft.com/CrossTenantInformation.ReadBasic.All"];
37
+ const TRUNCATE_AT = 500;
38
+ function scopesFor(flags, defaults, profile, help) {
39
+ if (profile.mode === "application") {
40
+ if (flags.scopes !== undefined) {
41
+ throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
42
+ }
43
+ return undefined;
44
+ }
45
+ if (flags.scopes === undefined)
46
+ return [...defaults];
47
+ const scopes = String(flags.scopes)
48
+ .split(",")
49
+ .map(scope => scope.trim())
50
+ .filter(scope => scope.length > 0);
51
+ return scopes;
52
+ }
53
+ function truncateValue(value, full) {
54
+ const truncate = (text) => full || text.length <= TRUNCATE_AT
55
+ ? { value: text, truncated: false }
56
+ : { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
57
+ if (typeof value === "string")
58
+ return truncate(value);
59
+ if (Array.isArray(value) && value.every(entry => typeof entry === "string")) {
60
+ const projected = value.map(truncate);
61
+ return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
62
+ }
63
+ return { value, truncated: false };
64
+ }
65
+ // Local projection preserves Graph's null/missing distinction: an explicit
66
+ // null stays null, an absent property stays absent and is never synthesized.
67
+ function project(row, fields, full) {
68
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
69
+ const projected = {};
70
+ let truncated = false;
71
+ for (const field of fields) {
72
+ if (!Object.hasOwn(source, field))
73
+ continue;
74
+ const result = truncateValue(source[field], full);
75
+ projected[field] = result.value;
76
+ truncated = truncated || result.truncated;
77
+ }
78
+ return { row: projected, truncated };
79
+ }
80
+ function profileHint(profileName) {
81
+ return `--profile ${shellValue(profileName)}`;
82
+ }
83
+ function shellValue(value) {
84
+ return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
85
+ }
86
+ function fullHint(command, flags, profileName) {
87
+ const args = Object.entries({ ...flags, profile: profileName, full: true })
88
+ .map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
89
+ return `mg-axi ${command} ${args.join(" ")}`;
90
+ }
91
+ // Denials carry the session's generic grant/role/licence cause; each
92
+ // operation adds the scope and prerequisites that actually unlock it,
93
+ // because a 403 alone never says which prerequisite is missing.
94
+ function withGuidance(hints, run) {
95
+ return run().catch(error => {
96
+ if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
97
+ throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
98
+ }
99
+ throw error;
100
+ });
101
+ }
102
+ const TENANT_INFORMATION_DENIAL_HINTS = [
103
+ "Tenant-information lookups need CrossTenantInformation.ReadBasic.All, passed as --scopes for delegated access or admin-consented for application access",
104
+ "No Entra role is required for tenant-information lookups; a 403 names a missing grant, not a missing role",
105
+ "Personal Microsoft accounts are not supported for tenant-information lookups",
106
+ "No P1/P2 prerequisite is stated for tenant-information lookups; never diagnose licence solely from HTTP 403",
107
+ ];
108
+ // Exactly one lookup key selects the function route; the value itself is
109
+ // validated by the allowlisted session binding before credentials. The
110
+ // domain operation arrives catalogue-resolved; the tenant-ID route resolves
111
+ // from the same inventory authority beside its command.
112
+ function lookupTarget(flags, domainOperation, help) {
113
+ const domain = flags["domain-name"];
114
+ const tenantId = flags["tenant-id"];
115
+ if (domain !== undefined && tenantId !== undefined) {
116
+ throw new AxiError("--domain-name and --tenant-id cannot be combined", "VALIDATION_ERROR", [help]);
117
+ }
118
+ if (domain !== undefined) {
119
+ if (!String(domain).trim())
120
+ throw new AxiError("--domain-name needs the tenant domain name", "VALIDATION_ERROR", [help]);
121
+ return { operation: domainOperation, params: { domainName: String(domain) } };
122
+ }
123
+ if (tenantId !== undefined) {
124
+ if (!String(tenantId).trim())
125
+ throw new AxiError("--tenant-id needs the tenant GUID", "VALIDATION_ERROR", [help]);
126
+ return {
127
+ operation: resolveSessionOperation("v1.0", "GET", "/tenantRelationships/findTenantInformationByTenantId(tenantId='{tenantId}')"),
128
+ params: { tenantId: String(tenantId) },
129
+ };
130
+ }
131
+ throw new AxiError("Tenant-information show needs exactly one of --domain-name or --tenant-id", "VALIDATION_ERROR", [help]);
132
+ }
133
+ export async function showTenantInformation(session, flags, profile, operation, help, profileName) {
134
+ const target = lookupTarget(flags, operation, help);
135
+ const scopes = scopesFor(flags, DEFAULT_TENANT_INFORMATION_SCOPES, profile, help);
136
+ const full = flags.full === true;
137
+ const raw = await withGuidance(TENANT_INFORMATION_DENIAL_HINTS, () => session.execute({
138
+ profile,
139
+ operation: target.operation,
140
+ params: target.params,
141
+ scopes,
142
+ }));
143
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
144
+ throw new AxiError("Graph returned a malformed tenant-information body", "GRAPH_ERROR", [
145
+ "Tenant-information lookups carry one tenantInformation object; treat anything else as unknown, not empty",
146
+ ]);
147
+ }
148
+ const { row, truncated } = project(raw, KNOWN_TENANT_INFORMATION_FIELDS, full);
149
+ if (truncated)
150
+ return { tenantInformation: row, help: [fullHint("entra tenant-information show", flags, profileName)] };
151
+ return { tenantInformation: row };
152
+ }
@@ -0,0 +1,380 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ // EXT-01 terms-of-use subfamily: the read mapping behind
3
+ // `mg-axi entra agreement list/show`,
4
+ // `mg-axi entra agreement acceptance list/show --agreement <agreement-id>`
5
+ // and `mg-axi entra agreement-acceptance list/show`. Operation construction
6
+ // stays beside its command; the shared session owns URLs, credentials,
7
+ // paging, retries and error translation, and the SDK owns TOON rendering.
8
+ // This module only maps flags to session calls and projects rows for compact
9
+ // output.
10
+ //
11
+ // Reviewed against the v1.0 termsofusecontainer-list-agreements,
12
+ // agreement-get and agreement-list-acceptances operation documentation and
13
+ // the agreement and agreementAcceptance resource references on 2026-10-05,
14
+ // corroborated by the Microsoft Entra terms-of-use feature documentation
15
+ // (same P1 prerequisite, same Security Reader read role). The legacy
16
+ // /agreements and /agreementAcceptances roots share the termsOfUse
17
+ // operation contract documented on the /identityGovernance/termsOfUse
18
+ // routes; the root acceptance list and get carry no dedicated operation
19
+ // page, so they follow the reviewed acceptance-list contract and the
20
+ // agreementAcceptance resource reference. Agreement reads take delegated
21
+ // Agreement.Read.All; acceptance reads take delegated
22
+ // AgreementAcceptance.Read least-privileged (AgreementAcceptance.Read.All
23
+ // is the documented higher-privileged alternative). Graph documents no
24
+ // supported application permission for any of these operations, so
25
+ // application profiles are refused before credentials rather than sent to
26
+ // a certain denial. Delegated callers additionally need Security Reader,
27
+ // the least-privileged supported role (Global Reader, Conditional Access
28
+ // Administrator and Security Administrator also work). Personal Microsoft
29
+ // accounts are not supported. Terms of use needs Microsoft Entra ID P1.
30
+ // The agreement list documents $select, $filter and $top; the agreement
31
+ // get documents $select only, so it offers no --filter. The nested
32
+ // acceptance list documents $select and $filter; the root acceptance list
33
+ // follows the same reviewed contract. --filter passes through as plain
34
+ // $filter with no $count or ConsistencyLevel contract. The acceptances
35
+ // $count scalar, every agreement file/localization/files/versions
36
+ // sub-read, beta and every mutation stay out. No mutation lives here.
37
+ //
38
+ // Secrecy by construction: agreement file and files navigation properties
39
+ // are never requested ($expand is not reviewed here) and file bytes are
40
+ // never downloaded or printed; only agreement metadata is projected.
41
+ // Acceptance records are personal data, so default rows stay minimal (id,
42
+ // agreementId, state, recordedDateTime) and the identifying fields
43
+ // (userDisplayName, userEmail, userPrincipalName, userId and device
44
+ // detail) need an explicit --select naming them.
45
+ // Every agreement property this slice may request or display, matching the
46
+ // reviewed resource. The acceptances, file and files relationships are
47
+ // never projected here. Anything else fails before credentials.
48
+ export const KNOWN_AGREEMENT_FIELDS = [
49
+ "displayName",
50
+ "id",
51
+ "isPerDeviceAcceptanceRequired",
52
+ "isViewingBeforeAcceptanceRequired",
53
+ "termsExpiration",
54
+ "userReacceptRequiredFrequency",
55
+ ];
56
+ const KNOWN_AGREEMENTS = new Set(KNOWN_AGREEMENT_FIELDS);
57
+ // Every acceptance property this slice may request or display, matching
58
+ // the reviewed resource. Anything else fails before credentials.
59
+ export const KNOWN_ACCEPTANCE_FIELDS = [
60
+ "agreementFileId",
61
+ "agreementId",
62
+ "deviceDisplayName",
63
+ "deviceId",
64
+ "deviceOSType",
65
+ "deviceOSVersion",
66
+ "expirationDateTime",
67
+ "id",
68
+ "recordedDateTime",
69
+ "state",
70
+ "userDisplayName",
71
+ "userEmail",
72
+ "userId",
73
+ "userPrincipalName",
74
+ ];
75
+ const KNOWN_ACCEPTANCES = new Set(KNOWN_ACCEPTANCE_FIELDS);
76
+ // Compact agreement rows: the identifier plus the internal tracking name
77
+ // agents need next.
78
+ const DEFAULT_AGREEMENT_LIST_SELECT = ["id", "displayName"];
79
+ // Show rows: the full reviewed agreement metadata set.
80
+ const DEFAULT_AGREEMENT_SHOW_SELECT = [...KNOWN_AGREEMENT_FIELDS];
81
+ // Acceptance rows stay minimal by default because they are personal data:
82
+ // the identifier, the owning agreement, the current state and when it was
83
+ // recorded. Identifying fields need an explicit --select.
84
+ const DEFAULT_ACCEPTANCE_SELECT = ["id", "agreementId", "state", "recordedDateTime"];
85
+ // Delegated defaults are operation-specific; application profiles are
86
+ // refused before credentials because Graph documents no supported
87
+ // application permission here.
88
+ export const DEFAULT_AGREEMENT_SCOPES = ["https://graph.microsoft.com/Agreement.Read.All"];
89
+ export const DEFAULT_ACCEPTANCE_SCOPES = ["https://graph.microsoft.com/AgreementAcceptance.Read"];
90
+ const TRUNCATE_AT = 500;
91
+ function fieldList(raw, known, knownList, flag, help, noun) {
92
+ const parts = String(raw)
93
+ .split(",")
94
+ .map(part => part.trim())
95
+ .filter(part => part.length > 0);
96
+ if (!parts.length)
97
+ throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
98
+ const fields = [];
99
+ for (const part of parts) {
100
+ if (!known.has(part)) {
101
+ throw new AxiError(`Unknown terms-of-use property ${part} in --${flag}`, "VALIDATION_ERROR", [
102
+ help,
103
+ `Known ${noun} properties: ${knownList.join(", ")}`,
104
+ ]);
105
+ }
106
+ if (!fields.includes(part))
107
+ fields.push(part);
108
+ }
109
+ return fields;
110
+ }
111
+ function scopesFor(flags, defaults, profile, help, noun) {
112
+ if (profile.mode === "application") {
113
+ throw new AxiError(`${noun} reads need a delegated profile; Graph documents no supported application permission for this operation`, "VALIDATION_ERROR", [help, "mg-axi profile create --name <name> --tenant <tenant-id> --client <client-id> --cloud commercial"]);
114
+ }
115
+ if (flags.scopes === undefined)
116
+ return [...defaults];
117
+ const scopes = String(flags.scopes)
118
+ .split(",")
119
+ .map(scope => scope.trim())
120
+ .filter(scope => scope.length > 0);
121
+ return scopes;
122
+ }
123
+ function truncateValue(value, full) {
124
+ const truncate = (text) => full || text.length <= TRUNCATE_AT
125
+ ? { value: text, truncated: false }
126
+ : { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
127
+ if (typeof value === "string")
128
+ return truncate(value);
129
+ if (Array.isArray(value) && value.every(entry => typeof entry === "string")) {
130
+ const projected = value.map(truncate);
131
+ return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
132
+ }
133
+ return { value, truncated: false };
134
+ }
135
+ // Local projection preserves Graph's null/missing distinction: an explicit
136
+ // null stays null, an absent property stays absent and is never synthesized.
137
+ function project(row, fields, full) {
138
+ const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
139
+ const projected = {};
140
+ let truncated = false;
141
+ for (const field of fields) {
142
+ if (!Object.hasOwn(source, field))
143
+ continue;
144
+ const result = truncateValue(source[field], full);
145
+ projected[field] = result.value;
146
+ truncated = truncated || result.truncated;
147
+ }
148
+ return { row: projected, truncated };
149
+ }
150
+ function selectedFields(flags, known, knownList, defaults, help, noun) {
151
+ const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, known, knownList, "select", help, noun);
152
+ const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, known, knownList, "fields", help, noun);
153
+ const missing = fields.find(field => !select.includes(field));
154
+ if (missing) {
155
+ throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
156
+ }
157
+ return { select, fields };
158
+ }
159
+ function profileHint(profileName) {
160
+ return `--profile ${shellValue(profileName)}`;
161
+ }
162
+ function shellValue(value) {
163
+ return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
164
+ }
165
+ function fullHint(command, flags, profileName) {
166
+ const args = Object.entries({ ...flags, profile: profileName, full: true })
167
+ .map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
168
+ return `mg-axi ${command} ${args.join(" ")}`;
169
+ }
170
+ // Denials carry the session's generic grant/role/licence cause; each
171
+ // operation adds the scope, roles and licensing that actually unlock it,
172
+ // because a 403 alone never says which prerequisite is missing.
173
+ function withGuidance(hints, run) {
174
+ return run().catch(error => {
175
+ if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
176
+ throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
177
+ }
178
+ throw error;
179
+ });
180
+ }
181
+ const AGREEMENT_DENIAL_HINTS = [
182
+ "Agreement reads need delegated Agreement.Read.All; delegated callers pass it as --scopes",
183
+ "Delegated reads additionally need Security Reader, the least-privileged supported Entra role for this operation (Global Reader, Conditional Access Administrator or Security Administrator also work); Graph documents no supported application permission, so application profiles are refused before credentials",
184
+ "Personal Microsoft accounts are not supported for agreement reads",
185
+ "Terms of use needs Microsoft Entra ID P1; never diagnose licence solely from HTTP 403",
186
+ ];
187
+ const ACCEPTANCE_DENIAL_HINTS = [
188
+ "Acceptance reads need delegated AgreementAcceptance.Read least-privileged (AgreementAcceptance.Read.All is the documented higher-privileged alternative); delegated callers pass one as --scopes",
189
+ "Delegated reads additionally need Security Reader, the least-privileged supported Entra role for this operation (Global Reader, Conditional Access Administrator or Security Administrator also work); Graph documents no supported application permission, so application profiles are refused before credentials",
190
+ "Personal Microsoft accounts are not supported for acceptance reads",
191
+ "Terms of use needs Microsoft Entra ID P1; never diagnose licence solely from HTTP 403",
192
+ ];
193
+ // Restoring the saved select/filter keeps cursor resumes lossless when
194
+ // --select or --filter is omitted. Both lists document $filter, so
195
+ // --filter passes through as plain $filter with no $count or
196
+ // ConsistencyLevel contract; strict input validation refuses the flag on
197
+ // the show leaves before credentials instead of forwarding a misleading
198
+ // request.
199
+ function collectionCommon(session, flags, known, knownList, defaults, noun, defaultScopes, denialNoun, operation, help, profile) {
200
+ const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
201
+ if (cursor !== undefined && !cursor.trim())
202
+ throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
203
+ const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
204
+ const { select, fields } = selectedFields(flags, known, knownList, saved?.["$select"] === undefined ? defaults : fieldList(saved["$select"], known, knownList, "select", help, noun), help, noun);
205
+ const filter = flags.filter === undefined ? saved?.["$filter"] : String(flags.filter);
206
+ return { cursor, select, fields, scopes: scopesFor(flags, defaultScopes, profile, help, denialNoun), full: flags.full === true, filter };
207
+ }
208
+ function collectArgs(profile, operation, common, flags, help, params) {
209
+ const query = { $select: common.select.join(",") };
210
+ if (common.filter !== undefined)
211
+ query.$filter = common.filter;
212
+ const args = { profile, operation, query, scopes: common.scopes };
213
+ if (params !== undefined)
214
+ args.params = params;
215
+ if (common.cursor !== undefined)
216
+ args.cursor = common.cursor;
217
+ if (flags.all === true) {
218
+ if (flags.limit !== undefined)
219
+ throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
220
+ }
221
+ else {
222
+ args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
223
+ }
224
+ return args;
225
+ }
226
+ function resumeHint(profileName) {
227
+ return `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`;
228
+ }
229
+ function agreementId(flags, help, flag = "id") {
230
+ const id = String(flags[flag]);
231
+ if (!id.trim())
232
+ throw new AxiError(`--${flag} needs the agreement identifier`, "VALIDATION_ERROR", [help]);
233
+ return id;
234
+ }
235
+ function acceptanceId(flags, help) {
236
+ const id = String(flags.id);
237
+ if (!id.trim())
238
+ throw new AxiError("--id needs the agreement-acceptance identifier", "VALIDATION_ERROR", [help]);
239
+ return id;
240
+ }
241
+ function singleResult(raw, fields, full, kind) {
242
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
243
+ throw new AxiError(`Graph returned a malformed ${kind} body`, "GRAPH_ERROR", [
244
+ `Single-${kind} reads carry one ${kind} object; treat anything else as unknown, not empty`,
245
+ ]);
246
+ }
247
+ return project(raw, fields, full);
248
+ }
249
+ export async function listAgreements(session, flags, profile, operation, help, profileName) {
250
+ const common = collectionCommon(session, flags, KNOWN_AGREEMENTS, KNOWN_AGREEMENT_FIELDS, DEFAULT_AGREEMENT_LIST_SELECT, "agreement", DEFAULT_AGREEMENT_SCOPES, "Agreement", operation, help, profile);
251
+ const result = await withGuidance(AGREEMENT_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
252
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_AGREEMENT_LIST_SELECT.join(",") };
253
+ if (result.query.$filter !== undefined)
254
+ effectiveFlags.filter = result.query.$filter;
255
+ const agreements = [];
256
+ let truncated = false;
257
+ for (const row of result.value) {
258
+ const projected = project(row, common.fields, common.full);
259
+ agreements.push(projected.row);
260
+ truncated = truncated || projected.truncated;
261
+ }
262
+ const showHint = `mg-axi entra agreement show --id <agreement-id> ${profileHint(profileName)}`;
263
+ const truncationHints = truncated ? [fullHint("entra agreement list", effectiveFlags, profileName)] : [];
264
+ if (!result.complete) {
265
+ return {
266
+ agreements,
267
+ count: { returned: agreements.length, complete: false, reason: result.reason },
268
+ cursor: result.cursor,
269
+ help: [...truncationHints, resumeHint(profileName), showHint],
270
+ };
271
+ }
272
+ const count = { returned: agreements.length, complete: true };
273
+ if (!agreements.length) {
274
+ return {
275
+ agreements,
276
+ count,
277
+ help: ["0 agreements matched; no terms-of-use agreements are configured for the tenant, or no row passed --filter"],
278
+ };
279
+ }
280
+ return { agreements, count, help: [...truncationHints, showHint] };
281
+ }
282
+ export async function showAgreement(session, flags, profile, operation, help, profileName) {
283
+ const { select, fields } = selectedFields(flags, KNOWN_AGREEMENTS, KNOWN_AGREEMENT_FIELDS, DEFAULT_AGREEMENT_SHOW_SELECT, help, "agreement");
284
+ const scopes = scopesFor(flags, DEFAULT_AGREEMENT_SCOPES, profile, help, "Agreement");
285
+ const full = flags.full === true;
286
+ const id = agreementId(flags, help);
287
+ const raw = await withGuidance(AGREEMENT_DENIAL_HINTS, () => session.execute({
288
+ profile,
289
+ operation,
290
+ params: { "agreement-id": id },
291
+ query: { $select: select.join(",") },
292
+ scopes,
293
+ }));
294
+ const { row, truncated } = singleResult(raw, fields, full, "agreement");
295
+ const helpHints = [
296
+ ...(truncated ? [fullHint("entra agreement show", flags, profileName)] : []),
297
+ `mg-axi entra agreement acceptance list --agreement ${shellValue(id)} ${profileHint(profileName)}`,
298
+ ];
299
+ return { agreement: row, help: helpHints };
300
+ }
301
+ function acceptanceCollection(session, flags, operation, help, profile, agreement) {
302
+ const common = collectionCommon(session, flags, KNOWN_ACCEPTANCES, KNOWN_ACCEPTANCE_FIELDS, DEFAULT_ACCEPTANCE_SELECT, "agreement-acceptance", DEFAULT_ACCEPTANCE_SCOPES, "Acceptance", operation, help, profile);
303
+ if (agreement === undefined)
304
+ return { common, params: undefined };
305
+ if (!agreement.trim())
306
+ throw new AxiError("--agreement needs the agreement identifier", "VALIDATION_ERROR", [help]);
307
+ return { common, params: { "agreement-id": agreement } };
308
+ }
309
+ async function collectAcceptances(session, flags, profile, operation, help, profileName, command, showCommand, agreement) {
310
+ const { common, params } = acceptanceCollection(session, flags, operation, help, profile, agreement);
311
+ const result = await withGuidance(ACCEPTANCE_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help, params)));
312
+ const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_ACCEPTANCE_SELECT.join(",") };
313
+ if (result.query.$filter !== undefined)
314
+ effectiveFlags.filter = result.query.$filter;
315
+ const acceptances = [];
316
+ let truncated = false;
317
+ for (const row of result.value) {
318
+ const projected = project(row, common.fields, common.full);
319
+ acceptances.push(projected.row);
320
+ truncated = truncated || projected.truncated;
321
+ }
322
+ const scope = agreement === undefined ? "" : `--agreement ${shellValue(agreement)} `;
323
+ const showHint = `mg-axi ${showCommand} ${scope}--id <acceptance-id> ${profileHint(profileName)}`;
324
+ const truncationHints = truncated ? [fullHint(command, effectiveFlags, profileName)] : [];
325
+ if (!result.complete) {
326
+ return {
327
+ agreementAcceptances: acceptances,
328
+ count: { returned: acceptances.length, complete: false, reason: result.reason },
329
+ cursor: result.cursor,
330
+ help: [...truncationHints, resumeHint(profileName), showHint],
331
+ };
332
+ }
333
+ const count = { returned: acceptances.length, complete: true };
334
+ if (!acceptances.length) {
335
+ return {
336
+ agreementAcceptances: acceptances,
337
+ count,
338
+ help: ["0 agreement acceptances matched; nobody recorded an acceptance in scope yet, or no row passed --filter"],
339
+ };
340
+ }
341
+ return { agreementAcceptances: acceptances, count, help: [...truncationHints, showHint] };
342
+ }
343
+ async function showAcceptanceById(session, flags, profile, operation, help, profileName, command, agreement) {
344
+ const { select, fields } = selectedFields(flags, KNOWN_ACCEPTANCES, KNOWN_ACCEPTANCE_FIELDS, DEFAULT_ACCEPTANCE_SELECT, help, "agreement-acceptance");
345
+ const scopes = scopesFor(flags, DEFAULT_ACCEPTANCE_SCOPES, profile, help, "Acceptance");
346
+ const full = flags.full === true;
347
+ const id = acceptanceId(flags, help);
348
+ const params = agreement === undefined
349
+ ? { "agreementAcceptance-id": id }
350
+ : { "agreement-id": agreement, "agreementAcceptance-id": id };
351
+ const raw = await withGuidance(ACCEPTANCE_DENIAL_HINTS, () => session.execute({
352
+ profile,
353
+ operation,
354
+ params,
355
+ query: { $select: select.join(",") },
356
+ scopes,
357
+ }));
358
+ const { row, truncated } = singleResult(raw, fields, full, "agreement acceptance");
359
+ if (truncated)
360
+ return { agreementAcceptance: row, help: [fullHint(command, flags, profileName)] };
361
+ return { agreementAcceptance: row };
362
+ }
363
+ export async function listAgreementAcceptances(session, flags, profile, operation, help, profileName) {
364
+ const agreement = String(flags.agreement);
365
+ if (!agreement.trim())
366
+ throw new AxiError("--agreement needs the agreement identifier", "VALIDATION_ERROR", [help]);
367
+ return collectAcceptances(session, flags, profile, operation, help, profileName, "entra agreement acceptance list", "entra agreement acceptance show", agreement);
368
+ }
369
+ export async function showAgreementAcceptance(session, flags, profile, operation, help, profileName) {
370
+ const agreement = String(flags.agreement);
371
+ if (!agreement.trim())
372
+ throw new AxiError("--agreement needs the agreement identifier", "VALIDATION_ERROR", [help]);
373
+ return showAcceptanceById(session, flags, profile, operation, help, profileName, "entra agreement acceptance show", agreement);
374
+ }
375
+ export async function listAcceptances(session, flags, profile, operation, help, profileName) {
376
+ return collectAcceptances(session, flags, profile, operation, help, profileName, "entra agreement-acceptance list", "entra agreement-acceptance show", undefined);
377
+ }
378
+ export async function showAcceptance(session, flags, profile, operation, help, profileName) {
379
+ return showAcceptanceById(session, flags, profile, operation, help, profileName, "entra agreement-acceptance show", undefined);
380
+ }