@knowttl/mg-axi 0.1.4 → 0.1.7
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 +303 -1268
- package/axi-credential.schema.json +65 -0
- package/dist/api.js +5 -21
- package/dist/app-auth.js +9 -4
- package/dist/auth-status.js +20 -0
- package/dist/auth.js +24 -10
- package/dist/catalogue.js +26 -20
- package/dist/cli.js +57 -26
- package/dist/credentials.js +78 -0
- package/dist/docs.js +17 -13
- package/dist/doctor.js +10 -8
- package/dist/entra-audit-logs.js +5 -4
- package/dist/entra-conditional-access.js +4 -3
- package/dist/entra-directory-objects.js +4 -5
- package/dist/entra-domains.js +4 -2
- package/dist/entra-group-lifecycle.js +3 -2
- package/dist/entra-roles.js +6 -5
- package/dist/graph-session.js +30 -14
- package/dist/hook.js +9 -32
- package/dist/msal-app-provider.js +27 -101
- package/dist/msal-provider.js +67 -54
- package/dist/mutations.js +42 -53
- package/dist/paths.js +17 -0
- package/dist/private-file.js +227 -0
- package/dist/profiles.js +180 -134
- package/dist/redact.js +26 -0
- package/dist/secrets.js +37 -0
- package/dist/setup.js +2 -2
- package/docs/coverage.md +6 -3
- package/package.json +4 -6
- package/skills/mg-axi/SKILL.md +15 -11
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"title": "mg-axi credential profile",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"additionalProperties": false,
|
|
6
|
+
"required": ["mode", "tenantId", "clientId"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"mode": { "enum": ["delegated", "application"] },
|
|
9
|
+
"tenantId": { "$ref": "#/$defs/uuid" },
|
|
10
|
+
"clientId": { "$ref": "#/$defs/uuid" },
|
|
11
|
+
"cloud": { "const": "commercial", "default": "commercial" },
|
|
12
|
+
"enabledPacks": { "type": "array", "items": { "const": "entra" }, "uniqueItems": true, "default": ["entra"] },
|
|
13
|
+
"preview": { "type": "boolean", "default": false },
|
|
14
|
+
"sensitiveAreas": { "type": "array", "maxItems": 0, "default": [] },
|
|
15
|
+
"allowDeviceCode": { "type": "boolean", "default": false },
|
|
16
|
+
"auth": { "enum": ["certificate", "client-secret", "federated"] },
|
|
17
|
+
"thumbprintSha256": { "type": "string", "pattern": "^[0-9a-fA-F]{64}$" },
|
|
18
|
+
"privateKey": { "$ref": "#/$defs/externalSecret", "x-secret": true },
|
|
19
|
+
"clientSecret": { "$ref": "#/$defs/externalSecret", "x-secret": true },
|
|
20
|
+
"token": { "$ref": "#/$defs/secret", "x-secret": true },
|
|
21
|
+
"writes": {
|
|
22
|
+
"type": "object", "additionalProperties": false, "required": ["allowWrites", "operations"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"allowWrites": { "type": "boolean" },
|
|
25
|
+
"operations": { "type": "array", "minItems": 1, "maxItems": 64, "items": { "type": "string", "minLength": 1, "maxLength": 256 } }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"oneOf": [
|
|
30
|
+
{
|
|
31
|
+
"properties": { "mode": { "const": "delegated" } },
|
|
32
|
+
"not": { "anyOf": [{ "required": ["auth"] }, { "required": ["privateKey"] }, { "required": ["clientSecret"] }, { "required": ["thumbprintSha256"] }] }
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"required": ["auth", "thumbprintSha256"],
|
|
36
|
+
"properties": { "mode": { "const": "application" }, "auth": { "const": "certificate" }, "allowDeviceCode": { "const": false } },
|
|
37
|
+
"not": { "anyOf": [{ "required": ["token"] }, { "required": ["clientSecret"] }] }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"required": ["auth"],
|
|
41
|
+
"properties": { "mode": { "const": "application" }, "auth": { "const": "client-secret" }, "allowDeviceCode": { "const": false } },
|
|
42
|
+
"not": { "anyOf": [{ "required": ["token"] }, { "required": ["privateKey"] }, { "required": ["thumbprintSha256"] }] }
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"required": ["auth"],
|
|
46
|
+
"properties": { "mode": { "const": "application" }, "auth": { "const": "federated" }, "token": { "$ref": "#/$defs/externalSecret" }, "allowDeviceCode": { "const": false } },
|
|
47
|
+
"not": { "anyOf": [{ "required": ["privateKey"] }, { "required": ["clientSecret"] }, { "required": ["thumbprintSha256"] }] }
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
"$defs": {
|
|
51
|
+
"uuid": { "type": "string", "pattern": "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$" },
|
|
52
|
+
"externalSecret": {
|
|
53
|
+
"oneOf": [
|
|
54
|
+
{ "type": "object", "additionalProperties": false, "required": ["env"], "properties": { "env": { "type": "string", "pattern": "^[A-Za-z_][A-Za-z0-9_]*$" } } },
|
|
55
|
+
{ "type": "object", "additionalProperties": false, "required": ["file"], "properties": { "file": { "type": "string", "pattern": "^(?:/|[A-Za-z]:[\\\\/])" } } }
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"secret": {
|
|
59
|
+
"oneOf": [
|
|
60
|
+
{ "$ref": "#/$defs/externalSecret" },
|
|
61
|
+
{ "type": "object", "additionalProperties": false, "required": ["stored"], "properties": { "stored": { "const": true } } }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
package/dist/api.js
CHANGED
|
@@ -61,7 +61,7 @@ const AUTH_METHOD_FIELDS = ["id", "createdDateTime", "displayName", "phoneNumber
|
|
|
61
61
|
const REGISTRATION_FIELDS = ["id", "userPrincipalName", "userDisplayName", "userType", "isAdmin", "isMfaRegistered", "isMfaCapable", "isPasswordlessCapable", "isSsprRegistered", "isSsprEnabled", "isSsprCapable", "userPreferredMethodForSecondaryAuthentication", "lastUpdatedDateTime"];
|
|
62
62
|
const SIGNIN_FIELDS = ["id", "createdDateTime", "userId", "userPrincipalName", "userDisplayName", "appDisplayName", "appId", "ipAddress", "location", "status", "conditionalAccessStatus", "riskDetail", "riskLevelAggregated", "riskLevelDuringSignIn", "riskState", "resourceDisplayName", "resourceId", "clientAppUsed"];
|
|
63
63
|
const AUDIT_FIELDS = ["id", "activityDateTime", "activityDisplayName", "category", "loggedByService", "operationType", "result", "resultReason", "correlationId", "initiatedBy", "targetResources"];
|
|
64
|
-
const PROVISIONING_FIELDS = ["id", "activityDateTime", "tenantId", "jobId", "cycleId", "changeId", "
|
|
64
|
+
const PROVISIONING_FIELDS = ["id", "activityDateTime", "tenantId", "jobId", "cycleId", "changeId", "provisioningAction", "durationInMilliseconds", "sourceSystem", "sourceIdentity", "targetSystem", "targetIdentity", "provisioningStatusInfo", "provisioningSteps", "modifiedProperties", "servicePrincipal", "initiatedBy"];
|
|
65
65
|
const RISKY_USER_FIELDS = ["id", "userPrincipalName", "userDisplayName", "riskDetail", "riskLastUpdatedDateTime", "riskLevel", "riskState"];
|
|
66
66
|
const RISK_DETECTION_FIELDS = ["id", "detectedDateTime", "activityDateTime", "userId", "userPrincipalName", "userDisplayName", "ipAddress", "location", "riskDetail", "riskLevel", "riskState", "riskEventType", "detectionTimingType", "lastUpdatedDateTime", "source"];
|
|
67
67
|
const RISKY_SP_FIELDS = ["id", "displayName", "appId", "servicePrincipalType", "riskDetail", "riskLastUpdatedDateTime", "riskLevel", "riskState", "isEnabled", "isProcessing"];
|
|
@@ -117,7 +117,7 @@ export const REVIEWED_ROUTES = [
|
|
|
117
117
|
access: "D/A Policy.Read.AuthenticationMethod. Delegated callers pass it as --scopes; a supported administrator role (Conditional Access Administrator, Security Administrator or Security Reader) is also required.",
|
|
118
118
|
note: "Conditional Access needs P1.",
|
|
119
119
|
sources: ["https://learn.microsoft.com/en-us/graph/api/authenticationstrengthroot-list-policies?view=graph-rest-1.0"] },
|
|
120
|
-
{ id: "v1.0:GET:/identity/conditionalAccess/authenticationStrength/policies/{authenticationStrengthPolicy-id}", kind: "single", query:
|
|
120
|
+
{ id: "v1.0:GET:/identity/conditionalAccess/authenticationStrength/policies/{authenticationStrengthPolicy-id}", kind: "single", query: [], fields: KNOWN_STRENGTH_FIELDS,
|
|
121
121
|
access: "D/A Policy.Read.AuthenticationMethod. Delegated callers pass it as --scopes; a supported administrator role (Conditional Access Administrator, Security Administrator or Security Reader) is also required.",
|
|
122
122
|
note: "Conditional Access needs P1.",
|
|
123
123
|
sources: ["https://learn.microsoft.com/en-us/graph/api/authenticationstrengthpolicy-get?view=graph-rest-1.0"] },
|
|
@@ -145,7 +145,7 @@ export const REVIEWED_ROUTES = [
|
|
|
145
145
|
access: "D/A Policy.Read.All. Delegated callers pass it as --scopes; a supported administrator role (for example Security Reader) is also required.",
|
|
146
146
|
note: "Conditional Access needs P1.",
|
|
147
147
|
sources: ["https://learn.microsoft.com/en-us/graph/api/conditionalaccesstemplate-get?view=graph-rest-1.0"] },
|
|
148
|
-
{ id: "v1.0:GET:/identity/conditionalAccess/authenticationContextClassReferences", kind: "collection", query:
|
|
148
|
+
{ id: "v1.0:GET:/identity/conditionalAccess/authenticationContextClassReferences", kind: "collection", query: ["$filter", "$orderby", "$top"], fields: KNOWN_AUTH_CONTEXT_FIELDS,
|
|
149
149
|
access: "D/A AuthenticationContext.Read.All. Delegated callers pass it as --scopes; a supported administrator role (Conditional Access Administrator, Global Reader, Global Secure Access Administrator, Security Administrator or Security Reader) is also required.",
|
|
150
150
|
note: "Conditional Access needs P1.",
|
|
151
151
|
sources: ["https://learn.microsoft.com/graph/api/conditionalaccessroot-list-authenticationcontextclassreferences?view=graph-rest-1.0"] },
|
|
@@ -187,7 +187,7 @@ export const REVIEWED_ROUTES = [
|
|
|
187
187
|
{ id: "v1.0:GET:/auditLogs/directoryAudits", kind: "collection", query: COLLECTION_QUERY, fields: AUDIT_FIELDS,
|
|
188
188
|
access: "D/A AuditLog.Read.All. Delegated callers pass it as --scopes.",
|
|
189
189
|
sources: ["https://learn.microsoft.com/graph/api/directoryaudit-list?view=graph-rest-1.0"] },
|
|
190
|
-
{ id: "v1.0:GET:/auditLogs/provisioning", kind: "collection", query:
|
|
190
|
+
{ id: "v1.0:GET:/auditLogs/provisioning", kind: "collection", query: ["$filter", "$orderby", "$top"], fields: PROVISIONING_FIELDS,
|
|
191
191
|
access: "D/A AuditLog.Read.All and Directory.Read.All. Delegated callers pass both as --scopes; Reports Reader is the least privileged directory role.",
|
|
192
192
|
note: "Entra ID P1/P2 licence prerequisite; retention is 30 days on premium and 7 days on free.",
|
|
193
193
|
sources: ["https://learn.microsoft.com/graph/api/provisioningobjectsummary-list?view=graph-rest-1.0"] },
|
|
@@ -313,14 +313,6 @@ export const REVIEWED_ROUTES = [
|
|
|
313
313
|
access: "D/A RoleManagement.Read.Directory. Delegated callers pass it as --scopes.",
|
|
314
314
|
note: "Administrative-unit scopes need P1.",
|
|
315
315
|
sources: ["https://learn.microsoft.com/graph/api/scopedrolemembership-get?view=graph-rest-1.0"] },
|
|
316
|
-
{ id: "v1.0:GET:/scopedRoleMemberships", kind: "collection", query: COLLECTION_QUERY, fields: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
317
|
-
access: "D/A RoleManagement.Read.Directory. Delegated callers pass it as --scopes.",
|
|
318
|
-
note: "Administrative-unit-scoped admin assignments; directory-wide assignments live on roleAssignments. Administrative-unit scopes need P1.",
|
|
319
|
-
sources: ["https://learn.microsoft.com/graph/api/scopedrolemembership-list?view=graph-rest-1.0"] },
|
|
320
|
-
{ id: "v1.0:GET:/scopedRoleMemberships/{scopedRoleMembership-id}", kind: "single", query: SINGLE_QUERY, fields: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
321
|
-
access: "D/A RoleManagement.Read.Directory. Delegated callers pass it as --scopes.",
|
|
322
|
-
note: "Administrative-unit scopes need P1.",
|
|
323
|
-
sources: ["https://learn.microsoft.com/graph/api/scopedrolemembership-get?view=graph-rest-1.0"] },
|
|
324
316
|
{ id: "v1.0:GET:/identityGovernance/privilegedAccess/group/assignmentSchedules", kind: "collection", query: COLLECTION_QUERY, fields: KNOWN_ASSIGNMENT_SCHEDULE_FIELDS,
|
|
325
317
|
access: "D/A PrivilegedAssignmentSchedule.Read.AzureADGroup. Delegated callers pass it as --scopes.",
|
|
326
318
|
note: "Assignment schedules govern group membership or ownership over time; PIM for Groups needs P2 or ID Governance; lists require $filter on groupId or principalId. AssignmentScheduleRequests stay out: their least privilege is the write scope PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup.",
|
|
@@ -415,10 +407,6 @@ export const REVIEWED_ROUTES = [
|
|
|
415
407
|
access: "D/A Domain.Read.All. Delegated callers pass it as --scopes; Domain Name Administrator or Global Reader are the least-privileged delegated roles. No P1/P2 prerequisite is stated for DNS record reads.",
|
|
416
408
|
note: "No operation-level documentation page; access follows the parent service-configuration contract and the domainDnsRecord resource reference.",
|
|
417
409
|
sources: ["https://learn.microsoft.com/en-us/graph/api/domain-list-serviceconfigurationrecords?view=graph-rest-1.0", "https://learn.microsoft.com/en-us/graph/api/resources/domaindnsrecord?view=graph-rest-1.0"] },
|
|
418
|
-
{ id: "v1.0:GET:/domainDnsRecords", kind: "collection", query: COLLECTION_QUERY, fields: DNS_RECORD_FIELDS,
|
|
419
|
-
access: "D/A Domain.Read.All. Delegated callers pass it as --scopes; Domain Name Administrator or Global Reader are the least-privileged delegated roles. No P1/P2 prerequisite is stated for DNS record reads.",
|
|
420
|
-
note: "No operation-level documentation page; access follows the documented domain/DNS-read contract and the domainDnsRecord resource reference.",
|
|
421
|
-
sources: ["https://learn.microsoft.com/en-us/graph/api/domain-list?view=graph-rest-1.0", "https://learn.microsoft.com/en-us/graph/api/resources/domaindnsrecord?view=graph-rest-1.0"] },
|
|
422
410
|
{ id: "v1.0:GET:/domainDnsRecords/{domainDnsRecord-id}", kind: "single", query: SINGLE_QUERY, fields: DNS_RECORD_FIELDS,
|
|
423
411
|
access: "D/A Domain.Read.All. Delegated callers pass it as --scopes; Domain Name Administrator or Global Reader are the least-privileged delegated roles. No P1/P2 prerequisite is stated for DNS record reads.",
|
|
424
412
|
note: "No operation-level documentation page; access follows the documented domain/DNS-read contract and the domainDnsRecord resource reference.",
|
|
@@ -489,10 +477,6 @@ export const REVIEWED_ROUTES = [
|
|
|
489
477
|
delegatedOnly: true,
|
|
490
478
|
note: "No single-acceptance operation page; access follows the parent acceptance-list contract and the agreementAcceptance resource reference. Acceptance records are personal data: default rows carry id, agreementId, state and recordedDateTime only.",
|
|
491
479
|
sources: ["https://learn.microsoft.com/en-us/graph/api/agreement-list-acceptances?view=graph-rest-1.0", "https://learn.microsoft.com/en-us/graph/api/resources/agreementacceptance?view=graph-rest-1.0"] },
|
|
492
|
-
{ id: "v1.0:GET:/directoryObjects", kind: "collection", query: ["$select"], fields: KNOWN_DIRECTORY_OBJECT_FIELDS, defaultFields: ["id"], keepODataType: true,
|
|
493
|
-
access: "D/A Directory.Read.All. Delegated callers pass it as --scopes; no delegated role prerequisite is stated for directory-object reads. Personal Microsoft accounts are not supported.",
|
|
494
|
-
note: "No List operation page exists; access follows the sibling directoryobject-get contract and the directoryObject resource reference. Graph documents no collection filter contract here, so only $select is reviewed. Rows are polymorphic: the @odata.type discriminator rides along automatically and subtype properties need the subtype's named reads, never raw $select of unreviewed fields.",
|
|
495
|
-
sources: ["https://learn.microsoft.com/en-us/graph/api/directoryobject-get?view=graph-rest-1.0", "https://learn.microsoft.com/en-us/graph/api/resources/directoryobject?view=graph-rest-1.0"] },
|
|
496
480
|
{ id: "v1.0:GET:/directoryObjects/{directoryObject-id}", kind: "single", query: SINGLE_QUERY, fields: KNOWN_DIRECTORY_OBJECT_FIELDS, defaultFields: ["id", "deletedDateTime"], keepODataType: true,
|
|
497
481
|
access: "D/A Directory.Read.All. Delegated callers pass it as --scopes; no delegated role prerequisite is stated for this read. Personal Microsoft accounts are not supported.",
|
|
498
482
|
note: "Single reads return the base-type properties plus the @odata.type discriminator; subtype detail needs the subtype's named reads. Only base properties are projected, so subtype secrets or credentials can never appear.",
|
|
@@ -605,7 +589,7 @@ export const REVIEWED_ROUTES = [
|
|
|
605
589
|
access: "D/A OrgContact.Read.All and Group.Read.All together least-privileged (Directory.Read.All is the documented higher-privileged alternative). Delegated callers pass both as --scopes; delegated access additionally needs a supported Entra role (Directory Readers reads basic properties; Global Reader, Directory Writers, Intune Administrator or User Administrator also work). Personal Microsoft accounts are not supported.",
|
|
606
590
|
note: "No operation-level documentation page; OData cast selects the administrativeUnit subtype and access follows the parent transitiveMemberOf contract and the administrativeUnit resource reference.",
|
|
607
591
|
sources: ["https://learn.microsoft.com/en-us/graph/api/orgcontact-list-transitivememberof?view=graph-rest-1.0", "https://learn.microsoft.com/en-us/graph/api/resources/administrativeunit?view=graph-rest-1.0"] },
|
|
608
|
-
{ id: "v1.0:GET:/groupLifecyclePolicies", kind: "collection", query:
|
|
592
|
+
{ id: "v1.0:GET:/groupLifecyclePolicies", kind: "collection", query: ["$filter", "$orderby", "$top"], fields: KNOWN_LIFECYCLE_FIELDS,
|
|
609
593
|
access: "D/A Directory.Read.All. Delegated callers pass it as --scopes; no delegated role prerequisite is stated for lifecycle-policy reads. Personal Microsoft accounts are not supported.",
|
|
610
594
|
note: "Filtering uses plain $filter with no $count or ConsistencyLevel contract; no P1/P2 prerequisite is stated for lifecycle-policy reads.",
|
|
611
595
|
sources: ["https://learn.microsoft.com/graph/api/grouplifecyclepolicy-list?view=graph-rest-1.0", "https://learn.microsoft.com/graph/api/resources/grouplifecyclepolicy?view=graph-rest-1.0"] },
|
package/dist/app-auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { PrivateFiles } from "./private-file.js";
|
|
2
3
|
import { validateApplicationProfile } from "./profiles.js";
|
|
4
|
+
import { registerSecret } from "./redact.js";
|
|
3
5
|
// The configured Graph audience is the app's consented application grants, never a per-command subset.
|
|
4
6
|
export const graphAudience = (profile) => `https://graph.microsoft.com/.default`;
|
|
5
7
|
// This service is the application credential boundary for CORE-01; tokens never reach CLI output.
|
|
@@ -7,13 +9,15 @@ export const graphAudience = (profile) => `https://graph.microsoft.com/.default`
|
|
|
7
9
|
export class ApplicationAuth {
|
|
8
10
|
provider;
|
|
9
11
|
now;
|
|
12
|
+
files;
|
|
10
13
|
cache = new Map();
|
|
11
|
-
constructor(provider, now = Date.now) {
|
|
14
|
+
constructor(provider, now = Date.now, files = new PrivateFiles()) {
|
|
12
15
|
this.provider = provider;
|
|
13
16
|
this.now = now;
|
|
17
|
+
this.files = files;
|
|
14
18
|
}
|
|
15
19
|
async credential(profile, scopes = [graphAudience(profile)]) {
|
|
16
|
-
profile = validateApplicationProfile(profile);
|
|
20
|
+
profile = validateApplicationProfile(profile, this.files);
|
|
17
21
|
const requested = this.audience(profile, scopes);
|
|
18
22
|
const key = this.key(profile, requested);
|
|
19
23
|
const hit = this.cache.get(key);
|
|
@@ -30,7 +34,7 @@ export class ApplicationAuth {
|
|
|
30
34
|
catch (error) {
|
|
31
35
|
// Provider AUTH_REQUIRED failures already name only the credential
|
|
32
36
|
// reference, never its value; everything else stays fully generic.
|
|
33
|
-
if (error instanceof AxiError &&
|
|
37
|
+
if (error instanceof AxiError && ["AUTH_REQUIRED", "CONFIG_INVALID", "CONFIG_INSECURE"].includes(error.code))
|
|
34
38
|
throw error;
|
|
35
39
|
throw this.failure();
|
|
36
40
|
}
|
|
@@ -41,10 +45,11 @@ export class ApplicationAuth {
|
|
|
41
45
|
throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", ["mg-axi profile show --profile <name>", "Application permissions apply to the app registration, not to individual commands"]);
|
|
42
46
|
return configured;
|
|
43
47
|
}
|
|
44
|
-
key(profile, scopes) { return JSON.stringify([profile.
|
|
48
|
+
key(profile, scopes) { return JSON.stringify([profile.auth, profile.auth === "certificate" ? [profile.privateKey, profile.thumbprintSha256] : profile.auth === "client-secret" ? profile.clientSecret : profile.token, profile.tenantId, profile.clientId, profile.cloud, scopes]); }
|
|
45
49
|
check(profile, credential) {
|
|
46
50
|
if (!credential.token || credential.tenantId.toLowerCase() !== profile.tenantId.toLowerCase() || credential.clientId.toLowerCase() !== profile.clientId.toLowerCase() || !Number.isFinite(credential.expiresAt) || credential.expiresAt <= this.now() + 60_000)
|
|
47
51
|
throw new Error("Invalid credential context");
|
|
52
|
+
registerSecret(credential.token);
|
|
48
53
|
return { ...credential };
|
|
49
54
|
}
|
|
50
55
|
failure() { return new AxiError("Application authentication failed for the configured identity; no user or device-code fallback was attempted", "AUTH_REQUIRED", ["mg-axi profile show --profile <name>", "Ask an administrator to grant application consent for the Graph .default audience on this app registration", "Confirm the certificate thumbprint or federated credential mapping on the app registration"]); }
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { Credentials } from "./credentials.js";
|
|
3
|
+
import { resolveSecret, secretSource } from "./secrets.js";
|
|
4
|
+
export function authStatus(name, profile, credentials = new Credentials()) {
|
|
5
|
+
const fields = profile.mode === "delegated" ? { token: profile.token }
|
|
6
|
+
: profile.auth === "certificate" ? { privateKey: profile.privateKey }
|
|
7
|
+
: profile.auth === "client-secret" ? { clientSecret: profile.clientSecret } : { token: profile.token };
|
|
8
|
+
const sources = Object.entries(fields).map(([field, ref]) => {
|
|
9
|
+
let available = false;
|
|
10
|
+
try {
|
|
11
|
+
available = "stored" in ref && profile.mode === "delegated" ? credentials.read(profile, true) !== undefined : !!resolveSecret(ref, credentials.files, true);
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (!(error instanceof AxiError) || error.code !== "AUTH_REQUIRED")
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
return { field, source: "stored" in ref ? `stored:${credentials.path}` : secretSource(ref), available };
|
|
18
|
+
});
|
|
19
|
+
return { profile: name, mode: profile.mode, endpoint: "https://graph.microsoft.com", tenantId: profile.tenantId, clientId: profile.clientId, sources };
|
|
20
|
+
}
|
package/dist/auth.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
-
import {
|
|
2
|
+
import { PrivateFiles } from "./private-file.js";
|
|
3
|
+
import { profileName, validateDelegatedProfile } from "./profiles.js";
|
|
4
|
+
import { registerSecret } from "./redact.js";
|
|
3
5
|
// This service is the credential boundary for CORE-01; tokens never reach CLI output.
|
|
4
6
|
export class DelegatedAuth {
|
|
5
7
|
provider;
|
|
6
8
|
now;
|
|
9
|
+
files;
|
|
7
10
|
cache = new Map();
|
|
8
|
-
constructor(provider, now = Date.now) {
|
|
11
|
+
constructor(provider, now = Date.now, files = new PrivateFiles()) {
|
|
9
12
|
this.provider = provider;
|
|
10
13
|
this.now = now;
|
|
14
|
+
this.files = files;
|
|
11
15
|
}
|
|
12
16
|
async login(profile, method = "browser", scopes = []) {
|
|
13
|
-
profile = validateDelegatedProfile(profile);
|
|
17
|
+
profile = validateDelegatedProfile(profile, this.files);
|
|
14
18
|
if (method !== "browser" && method !== "device-code")
|
|
15
|
-
throw new AxiError("Login method must be browser or device-code", "VALIDATION_ERROR", ["mg-axi login --help"]);
|
|
19
|
+
throw new AxiError("Login method must be browser or device-code", "VALIDATION_ERROR", ["mg-axi auth login --help"]);
|
|
16
20
|
if (method === "device-code" && !profile.allowDeviceCode)
|
|
17
21
|
throw new AxiError("Device code is disabled for this profile", "VALIDATION_ERROR", ["Enable device code only when organization policy permits it; browser login is the default"]);
|
|
18
22
|
const requested = this.scopes(scopes);
|
|
@@ -23,12 +27,14 @@ export class DelegatedAuth {
|
|
|
23
27
|
this.cache.set(key, credential);
|
|
24
28
|
return { status: "authenticated", mode: "delegated", storage: this.provider.storage };
|
|
25
29
|
}
|
|
26
|
-
catch {
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (error instanceof AxiError && ["CONFIG_INVALID", "CONFIG_INSECURE"].includes(error.code))
|
|
32
|
+
throw error;
|
|
27
33
|
throw this.failure("LOGIN_FAILED");
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
async credential(profile, scopes) {
|
|
31
|
-
profile = validateDelegatedProfile(profile);
|
|
37
|
+
profile = validateDelegatedProfile(profile, this.files);
|
|
32
38
|
const requested = this.scopes(scopes);
|
|
33
39
|
const key = this.key(profile, requested);
|
|
34
40
|
const hit = this.cache.get(key);
|
|
@@ -40,20 +46,28 @@ export class DelegatedAuth {
|
|
|
40
46
|
this.cache.set(key, credential);
|
|
41
47
|
return { ...credential };
|
|
42
48
|
}
|
|
43
|
-
catch {
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof AxiError && ["CONFIG_INVALID", "CONFIG_INSECURE"].includes(error.code))
|
|
51
|
+
throw error;
|
|
44
52
|
throw this.failure("AUTH_REQUIRED");
|
|
45
53
|
}
|
|
46
54
|
}
|
|
55
|
+
async logout(profile) {
|
|
56
|
+
this.cache.clear();
|
|
57
|
+
await this.provider.logout(validateDelegatedProfile(profile, this.files));
|
|
58
|
+
return { status: "signed out", revocation: "MSAL provides no token revocation API; local credentials deleted" };
|
|
59
|
+
}
|
|
47
60
|
scopes(scopes) {
|
|
48
61
|
if (!scopes.length || scopes.some(scope => !/^https:\/\/graph\.microsoft\.com\/[A-Za-z][A-Za-z.-]+$/.test(scope) || scope.endsWith("/.default")))
|
|
49
|
-
throw new AxiError("Explicit delegated Graph scopes are required", "VALIDATION_ERROR", ["mg-axi login --profile <name> --scopes <comma-separated-Graph-scopes>"]);
|
|
62
|
+
throw new AxiError("Explicit delegated Graph scopes are required", "VALIDATION_ERROR", ["mg-axi auth login --profile <name> --scopes <comma-separated-Graph-scopes>"]);
|
|
50
63
|
return [...new Set(scopes)].sort();
|
|
51
64
|
}
|
|
52
|
-
key(profile, scopes) { return JSON.stringify([profile.
|
|
65
|
+
key(profile, scopes) { return JSON.stringify([profileName(profile), profile.token, profile.tenantId, profile.clientId, profile.cloud, scopes]); }
|
|
53
66
|
check(profile, credential) {
|
|
54
67
|
if (!credential.token || !credential.accountId || credential.tenantId.toLowerCase() !== profile.tenantId.toLowerCase() || credential.clientId.toLowerCase() !== profile.clientId.toLowerCase() || !Number.isFinite(credential.expiresAt) || credential.expiresAt <= this.now() + 60_000)
|
|
55
68
|
throw new Error("Invalid credential context");
|
|
69
|
+
registerSecret(credential.token);
|
|
56
70
|
return { ...credential };
|
|
57
71
|
}
|
|
58
|
-
failure(code) { return new AxiError("Authentication unavailable for the configured identity; no interactive fallback was attempted", code, ["mg-axi login --profile <name> --scopes <comma-separated-Graph-scopes>", "Check dedicated app consent and organization sign-in policy", "Restore
|
|
72
|
+
failure(code) { return new AxiError("Authentication unavailable for the configured identity; no interactive fallback was attempted", code, ["mg-axi auth login --profile <name> --scopes <comma-separated-Graph-scopes>", "Check dedicated app consent and organization sign-in policy", "Restore access to the protected credentials.json before retrying login"]); }
|
|
59
73
|
}
|
package/dist/catalogue.js
CHANGED
|
@@ -506,23 +506,23 @@ export const LEAVES = [
|
|
|
506
506
|
cloud: { value: "commercial", required: true, description: "Explicit cloud; only commercial is supported" },
|
|
507
507
|
mode: { value: "delegated|application", default: "delegated", description: "Delegated analyst sign-in or application client credentials" },
|
|
508
508
|
"allow-device-code": { description: "Delegated only: opt in only when permitted by organization policy; never enables fallback" },
|
|
509
|
-
"certificate-thumbprint": { value: "
|
|
510
|
-
"certificate-key-file": { value: "path", description: "Application only: certificate PEM private key from this
|
|
511
|
-
"certificate-key-env": { value: "env-var-name", description: "Application only: certificate PEM private key from this environment variable;
|
|
509
|
+
"certificate-thumbprint-sha256": { value: "64-hex-digits", description: "Application only: SHA-256 certificate thumbprint" },
|
|
510
|
+
"certificate-key-file": { value: "absolute-path", description: "Application only: certificate PEM private key from this protected file" },
|
|
511
|
+
"certificate-key-env": { value: "env-var-name", description: "Application only: certificate PEM private key from this environment variable; defaults to MG_AXI_PRIVATE_KEY" },
|
|
512
512
|
"client-secret-env": { value: "env-var-name", description: "Application only: client secret from this environment variable; exactly one of this or --client-secret-file" },
|
|
513
|
-
"client-secret-file": { value: "path", description: "Application only: client secret from this
|
|
514
|
-
federated: { description: "Application only: workload federation
|
|
515
|
-
"federated-token-file
|
|
516
|
-
}, examples: ["mg-axi profile create --name soc --tenant <tenant-id> --client <client-id> --cloud commercial", "mg-axi profile create --name batch --tenant <tenant-id> --client <client-id> --cloud commercial --mode application --federated", "mg-axi profile create --name daemon --tenant <tenant-id> --client <client-id> --cloud commercial --mode application --certificate-thumbprint <
|
|
513
|
+
"client-secret-file": { value: "absolute-path", description: "Application only: client secret from this protected file; exactly one of this or --client-secret-env" },
|
|
514
|
+
federated: { description: "Application only: workload federation; token defaults to env MG_AXI_TOKEN; AZURE_FEDERATED_TOKEN_FILE stays accepted as a deprecated alias" },
|
|
515
|
+
"federated-token-file": { value: "absolute-path", description: "Application only: workload assertion content from this protected file" },
|
|
516
|
+
}, examples: ["mg-axi profile create --name soc --tenant <tenant-id> --client <client-id> --cloud commercial", "mg-axi profile create --name batch --tenant <tenant-id> --client <client-id> --cloud commercial --mode application --federated", "mg-axi profile create --name daemon --tenant <tenant-id> --client <client-id> --cloud commercial --mode application --certificate-thumbprint-sha256 <64-hex-digit-thumbprint> --certificate-key-file /run/secrets/mg-axi-key.pem", "mg-axi profile create --name svc --tenant <tenant-id> --client <client-id> --cloud commercial --mode application --client-secret-env MG_AXI_CLIENT_SECRET", "mg-axi profile create --help"] },
|
|
517
517
|
{ path: "profile list", description: "List configured profiles without accessing credentials", flags: {}, examples: ["mg-axi profile list", "mg-axi profile list --help"] },
|
|
518
518
|
{ path: "profile show", description: "Show profile policy and credential reference, never credentials", flags: {
|
|
519
519
|
profile: common.profile,
|
|
520
520
|
}, examples: ["mg-axi profile show --profile soc", "mg-axi profile show"] },
|
|
521
|
-
{ path: "login", description: "Explicit delegated login; browser by default, no automatic device-code fallback", flags: {
|
|
521
|
+
{ path: "auth login", description: "Explicit delegated login; browser by default, no automatic device-code fallback", flags: {
|
|
522
522
|
profile: common.profile,
|
|
523
523
|
method: { value: "browser|device-code", default: "browser", description: "Device code additionally requires profile opt-in" },
|
|
524
524
|
scopes: { value: "comma-separated-Graph-scopes", required: true, description: "Explicit delegated permissions using full https://graph.microsoft.com/ scope names" },
|
|
525
|
-
}, examples: ["mg-axi login --profile soc --scopes https://graph.microsoft.com/User.Read", "mg-axi login --profile soc --method device-code --scopes https://graph.microsoft.com/User.Read"] },
|
|
525
|
+
}, examples: ["mg-axi auth login --profile soc --scopes https://graph.microsoft.com/User.Read", "mg-axi auth login --profile soc --method device-code --scopes https://graph.microsoft.com/User.Read"] },
|
|
526
526
|
{ path: "setup", description: "Show installation, configuration and capability guidance without signing in or writing anything", flags: {}, examples: ["mg-axi setup", "mg-axi setup --help"] },
|
|
527
527
|
{ path: "setup hooks", description: "Install or repair agent SessionStart hooks for mg-axi ambient context", flags: {}, examples: ["mg-axi setup hooks", "mg-axi setup hooks --help"] },
|
|
528
528
|
{ path: "doctor", description: "Check each selected profile with one bounded user-list read; never signs in interactively, installs nothing, enables no writes", flags: {
|
|
@@ -677,7 +677,7 @@ export const LEAVES = [
|
|
|
677
677
|
cursor: roleRead.cursor,
|
|
678
678
|
scopes: { value: "comma-separated-Graph-scopes", description: "Delegated only: explicit Graph scopes using full https://graph.microsoft.com/ names; defaults to RoleManagement.Read.Directory; delegated callers also need a supported directory role" },
|
|
679
679
|
}, examples: ["mg-axi entra directory-role member list --role <role-id> --profile soc", "mg-axi entra directory-role member list --role <role-id> --profile soc --filter \"displayName eq 'Adele Vance'\""] },
|
|
680
|
-
{ path: "entra directory-role scoped-member list", description: "List the scoped members of one activated directory role (id,
|
|
680
|
+
{ path: "entra directory-role scoped-member list", description: "List the scoped members of one activated directory role (id, roleMemberInfo, roleId, administrativeUnitId); administrative-unit scopes need P1", operation: "GET:/directoryRoles/{directoryRole-id}/scopedMembers", flags: {
|
|
681
681
|
...common, role: { value: "role-id", required: true, description: "Activated directory-role object ID whose scoped members are listed" },
|
|
682
682
|
limit: { value: "positive-integer", default: "100", description: "Cap returned rows; the remainder is buffered into an opaque cursor, never discarded; incompatible with --all" },
|
|
683
683
|
all: { description: "Follow pages within request, byte and deadline budgets" },
|
|
@@ -687,7 +687,7 @@ export const LEAVES = [
|
|
|
687
687
|
full: roleRead.full,
|
|
688
688
|
cursor: roleRead.cursor,
|
|
689
689
|
scopes: { value: "comma-separated-Graph-scopes", description: "Delegated only: explicit Graph scopes using full https://graph.microsoft.com/ names; defaults to RoleManagement.Read.Directory; delegated callers also need a supported directory role; administrative-unit scopes need P1" },
|
|
690
|
-
}, examples: ["mg-axi entra directory-role scoped-member list --role <role-id> --profile soc", "mg-axi entra directory-role scoped-member list --role <role-id> --profile soc --filter \"
|
|
690
|
+
}, examples: ["mg-axi entra directory-role scoped-member list --role <role-id> --profile soc", "mg-axi entra directory-role scoped-member list --role <role-id> --profile soc --filter \"administrativeUnitId eq '<unit-id>'\""] },
|
|
691
691
|
{ path: "entra directory-role scoped-member show", description: "Show one scoped member of an activated directory role with richer default properties", operation: "GET:/directoryRoles/{directoryRole-id}/scopedMembers/{scopedRoleMembership-id}", flags: {
|
|
692
692
|
...common, role: { value: "role-id", required: true, description: "Activated directory-role object ID owning the scoped member" },
|
|
693
693
|
id: { value: "scoped-membership-id", required: true, description: "Scoped membership object ID" },
|
|
@@ -696,7 +696,7 @@ export const LEAVES = [
|
|
|
696
696
|
full: roleRead.full,
|
|
697
697
|
scopes: { value: "comma-separated-Graph-scopes", description: "Delegated only: explicit Graph scopes using full https://graph.microsoft.com/ names; defaults to RoleManagement.Read.Directory; delegated callers also need a supported directory role; administrative-unit scopes need P1" },
|
|
698
698
|
}, examples: ["mg-axi entra directory-role scoped-member show --role <role-id> --id <scoped-membership-id> --profile soc", "mg-axi entra directory-role scoped-member show --role <role-id> --id <scoped-membership-id> --profile soc --full"] },
|
|
699
|
-
{ path: "entra scoped-role-membership list", description: "
|
|
699
|
+
{ path: "entra scoped-role-membership list", description: "Unsupported by Graph v1.0; use directory-role scoped-member list --role <role-id>", operation: "GET:/scopedRoleMemberships", flags: {
|
|
700
700
|
...common,
|
|
701
701
|
limit: { value: "positive-integer", default: "100", description: "Cap returned rows; the remainder is buffered into an opaque cursor, never discarded; incompatible with --all" },
|
|
702
702
|
all: { description: "Follow pages within request, byte and deadline budgets" },
|
|
@@ -707,7 +707,7 @@ export const LEAVES = [
|
|
|
707
707
|
cursor: roleRead.cursor,
|
|
708
708
|
scopes: { value: "comma-separated-Graph-scopes", description: "Delegated only: explicit Graph scopes using full https://graph.microsoft.com/ names; defaults to RoleManagement.Read.Directory; delegated callers also need a supported directory role; administrative-unit scopes need P1" },
|
|
709
709
|
}, examples: ["mg-axi entra scoped-role-membership list --profile soc", "mg-axi entra scoped-role-membership list --profile soc --filter \"roleId eq '<role-id>'\""] },
|
|
710
|
-
{ path: "entra scoped-role-membership show", description: "
|
|
710
|
+
{ path: "entra scoped-role-membership show", description: "Unsupported by Graph v1.0; use directory-role scoped-member show --role <role-id> --id <scoped-membership-id>", operation: "GET:/scopedRoleMemberships/{scopedRoleMembership-id}", flags: {
|
|
711
711
|
...common, id: { value: "scoped-membership-id", required: true, description: "Scoped membership object ID" },
|
|
712
712
|
select: roleRead.select,
|
|
713
713
|
fields: roleRead.fields,
|
|
@@ -981,7 +981,7 @@ export const LEAVES = [
|
|
|
981
981
|
full: dnsRead.full,
|
|
982
982
|
scopes: dnsRead.scopes,
|
|
983
983
|
}, examples: ["mg-axi entra domain service-configuration-record show --domain contoso.com --id <record-id> --profile soc"] },
|
|
984
|
-
{ path: "entra domain-dns-record list", description: "
|
|
984
|
+
{ path: "entra domain-dns-record list", description: "Unsupported by Graph v1.0; use domain verification-dns-record list or domain service-configuration-record list with --domain <domain-id>", operation: "GET:/domainDnsRecords", flags: {
|
|
985
985
|
...common,
|
|
986
986
|
limit: { value: "positive-integer", default: "100", description: "Cap returned rows; the remainder is buffered into an opaque cursor, never discarded; incompatible with --all" },
|
|
987
987
|
all: { description: "Follow pages within request, byte and deadline budgets" },
|
|
@@ -1145,7 +1145,7 @@ export const LEAVES = [
|
|
|
1145
1145
|
full: acceptanceRead.full,
|
|
1146
1146
|
scopes: acceptanceRead.scopes,
|
|
1147
1147
|
}, examples: ["mg-axi entra agreement-acceptance show --id <acceptance-id> --profile soc"] },
|
|
1148
|
-
{ path: "entra directory-object list", description: "
|
|
1148
|
+
{ path: "entra directory-object list", description: "Unsupported by Graph v1.0; use directory-object show --id <object-id> or a named user, group, device, application or service-principal list", operation: "GET:/directoryObjects", flags: {
|
|
1149
1149
|
...common,
|
|
1150
1150
|
limit: { value: "positive-integer", default: "100", description: "Cap returned rows; the remainder is buffered into an opaque cursor, never discarded; incompatible with --all" },
|
|
1151
1151
|
all: { description: "Follow pages within request, byte and deadline budgets" },
|
|
@@ -1254,7 +1254,7 @@ export const LEAVES = [
|
|
|
1254
1254
|
limit: { value: "positive-integer", default: "100", description: "Cap returned rows; the remainder is buffered into an opaque cursor, never discarded; incompatible with --all" },
|
|
1255
1255
|
all: { description: "Follow pages within request, byte and deadline budgets" },
|
|
1256
1256
|
filter: lifecycleRead.filter,
|
|
1257
|
-
select:
|
|
1257
|
+
select: { value: "comma-separated-properties", description: "Select output properties locally; no $select is sent to Graph" },
|
|
1258
1258
|
fields: lifecycleRead.fields,
|
|
1259
1259
|
full: lifecycleRead.full,
|
|
1260
1260
|
cursor: lifecycleRead.cursor,
|
|
@@ -1599,7 +1599,7 @@ export const LEAVES = [
|
|
|
1599
1599
|
since: { ...logRead.since, description: "Required for a new query (resume with --cursor instead): earliest activityDateTime bounding the server time range" },
|
|
1600
1600
|
until: logRead.until,
|
|
1601
1601
|
filter: logRead.filter,
|
|
1602
|
-
select: { value: "comma-separated-properties", description: "
|
|
1602
|
+
select: { value: "comma-separated-properties", description: "Select output properties locally from the provisioning-log set, including provisioningAction; no $select is sent to Graph" },
|
|
1603
1603
|
fields: logRead.fields,
|
|
1604
1604
|
full: logRead.full,
|
|
1605
1605
|
cursor: logRead.cursor,
|
|
@@ -1921,7 +1921,7 @@ export const LEAVES = [
|
|
|
1921
1921
|
}, examples: ["mg-axi entra conditional-access auth-strength-policy list --profile soc", "mg-axi entra conditional-access auth-strength-policy list --profile soc --limit 10", "mg-axi entra conditional-access auth-strength-policy list --profile soc --filter \"policyType eq 'builtIn'\" --select id,displayName,policyType"] },
|
|
1922
1922
|
{ path: "entra conditional-access auth-strength-policy show", description: "Show one authentication-strength policy with the full reviewed property set", operation: "GET:/identity/conditionalAccess/authenticationStrength/policies/{authenticationStrengthPolicy-id}", flags: {
|
|
1923
1923
|
...common, id: { value: "auth-strength-policy-id", required: true, description: "Authentication-strength policy object ID" },
|
|
1924
|
-
select:
|
|
1924
|
+
select: { value: "comma-separated-properties", description: "Select output properties locally; no $select is sent to Graph" },
|
|
1925
1925
|
fields: caStrengthRead.fields,
|
|
1926
1926
|
full: caStrengthRead.full,
|
|
1927
1927
|
scopes: caStrengthRead.scopes,
|
|
@@ -1987,7 +1987,7 @@ export const LEAVES = [
|
|
|
1987
1987
|
limit: { value: "positive-integer", default: "100", description: "Cap returned rows; the remainder is buffered into an opaque cursor, never discarded; incompatible with --all" },
|
|
1988
1988
|
all: { description: "Follow pages within request, byte and deadline budgets" },
|
|
1989
1989
|
filter: caAuthContextRead.filter,
|
|
1990
|
-
select:
|
|
1990
|
+
select: { value: "comma-separated-properties", description: "Select output properties locally; no $select is sent to Graph" },
|
|
1991
1991
|
fields: caAuthContextRead.fields,
|
|
1992
1992
|
full: caAuthContextRead.full,
|
|
1993
1993
|
cursor: caAuthContextRead.cursor,
|
|
@@ -2790,6 +2790,12 @@ export const LEAVES = [
|
|
|
2790
2790
|
full: { description: "Disable 500-character string truncation; never disables redaction or row caps" },
|
|
2791
2791
|
}, examples: ["mg-axi api get /users --scopes https://graph.microsoft.com/User.Read.All", "mg-axi api get /groups --odata '$filter=securityEnabled eq true&$top=5' --scopes https://graph.microsoft.com/GroupMember.Read.All", "mg-axi api get /users --cursor - --all --scopes https://graph.microsoft.com/User.Read.All < cursor.txt"] },
|
|
2792
2792
|
];
|
|
2793
|
+
LEAVES.push({ ...LEAVES.find(leaf => leaf.path === "auth login"), path: "login", description: "Deprecated alias for auth login (one minor release)" });
|
|
2794
|
+
LEAVES.push({ path: "config path", description: "Show the selected configuration path without reading credentials", flags: {}, examples: ["mg-axi config path", "mg-axi config path --config /absolute/path/config.json"] });
|
|
2795
|
+
LEAVES.push({ path: "auth status", description: "Show credential sources and verify local file permissions without a live request", flags: { profile: common.profile }, examples: ["mg-axi auth status", "mg-axi auth status --profile soc"] });
|
|
2796
|
+
LEAVES.push({ path: "auth logout", description: "Delete the selected delegated login cache without prompting", flags: { profile: common.profile }, examples: ["mg-axi auth logout", "mg-axi auth logout --profile soc"] });
|
|
2797
|
+
for (const leaf of LEAVES)
|
|
2798
|
+
leaf.flags.config = { value: "path", description: "Configuration file override; takes precedence over MG_AXI_CONFIG" };
|
|
2793
2799
|
export function leafHelp(leaf) {
|
|
2794
2800
|
return [
|
|
2795
2801
|
`mg-axi ${leaf.path}${leaf.positional ? ` <${leaf.positional.name}>` : ""}`, leaf.description,
|
|
@@ -2828,7 +2834,7 @@ export function resolveCommand(argv) {
|
|
|
2828
2834
|
const flags = Object.create(null);
|
|
2829
2835
|
for (let i = flagStart; i < argv.length; i++) {
|
|
2830
2836
|
const arg = argv[i];
|
|
2831
|
-
const match = /^--([a-z-]
|
|
2837
|
+
const match = /^--([a-z][a-z0-9-]*)(?:=(.*))?$/.exec(arg);
|
|
2832
2838
|
if (!match)
|
|
2833
2839
|
fail("unexpected argument or short flag");
|
|
2834
2840
|
const name = match[1];
|