@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.
- package/README.md +1138 -0
- package/dist/api.js +953 -0
- package/dist/app-auth.js +47 -0
- package/dist/auth.js +59 -0
- package/dist/bin/mg-axi.js +7 -0
- package/dist/catalogue.js +2191 -0
- package/dist/cli.js +1314 -0
- package/dist/docs.js +559 -0
- package/dist/doctor.js +75 -0
- package/dist/entra-access-reviews.js +425 -0
- package/dist/entra-apps.js +374 -0
- package/dist/entra-audit-logs.js +381 -0
- package/dist/entra-auth-methods.js +296 -0
- package/dist/entra-ca-policy-update.js +353 -0
- package/dist/entra-certificate-auth.js +384 -0
- package/dist/entra-conditional-access.js +317 -0
- package/dist/entra-contacts.js +827 -0
- package/dist/entra-contracts.js +252 -0
- package/dist/entra-custom-security-attributes.js +464 -0
- package/dist/entra-data-policy-operations.js +274 -0
- package/dist/entra-delegated-admin.js +468 -0
- package/dist/entra-deleted-items.js +461 -0
- package/dist/entra-directory-objects.js +274 -0
- package/dist/entra-directory.js +411 -0
- package/dist/entra-domains.js +399 -0
- package/dist/entra-entitlement-management.js +645 -0
- package/dist/entra-federation-configurations.js +290 -0
- package/dist/entra-grants.js +272 -0
- package/dist/entra-group-lifecycle.js +349 -0
- package/dist/entra-group-member-add.js +278 -0
- package/dist/entra-groups.js +336 -0
- package/dist/entra-identity-providers.js +297 -0
- package/dist/entra-lifecycle-workflows.js +673 -0
- package/dist/entra-multi-tenant-organization.js +309 -0
- package/dist/entra-on-premises-synchronization.js +231 -0
- package/dist/entra-organization.js +398 -0
- package/dist/entra-risk-dismiss.js +191 -0
- package/dist/entra-risk.js +404 -0
- package/dist/entra-roles.js +310 -0
- package/dist/entra-subscriptions.js +289 -0
- package/dist/entra-tenant-information.js +152 -0
- package/dist/entra-terms-of-use.js +380 -0
- package/dist/entra-user-revoke-sessions.js +170 -0
- package/dist/entra-user-update.js +170 -0
- package/dist/entra-users.js +211 -0
- package/dist/graph-session.js +1034 -0
- package/dist/msal-app-provider.js +42 -0
- package/dist/msal-provider.js +82 -0
- package/dist/mutations.js +529 -0
- package/dist/profiles.js +139 -0
- package/dist/setup.js +116 -0
- package/dist/version.js +2 -0
- package/docs/coverage.md +1028 -0
- package/inventory/operations.json +4 -0
- package/inventory/schema.json +71 -0
- package/package.json +32 -0
- package/skills/mg-axi/SKILL.md +293 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
// EXT-01 domains subfamily: the read mapping behind `mg-axi entra domain
|
|
3
|
+
// list/show`, the per-domain verification and service-configuration DNS
|
|
4
|
+
// record reads, and the top-level domain DNS record reads. Operation
|
|
5
|
+
// construction stays beside its command; the shared session owns URLs,
|
|
6
|
+
// credentials, paging, retries and error translation, and the SDK owns TOON
|
|
7
|
+
// rendering. This module only maps flags to session calls and projects rows
|
|
8
|
+
// for compact output.
|
|
9
|
+
//
|
|
10
|
+
// Reviewed against the v1.0 domain-list, domain-get,
|
|
11
|
+
// domain-list-verificationdnsrecords and
|
|
12
|
+
// domain-list-serviceconfigurationrecords operation documentation on
|
|
13
|
+
// 2026-10-04. Every read takes D/A Domain.Read.All; delegated callers
|
|
14
|
+
// additionally need a supported Entra role (Domain Name Administrator or
|
|
15
|
+
// Global Reader are the least-privileged for the DNS record reads, and the
|
|
16
|
+
// domain list/get support those plus Directory Readers and other directory
|
|
17
|
+
// roles). Personal Microsoft accounts are not supported. No P1/P2
|
|
18
|
+
// prerequisite is stated for these reads. The single-record GETs and the
|
|
19
|
+
// top-level /domainDnsRecords GETs carry no operation-level documentation
|
|
20
|
+
// page; they read the same domainDnsRecord resource through the same
|
|
21
|
+
// permission contract, so the review cites the parent list documentation
|
|
22
|
+
// alongside the domainDnsRecord resource reference. Domain ids are fully
|
|
23
|
+
// qualified names (for example contoso.com), not object UUIDs. Graph
|
|
24
|
+
// documents a known issue with $search, $top and $filter on domain lists,
|
|
25
|
+
// so `domain list` offers no --filter; the DNS record collections document
|
|
26
|
+
// plain OData parameters and --filter passes through as plain $filter with
|
|
27
|
+
// no $count or ConsistencyLevel contract. No domain mutation exists in this
|
|
28
|
+
// slice: verify, forceDelete, promote, federation and shared-email surfaces
|
|
29
|
+
// belong to later pieces.
|
|
30
|
+
// Every domain property this slice may request or display, matching the
|
|
31
|
+
// reviewed raw surface. Anything else fails before credentials.
|
|
32
|
+
export const KNOWN_DOMAIN_FIELDS = [
|
|
33
|
+
"id",
|
|
34
|
+
"authenticationType",
|
|
35
|
+
"availabilityStatus",
|
|
36
|
+
"isAdminManaged",
|
|
37
|
+
"isDefault",
|
|
38
|
+
"isInitial",
|
|
39
|
+
"isRoot",
|
|
40
|
+
"isVerified",
|
|
41
|
+
"supportedServices",
|
|
42
|
+
"passwordValidityPeriodInDays",
|
|
43
|
+
"passwordNotificationWindowInDays",
|
|
44
|
+
"state",
|
|
45
|
+
];
|
|
46
|
+
const KNOWN_DOMAINS = new Set(KNOWN_DOMAIN_FIELDS);
|
|
47
|
+
// Every DNS record property this slice may request or display. The base
|
|
48
|
+
// domainDnsRecord properties stay selectable on every row; the derived-type
|
|
49
|
+
// properties (Mx mailExchange/preference, CNAME canonicalName, SRV
|
|
50
|
+
// nameTarget/port/priority/protocol/service/weight, TXT text) are returned
|
|
51
|
+
// by the server only on rows of that record kind.
|
|
52
|
+
export const KNOWN_DNS_FIELDS = [
|
|
53
|
+
"id",
|
|
54
|
+
"isOptional",
|
|
55
|
+
"label",
|
|
56
|
+
"recordType",
|
|
57
|
+
"supportedService",
|
|
58
|
+
"ttl",
|
|
59
|
+
"mailExchange",
|
|
60
|
+
"preference",
|
|
61
|
+
"canonicalName",
|
|
62
|
+
"nameTarget",
|
|
63
|
+
"port",
|
|
64
|
+
"priority",
|
|
65
|
+
"protocol",
|
|
66
|
+
"service",
|
|
67
|
+
"weight",
|
|
68
|
+
"text",
|
|
69
|
+
];
|
|
70
|
+
const KNOWN_DNS = new Set(KNOWN_DNS_FIELDS);
|
|
71
|
+
// @odata.type is preserved on DNS rows without being selectable: it names
|
|
72
|
+
// the derived record kind (for example #microsoft.graph.domainDnsMxRecord).
|
|
73
|
+
const DNS_TYPE_PROPERTY = "@odata.type";
|
|
74
|
+
// Compact domain rows: the fully qualified name, authentication kind and the
|
|
75
|
+
// verification/default state that decides the next step.
|
|
76
|
+
const DEFAULT_DOMAIN_LIST_SELECT = ["id", "authenticationType", "isVerified", "isDefault"];
|
|
77
|
+
// Show rows: the full reviewed domain set.
|
|
78
|
+
const DEFAULT_DOMAIN_SHOW_SELECT = [...KNOWN_DOMAIN_FIELDS];
|
|
79
|
+
// Compact DNS rows: identifier, zone label, record kind and service.
|
|
80
|
+
const DEFAULT_DNS_LIST_SELECT = ["id", "label", "recordType", "supportedService"];
|
|
81
|
+
// Show rows: the full reviewed base set; derived-type detail needs an
|
|
82
|
+
// explicit --select naming the derived property.
|
|
83
|
+
const DEFAULT_DNS_SHOW_SELECT = ["id", "isOptional", "label", "recordType", "supportedService", "ttl"];
|
|
84
|
+
// Delegated defaults are operation-specific; application profiles use their
|
|
85
|
+
// configured .default audience and reject --scopes.
|
|
86
|
+
export const DEFAULT_DOMAIN_SCOPES = ["https://graph.microsoft.com/Domain.Read.All"];
|
|
87
|
+
export const DEFAULT_DNS_SCOPES = ["https://graph.microsoft.com/Domain.Read.All"];
|
|
88
|
+
const TRUNCATE_AT = 500;
|
|
89
|
+
function fieldList(raw, known, knownList, flag, help) {
|
|
90
|
+
const parts = String(raw)
|
|
91
|
+
.split(",")
|
|
92
|
+
.map(part => part.trim())
|
|
93
|
+
.filter(part => part.length > 0);
|
|
94
|
+
if (!parts.length)
|
|
95
|
+
throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
|
|
96
|
+
const fields = [];
|
|
97
|
+
for (const part of parts) {
|
|
98
|
+
if (!known.has(part)) {
|
|
99
|
+
throw new AxiError(`Unknown domain property ${part} in --${flag}`, "VALIDATION_ERROR", [
|
|
100
|
+
help,
|
|
101
|
+
`Known properties: ${knownList.join(", ")}`,
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
if (!fields.includes(part))
|
|
105
|
+
fields.push(part);
|
|
106
|
+
}
|
|
107
|
+
return fields;
|
|
108
|
+
}
|
|
109
|
+
function scopesFor(flags, defaults, profile, help) {
|
|
110
|
+
if (profile.mode === "application") {
|
|
111
|
+
if (flags.scopes !== undefined) {
|
|
112
|
+
throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
if (flags.scopes === undefined)
|
|
117
|
+
return [...defaults];
|
|
118
|
+
const scopes = String(flags.scopes)
|
|
119
|
+
.split(",")
|
|
120
|
+
.map(scope => scope.trim())
|
|
121
|
+
.filter(scope => scope.length > 0);
|
|
122
|
+
return scopes;
|
|
123
|
+
}
|
|
124
|
+
function truncateValue(value, full) {
|
|
125
|
+
const truncate = (text) => full || text.length <= TRUNCATE_AT
|
|
126
|
+
? { value: text, truncated: false }
|
|
127
|
+
: { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
|
|
128
|
+
if (typeof value === "string")
|
|
129
|
+
return truncate(value);
|
|
130
|
+
if (Array.isArray(value) && value.every(entry => typeof entry === "string")) {
|
|
131
|
+
const projected = value.map(truncate);
|
|
132
|
+
return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
|
|
133
|
+
}
|
|
134
|
+
return { value, truncated: false };
|
|
135
|
+
}
|
|
136
|
+
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
137
|
+
// null stays null, an absent property stays absent and is never synthesized.
|
|
138
|
+
function project(row, fields, full) {
|
|
139
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
140
|
+
const projected = {};
|
|
141
|
+
let truncated = false;
|
|
142
|
+
for (const field of fields) {
|
|
143
|
+
if (!Object.hasOwn(source, field))
|
|
144
|
+
continue;
|
|
145
|
+
const result = truncateValue(source[field], full);
|
|
146
|
+
projected[field] = result.value;
|
|
147
|
+
truncated = truncated || result.truncated;
|
|
148
|
+
}
|
|
149
|
+
return { row: projected, truncated };
|
|
150
|
+
}
|
|
151
|
+
// DNS projection additionally preserves @odata.type so the derived record
|
|
152
|
+
// kind survives local projection.
|
|
153
|
+
function projectDns(row, fields, full) {
|
|
154
|
+
const { row: projected, truncated } = project(row, fields, full);
|
|
155
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
156
|
+
if (typeof source[DNS_TYPE_PROPERTY] === "string")
|
|
157
|
+
projected[DNS_TYPE_PROPERTY] = source[DNS_TYPE_PROPERTY];
|
|
158
|
+
return { row: projected, truncated };
|
|
159
|
+
}
|
|
160
|
+
function selectedFields(flags, known, knownList, defaults, help) {
|
|
161
|
+
const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, known, knownList, "select", help);
|
|
162
|
+
const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, known, knownList, "fields", help);
|
|
163
|
+
const missing = fields.find(field => !select.includes(field));
|
|
164
|
+
if (missing) {
|
|
165
|
+
throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
|
|
166
|
+
}
|
|
167
|
+
return { select, fields };
|
|
168
|
+
}
|
|
169
|
+
function profileHint(profileName) {
|
|
170
|
+
return `--profile ${shellValue(profileName)}`;
|
|
171
|
+
}
|
|
172
|
+
function shellValue(value) {
|
|
173
|
+
return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
174
|
+
}
|
|
175
|
+
function fullHint(command, flags, profileName) {
|
|
176
|
+
const args = Object.entries({ ...flags, profile: profileName, full: true })
|
|
177
|
+
.map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
|
|
178
|
+
return `mg-axi ${command} ${args.join(" ")}`;
|
|
179
|
+
}
|
|
180
|
+
// Denials carry the session's generic grant/role/licence cause; each
|
|
181
|
+
// operation adds the scope, roles and licensing that actually unlock it,
|
|
182
|
+
// because a 403 alone never says which prerequisite is missing.
|
|
183
|
+
function withGuidance(hints, run) {
|
|
184
|
+
return run().catch(error => {
|
|
185
|
+
if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
|
|
186
|
+
throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
const DOMAIN_DENIAL_HINTS = [
|
|
192
|
+
"Domain reads need Domain.Read.All plus a supported Entra role for delegated access (Domain Name Administrator or Global Reader are least-privileged; Directory Readers and other directory roles are also supported for list/get), or admin-consented Domain.Read.All for application access",
|
|
193
|
+
"Personal Microsoft accounts are not supported for domain reads",
|
|
194
|
+
"No P1/P2 prerequisite is stated for domain reads; never diagnose licence solely from HTTP 403",
|
|
195
|
+
];
|
|
196
|
+
const DNS_DENIAL_HINTS = [
|
|
197
|
+
"DNS record reads need Domain.Read.All plus a supported Entra role for delegated access (Domain Name Administrator or Global Reader are least-privileged), or admin-consented Domain.Read.All for application access",
|
|
198
|
+
"Personal Microsoft accounts are not supported for DNS record reads",
|
|
199
|
+
"No P1/P2 prerequisite is stated for DNS record reads; never diagnose licence solely from HTTP 403",
|
|
200
|
+
];
|
|
201
|
+
// Restoring the saved select/filter keeps cursor resumes lossless when
|
|
202
|
+
// --select or --filter is omitted. Domain lists carry no --filter flag at
|
|
203
|
+
// all: Graph has a documented known issue with $search, $top and $filter on
|
|
204
|
+
// /domains, so strict input validation refuses the flag before credentials
|
|
205
|
+
// instead of forwarding a misleading request.
|
|
206
|
+
function collectionCommon(session, flags, known, knownList, defaults, defaultScopes, operation, help, profile) {
|
|
207
|
+
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
208
|
+
if (cursor !== undefined && !cursor.trim())
|
|
209
|
+
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
210
|
+
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
211
|
+
const { select, fields } = selectedFields(flags, known, knownList, saved?.["$select"] === undefined ? defaults : fieldList(saved["$select"], known, knownList, "select", help), help);
|
|
212
|
+
const filter = flags.filter === undefined ? saved?.["$filter"] : String(flags.filter);
|
|
213
|
+
return { cursor, select, fields, scopes: scopesFor(flags, defaultScopes, profile, help), full: flags.full === true, filter };
|
|
214
|
+
}
|
|
215
|
+
function collectArgs(profile, operation, common, flags, help) {
|
|
216
|
+
const query = { $select: common.select.join(",") };
|
|
217
|
+
if (common.filter !== undefined)
|
|
218
|
+
query.$filter = common.filter;
|
|
219
|
+
const args = { profile, operation, query, scopes: common.scopes };
|
|
220
|
+
if (common.cursor !== undefined)
|
|
221
|
+
args.cursor = common.cursor;
|
|
222
|
+
if (flags.all === true) {
|
|
223
|
+
if (flags.limit !== undefined)
|
|
224
|
+
throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
|
|
228
|
+
}
|
|
229
|
+
return args;
|
|
230
|
+
}
|
|
231
|
+
function resumeHint(profileName) {
|
|
232
|
+
return `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`;
|
|
233
|
+
}
|
|
234
|
+
function domainName(flags, help) {
|
|
235
|
+
const domain = String(flags.domain);
|
|
236
|
+
if (!domain.trim())
|
|
237
|
+
throw new AxiError("--domain needs the fully qualified domain name", "VALIDATION_ERROR", [help]);
|
|
238
|
+
return domain;
|
|
239
|
+
}
|
|
240
|
+
export async function listDomains(session, flags, profile, operation, help, profileName) {
|
|
241
|
+
const common = collectionCommon(session, flags, KNOWN_DOMAINS, KNOWN_DOMAIN_FIELDS, DEFAULT_DOMAIN_LIST_SELECT, DEFAULT_DOMAIN_SCOPES, operation, help, profile);
|
|
242
|
+
const result = await withGuidance(DOMAIN_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
|
|
243
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_DOMAIN_LIST_SELECT.join(",") };
|
|
244
|
+
const domains = [];
|
|
245
|
+
let truncated = false;
|
|
246
|
+
for (const row of result.value) {
|
|
247
|
+
const projected = project(row, common.fields, common.full);
|
|
248
|
+
domains.push(projected.row);
|
|
249
|
+
truncated = truncated || projected.truncated;
|
|
250
|
+
}
|
|
251
|
+
const showHint = `mg-axi entra domain show --id <domain-name> ${profileHint(profileName)}`;
|
|
252
|
+
const filterNote = "Domain lists offer no --filter: Graph documents a known issue with $search, $top and $filter on /domains";
|
|
253
|
+
const truncationHints = truncated ? [fullHint("entra domain list", effectiveFlags, profileName)] : [];
|
|
254
|
+
if (!result.complete) {
|
|
255
|
+
return {
|
|
256
|
+
domains,
|
|
257
|
+
count: { returned: domains.length, complete: false, reason: result.reason },
|
|
258
|
+
cursor: result.cursor,
|
|
259
|
+
help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
const count = { returned: domains.length, complete: true };
|
|
263
|
+
if (!domains.length) {
|
|
264
|
+
return {
|
|
265
|
+
domains,
|
|
266
|
+
count,
|
|
267
|
+
help: [filterNote, "0 domains matched; the absence of results is the answer, not an error"],
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return { domains, count, help: [...truncationHints, showHint, filterNote] };
|
|
271
|
+
}
|
|
272
|
+
export async function showDomain(session, flags, profile, operation, help, profileName) {
|
|
273
|
+
const { select, fields } = selectedFields(flags, KNOWN_DOMAINS, KNOWN_DOMAIN_FIELDS, DEFAULT_DOMAIN_SHOW_SELECT, help);
|
|
274
|
+
const scopes = scopesFor(flags, DEFAULT_DOMAIN_SCOPES, profile, help);
|
|
275
|
+
const full = flags.full === true;
|
|
276
|
+
const raw = await withGuidance(DOMAIN_DENIAL_HINTS, () => session.execute({
|
|
277
|
+
profile,
|
|
278
|
+
operation,
|
|
279
|
+
params: { "domain-id": String(flags.id) },
|
|
280
|
+
query: { $select: select.join(",") },
|
|
281
|
+
scopes,
|
|
282
|
+
}));
|
|
283
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
284
|
+
throw new AxiError("Graph returned a malformed domain body", "GRAPH_ERROR", [
|
|
285
|
+
"Single-domain reads carry one domain object; treat anything else as unknown, not empty",
|
|
286
|
+
]);
|
|
287
|
+
}
|
|
288
|
+
const { row, truncated } = project(raw, fields, full);
|
|
289
|
+
const helpHints = truncated ? [fullHint("entra domain show", flags, profileName)] : [];
|
|
290
|
+
if (row["isVerified"] === false) {
|
|
291
|
+
helpHints.push(`mg-axi entra domain verification-dns-record list --domain ${shellValue(String(flags.id))} ${profileHint(profileName)}`);
|
|
292
|
+
}
|
|
293
|
+
if (helpHints.length)
|
|
294
|
+
return { domain: row, help: helpHints };
|
|
295
|
+
return { domain: row };
|
|
296
|
+
}
|
|
297
|
+
function dnsParams(flags, help, needsDomain) {
|
|
298
|
+
if (!needsDomain)
|
|
299
|
+
return {};
|
|
300
|
+
return { "domain-id": domainName(flags, help) };
|
|
301
|
+
}
|
|
302
|
+
async function listDnsRecords(session, flags, profile, operation, help, profileName, command) {
|
|
303
|
+
const common = collectionCommon(session, flags, KNOWN_DNS, KNOWN_DNS_FIELDS, DEFAULT_DNS_LIST_SELECT, DEFAULT_DNS_SCOPES, operation, help, profile);
|
|
304
|
+
const args = collectArgs(profile, operation, common, flags, help);
|
|
305
|
+
args.params = dnsParams(flags, help, command.needsDomain);
|
|
306
|
+
const result = await withGuidance(DNS_DENIAL_HINTS, () => session.collect(args));
|
|
307
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? DEFAULT_DNS_LIST_SELECT.join(",") };
|
|
308
|
+
if (result.query.$filter !== undefined)
|
|
309
|
+
effectiveFlags.filter = result.query.$filter;
|
|
310
|
+
const records = [];
|
|
311
|
+
let truncated = false;
|
|
312
|
+
for (const row of result.value) {
|
|
313
|
+
const projected = projectDns(row, common.fields, common.full);
|
|
314
|
+
records.push(projected.row);
|
|
315
|
+
truncated = truncated || projected.truncated;
|
|
316
|
+
}
|
|
317
|
+
const domainFlag = command.needsDomain ? `--domain ${shellValue(String(flags.domain))} ` : "";
|
|
318
|
+
const showHint = `mg-axi ${command.showCommand} ${domainFlag}--id <record-id> ${profileHint(profileName)}`;
|
|
319
|
+
const kindHint = "Rows carry @odata.type naming the derived record kind; derived-type detail needs an explicit --select naming the derived property";
|
|
320
|
+
const truncationHints = truncated ? [fullHint(command.command, effectiveFlags, profileName)] : [];
|
|
321
|
+
if (!result.complete) {
|
|
322
|
+
return {
|
|
323
|
+
[command.collectionKey]: records,
|
|
324
|
+
count: { returned: records.length, complete: false, reason: result.reason },
|
|
325
|
+
cursor: result.cursor,
|
|
326
|
+
help: [...truncationHints, resumeHint(profileName), showHint, kindHint],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const count = { returned: records.length, complete: true };
|
|
330
|
+
if (!records.length) {
|
|
331
|
+
return {
|
|
332
|
+
[command.collectionKey]: records,
|
|
333
|
+
count,
|
|
334
|
+
help: [kindHint, "0 DNS records matched; the absence of results is the answer, not an error"],
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return { [command.collectionKey]: records, count, help: [...truncationHints, showHint, kindHint] };
|
|
338
|
+
}
|
|
339
|
+
async function showDnsRecord(session, flags, profile, operation, help, profileName, command) {
|
|
340
|
+
const { select, fields } = selectedFields(flags, KNOWN_DNS, KNOWN_DNS_FIELDS, DEFAULT_DNS_SHOW_SELECT, help);
|
|
341
|
+
const scopes = scopesFor(flags, DEFAULT_DNS_SCOPES, profile, help);
|
|
342
|
+
const full = flags.full === true;
|
|
343
|
+
const raw = await withGuidance(DNS_DENIAL_HINTS, () => session.execute({
|
|
344
|
+
profile,
|
|
345
|
+
operation,
|
|
346
|
+
params: { ...dnsParams(flags, help, command.needsDomain), "domainDnsRecord-id": String(flags.id) },
|
|
347
|
+
query: { $select: select.join(",") },
|
|
348
|
+
scopes,
|
|
349
|
+
}));
|
|
350
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
351
|
+
throw new AxiError("Graph returned a malformed DNS record body", "GRAPH_ERROR", [
|
|
352
|
+
"Single-record reads carry one domainDnsRecord object; treat anything else as unknown, not empty",
|
|
353
|
+
]);
|
|
354
|
+
}
|
|
355
|
+
const { row, truncated } = projectDns(raw, fields, full);
|
|
356
|
+
const kindHint = "Rows carry @odata.type naming the derived record kind; derived-type detail needs an explicit --select naming the derived property";
|
|
357
|
+
if (truncated)
|
|
358
|
+
return { [command.recordKey]: row, help: [fullHint(command.showCommand, flags, profileName), kindHint] };
|
|
359
|
+
return { [command.recordKey]: row, help: [kindHint] };
|
|
360
|
+
}
|
|
361
|
+
const VERIFICATION = {
|
|
362
|
+
collectionKey: "verificationDnsRecords",
|
|
363
|
+
recordKey: "verificationDnsRecord",
|
|
364
|
+
command: "entra domain verification-dns-record list",
|
|
365
|
+
showCommand: "entra domain verification-dns-record show",
|
|
366
|
+
needsDomain: true,
|
|
367
|
+
};
|
|
368
|
+
const SERVICE = {
|
|
369
|
+
collectionKey: "serviceConfigurationRecords",
|
|
370
|
+
recordKey: "serviceConfigurationRecord",
|
|
371
|
+
command: "entra domain service-configuration-record list",
|
|
372
|
+
showCommand: "entra domain service-configuration-record show",
|
|
373
|
+
needsDomain: true,
|
|
374
|
+
};
|
|
375
|
+
const TOP_LEVEL = {
|
|
376
|
+
collectionKey: "domainDnsRecords",
|
|
377
|
+
recordKey: "domainDnsRecord",
|
|
378
|
+
command: "entra domain-dns-record list",
|
|
379
|
+
showCommand: "entra domain-dns-record show",
|
|
380
|
+
needsDomain: false,
|
|
381
|
+
};
|
|
382
|
+
export function listVerificationDnsRecords(session, flags, profile, operation, help, profileName) {
|
|
383
|
+
return listDnsRecords(session, flags, profile, operation, help, profileName, VERIFICATION);
|
|
384
|
+
}
|
|
385
|
+
export function showVerificationDnsRecord(session, flags, profile, operation, help, profileName) {
|
|
386
|
+
return showDnsRecord(session, flags, profile, operation, help, profileName, VERIFICATION);
|
|
387
|
+
}
|
|
388
|
+
export function listServiceConfigurationRecords(session, flags, profile, operation, help, profileName) {
|
|
389
|
+
return listDnsRecords(session, flags, profile, operation, help, profileName, SERVICE);
|
|
390
|
+
}
|
|
391
|
+
export function showServiceConfigurationRecord(session, flags, profile, operation, help, profileName) {
|
|
392
|
+
return showDnsRecord(session, flags, profile, operation, help, profileName, SERVICE);
|
|
393
|
+
}
|
|
394
|
+
export function listDomainDnsRecords(session, flags, profile, operation, help, profileName) {
|
|
395
|
+
return listDnsRecords(session, flags, profile, operation, help, profileName, TOP_LEVEL);
|
|
396
|
+
}
|
|
397
|
+
export function showDomainDnsRecord(session, flags, profile, operation, help, profileName) {
|
|
398
|
+
return showDnsRecord(session, flags, profile, operation, help, profileName, TOP_LEVEL);
|
|
399
|
+
}
|