@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,139 @@
1
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
+ import { randomUUID } from "node:crypto";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { AxiError } from "axi-sdk-js";
6
+ const guid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
+ const namePattern = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
8
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
9
+ const exact = (value, keys) => Object.keys(value).every(key => keys.includes(key));
10
+ function invalid(message) {
11
+ throw new AxiError(message, "VALIDATION_ERROR", ["mg-axi profile create --help", "Correct the version-1 profile configuration; no automatic migration is performed"]);
12
+ }
13
+ const thumbprint = /^[0-9a-f]{40}$/i;
14
+ const sharedKeys = ["mode", "tenantId", "clientId", "cloud", "enabledPacks", "preview", "sensitiveAreas", "credentialRef", "allowDeviceCode", "writes"];
15
+ const MAX_WRITE_OPERATIONS = 64;
16
+ const MAX_WRITE_OPERATION_LENGTH = 256;
17
+ function validateWrites(value) {
18
+ if (value === undefined)
19
+ return undefined;
20
+ if (!object(value) || !exact(value, ["allowWrites", "operations"]) || typeof value.allowWrites !== "boolean" ||
21
+ !Array.isArray(value.operations) || value.operations.length === 0 || value.operations.length > MAX_WRITE_OPERATIONS ||
22
+ value.operations.some(entry => typeof entry !== "string" || !entry.length || entry.length > MAX_WRITE_OPERATION_LENGTH)) {
23
+ invalid("Invalid write policy; hand-edit a writes object with boolean allowWrites and a nonempty operations array of nonempty operation names");
24
+ }
25
+ return Object.freeze({ allowWrites: value.allowWrites, operations: Object.freeze([...value.operations]) });
26
+ }
27
+ const sharedIdentity = (value) => value.cloud === "commercial" && typeof value.tenantId === "string" && guid.test(value.tenantId) &&
28
+ typeof value.clientId === "string" && guid.test(value.clientId) && typeof value.preview === "boolean" &&
29
+ Array.isArray(value.enabledPacks) && value.enabledPacks.every(pack => pack === "entra") && new Set(value.enabledPacks).size === value.enabledPacks.length &&
30
+ Array.isArray(value.sensitiveAreas) && value.sensitiveAreas.length === 0;
31
+ export function validateDelegatedProfile(value) {
32
+ if (!object(value) || !exact(value, sharedKeys) || value.mode !== "delegated" || !sharedIdentity(value) || typeof value.allowDeviceCode !== "boolean" ||
33
+ !object(value.credentialRef) || !exact(value.credentialRef, ["provider", "key"]) || value.credentialRef.provider !== "os-or-session" ||
34
+ typeof value.credentialRef.key !== "string" || !guid.test(value.credentialRef.key))
35
+ invalid("Invalid delegated profile; explicit tenant/client IDs and commercial cloud are required, with credentials only by reference");
36
+ const profile = value;
37
+ const writes = validateWrites(value.writes);
38
+ return Object.freeze({ mode: "delegated", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial", preview: profile.preview,
39
+ allowDeviceCode: profile.allowDeviceCode, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
40
+ credentialRef: Object.freeze({ provider: "os-or-session", key: profile.credentialRef.key }), ...(writes === undefined ? {} : { writes }) });
41
+ }
42
+ export function validateApplicationProfile(value) {
43
+ if (!object(value) || !exact(value, sharedKeys) || value.mode !== "application" || !sharedIdentity(value) || value.allowDeviceCode !== false ||
44
+ !object(value.credentialRef))
45
+ invalid("Invalid application profile; explicit tenant/client IDs and commercial cloud are required, with credentials only by reference");
46
+ const profile = value;
47
+ const ref = profile.credentialRef;
48
+ const writes = validateWrites(value.writes);
49
+ const writeFields = writes === undefined ? {} : { writes };
50
+ if (ref.provider === "certificate" && exact(ref, ["provider", "key", "thumbprint"]) && typeof ref.key === "string" && guid.test(ref.key) &&
51
+ typeof ref.thumbprint === "string" && thumbprint.test(ref.thumbprint)) {
52
+ return Object.freeze({ mode: "application", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial",
53
+ preview: profile.preview, allowDeviceCode: false, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
54
+ credentialRef: Object.freeze({ provider: "certificate", key: ref.key, thumbprint: ref.thumbprint.toLowerCase() }), ...writeFields });
55
+ }
56
+ if (ref.provider === "federated" && exact(ref, ["provider", "key"]) && typeof ref.key === "string" && guid.test(ref.key)) {
57
+ return Object.freeze({ mode: "application", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial",
58
+ preview: profile.preview, allowDeviceCode: false, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
59
+ credentialRef: Object.freeze({ provider: "federated", key: ref.key }), ...writeFields });
60
+ }
61
+ invalid("Invalid application profile; credentialRef must be a certificate or federated reference, never inlined key material");
62
+ }
63
+ export function validateProfile(value) {
64
+ if (!!value && typeof value === "object" && !Array.isArray(value) && value.mode === "application")
65
+ return validateApplicationProfile(value);
66
+ return validateDelegatedProfile(value);
67
+ }
68
+ export class Profiles {
69
+ path;
70
+ constructor(path = process.env.MG_AXI_CONFIG ?? join(homedir(), ".mg-axi", "config.json")) {
71
+ this.path = path;
72
+ }
73
+ load() {
74
+ let value;
75
+ try {
76
+ value = JSON.parse(readFileSync(this.path, "utf8"));
77
+ }
78
+ catch (error) {
79
+ if (error.code === "ENOENT")
80
+ return { version: 1, profiles: {} };
81
+ invalid("Unable to read profile configuration; check the file permissions and version-1 JSON schema");
82
+ }
83
+ if (!object(value) || value.version !== 1 || !exact(value, ["version", "defaultProfile", "profiles"]) || !object(value.profiles))
84
+ invalid("Unsupported profile configuration; explicitly migrate to version 1");
85
+ const profiles = Object.create(null);
86
+ for (const [name, profile] of Object.entries(value.profiles)) {
87
+ if (!namePattern.test(name))
88
+ invalid("Invalid profile name");
89
+ profiles[name] = validateProfile(profile);
90
+ }
91
+ if (value.defaultProfile !== undefined && (typeof value.defaultProfile !== "string" || !Object.hasOwn(profiles, value.defaultProfile)))
92
+ invalid("Default profile does not exist");
93
+ if (new Set(Object.values(profiles).map(profile => profile.credentialRef.key)).size !== Object.keys(profiles).length)
94
+ invalid("Each profile must have a separate credential reference");
95
+ return { version: 1, profiles, defaultProfile: value.defaultProfile };
96
+ }
97
+ list() { return Object.entries(this.load().profiles).map(([name, profile]) => ({ name, mode: profile.mode, tenant: profile.tenantId, cloud: profile.cloud })); }
98
+ resolve(name) {
99
+ const config = this.load();
100
+ const selected = name ?? config.defaultProfile;
101
+ if (!selected || !Object.hasOwn(config.profiles, selected))
102
+ throw new AxiError("No configured profile selected", "AUTH_REQUIRED", ["mg-axi profile list", "mg-axi profile create --help"]);
103
+ return { name: selected, profile: config.profiles[selected] };
104
+ }
105
+ create(name, tenantId, clientId, cloud, allowDeviceCode, application) {
106
+ if (!namePattern.test(name))
107
+ invalid("Profile name must contain only letters, digits, underscores or hyphens");
108
+ const certificate = application?.certificateThumbprint;
109
+ const federated = application?.federated ?? false;
110
+ if (application && (certificate !== undefined && (typeof certificate !== "string" || !/^[0-9a-f]{40}$/i.test(certificate))))
111
+ invalid("Application certificate profiles name a 40-hex-digit thumbprint; the private key stays in protected storage");
112
+ if (application && ((certificate === undefined) === !federated))
113
+ invalid("Application profiles use exactly one credential: --certificate-thumbprint or --federated");
114
+ if (application && allowDeviceCode)
115
+ invalid("Application profiles never use device code; it is a delegated login method");
116
+ const profile = application
117
+ ? validateApplicationProfile({ mode: "application", tenantId, clientId, cloud, enabledPacks: ["entra"], preview: false, sensitiveAreas: [],
118
+ credentialRef: certificate === undefined ? { provider: "federated", key: randomUUID() } : { provider: "certificate", key: randomUUID(), thumbprint: certificate }, allowDeviceCode: false })
119
+ : validateDelegatedProfile({ mode: "delegated", tenantId, clientId, cloud, enabledPacks: ["entra"], preview: false, sensitiveAreas: [], credentialRef: { provider: "os-or-session", key: randomUUID() }, allowDeviceCode });
120
+ const config = this.load();
121
+ if (Object.hasOwn(config.profiles, name))
122
+ invalid("Profile already exists; creation never replaces identity");
123
+ config.profiles[name] = profile;
124
+ config.defaultProfile ??= name;
125
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
126
+ const temporary = `${this.path}.${randomUUID()}.tmp`;
127
+ try {
128
+ writeFileSync(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600, flag: "wx" });
129
+ renameSync(temporary, this.path);
130
+ }
131
+ catch {
132
+ throw new AxiError("Unable to save profile configuration", "CONFIG_ERROR", ["Check configuration directory permissions"]);
133
+ }
134
+ finally {
135
+ rmSync(temporary, { force: true });
136
+ }
137
+ return { name, mode: profile.mode, tenant: profile.tenantId, cloud: profile.cloud };
138
+ }
139
+ }
package/dist/setup.js ADDED
@@ -0,0 +1,116 @@
1
+ import { LEAVES } from "./catalogue.js";
2
+ // PACK-01: explicit release setup guidance. The setup view is offline: it
3
+ // reads the selected configuration file, describes installation and the
4
+ // supported Entra read surface, and points at the generated skill and
5
+ // capability report. It writes nothing, signs in nowhere and installs no
6
+ // hooks; ordinary commands never gain installation side effects. See
7
+ // README.md for the release notes and skills/mg-axi/SKILL.md for the
8
+ // installable skill.
9
+ // EXT-01b document gate (D2): the capability prose lists the shipped read
10
+ // families, driven from the catalogue in catalogue order, so a merged read
11
+ // slice extends the sentence without touching this template. Paths outside
12
+ // the known prefixes fall back to their two-segment leaf key and are never
13
+ // silently omitted from the list.
14
+ const READ_FAMILY_PREFIXES = [
15
+ ["entra user", "user"],
16
+ ["entra registration", "registration"],
17
+ ["entra group", "group"],
18
+ ["entra directory-role", "directory-role/PIM"],
19
+ ["entra role-assignment", "directory-role/PIM"],
20
+ ["entra pim", "directory-role/PIM"],
21
+ ["entra device", "device"],
22
+ ["entra administrative-unit", "administrative-unit"],
23
+ ["entra organization", "organization and branding"],
24
+ ["entra domain", "domain and domain DNS"],
25
+ ["entra domain-dns-record", "domain and domain DNS"],
26
+ ["entra certificate-auth-pki", "certificate authentication"],
27
+ ["entra certificate-authority", "certificate authentication"],
28
+ ["entra subscription", "commercial subscriptions"],
29
+ ["entra on-premises-synchronization", "on-premises synchronization"],
30
+ ["entra agreement", "terms-of-use agreements and acceptances"],
31
+ ["entra agreement-acceptance", "terms-of-use agreements and acceptances"],
32
+ ["entra directory-object", "directory objects"],
33
+ ["entra deleted-user", "deleted directory items"],
34
+ ["entra deleted-group", "deleted directory items"],
35
+ ["entra deleted-application", "deleted directory items"],
36
+ ["entra deleted-service-principal", "deleted directory items"],
37
+ ["entra deleted-administrative-unit", "deleted directory items"],
38
+ ["entra deleted-item", "deleted directory items"],
39
+ ["entra contact", "organizational contacts"],
40
+ ["entra group-lifecycle-policy", "group lifecycle policies"],
41
+ ["entra group-setting-template", "group setting templates"],
42
+ ["entra attribute-set", "attribute sets and custom security attributes"],
43
+ ["entra custom-security-attribute-definition", "attribute sets and custom security attributes"],
44
+ ["entra allowed-value", "attribute sets and custom security attributes"],
45
+ ["entra contract", "partner contracts"],
46
+ ["entra delegated-admin-customer", "delegated administration"],
47
+ ["entra delegated-admin-relationship", "delegated administration"],
48
+ ["entra multi-tenant-organization", "multi-tenant organization"],
49
+ ["entra tenant-information", "tenant information"],
50
+ ["entra sign-in", "sign-in/directory-audit"],
51
+ ["entra directory-audit", "sign-in/directory-audit"],
52
+ ["entra application", "application/service-principal and consent grants"],
53
+ ["entra service-principal", "application/service-principal and consent grants"],
54
+ ["entra risky-user", "risk"],
55
+ ["entra risk-detection", "risk"],
56
+ ["entra conditional-access", "Conditional Access"],
57
+ ["entra identity-provider", "identity-provider"],
58
+ ["entra federation-configuration", "federation configurations"],
59
+ ["entra data-policy-operation", "data policy operations"],
60
+ ["entra access-review", "access-review"],
61
+ ["entra entitlement catalog", "entitlement catalogs and access packages"],
62
+ ["entra entitlement access-package", "entitlement catalogs and access packages"],
63
+ ["entra entitlement assignment", "entitlement assignments and requests"],
64
+ ["entra entitlement assignment-request", "entitlement assignments and requests"],
65
+ ["entra entitlement assignment-policy", "entitlement catalogs and access packages"],
66
+ ["entra entitlement resource-role-scope", "entitlement catalogs and access packages"],
67
+ ["entra lifecycle", "lifecycle workflows"],
68
+ ];
69
+ function shippedReadFamilies() {
70
+ const seen = [];
71
+ for (const leaf of LEAVES) {
72
+ if (leaf.operation === undefined || !leaf.operation.startsWith("GET:"))
73
+ continue;
74
+ const match = READ_FAMILY_PREFIXES.find(([prefix]) => leaf.path === prefix || leaf.path.startsWith(`${prefix} `));
75
+ const label = match ? match[1] : leaf.path.split(" ").slice(0, 2).join(" ");
76
+ if (!seen.includes(label))
77
+ seen.push(label);
78
+ }
79
+ return seen;
80
+ }
81
+ export function setupView(store) {
82
+ const families = shippedReadFamilies();
83
+ const items = store.list();
84
+ const implemented = LEAVES.map(leaf => leaf.path);
85
+ const reads = LEAVES.filter(leaf => (leaf.operation !== undefined && leaf.operation.startsWith("GET:")) || leaf.path === "api get" || leaf.path === "doctor").length;
86
+ const writes = LEAVES.filter(leaf => leaf.operation !== undefined && !leaf.operation.startsWith("GET:")).length;
87
+ return {
88
+ command: "mg-axi setup",
89
+ config: store.path,
90
+ state: items.length ? "configured" : "unconfigured",
91
+ profiles: items.length
92
+ ? items.map(item => ({ name: item.name, mode: item.mode }))
93
+ : "0 profiles configured",
94
+ installation: [
95
+ "corepack pnpm install --frozen-lockfile --ignore-scripts --config.confirm-modules-purge=false",
96
+ "corepack pnpm build",
97
+ "node dist/bin/mg-axi.js --version",
98
+ ],
99
+ guidance: [
100
+ `Hand-edit ${store.path} or set MG_AXI_CONFIG to a separate configuration file; secrets stay in protected storage references, never in argv or config`,
101
+ "node dist/bin/mg-axi.js profile create --name soc --tenant <tenant-id> --client <client-id> --cloud commercial",
102
+ "node dist/bin/mg-axi.js login --profile soc --scopes https://graph.microsoft.com/User.Read.All",
103
+ "node dist/bin/mg-axi.js doctor",
104
+ ],
105
+ capabilities: {
106
+ implemented,
107
+ reads,
108
+ writes,
109
+ local: implemented.length - reads - writes,
110
+ api: `Entra ${families.slice(0, -1).join(", ")} and ${families.at(-1)} reads plus reviewed raw api get, one gated group-membership write, one gated account enable/disable write, one gated session-revocation write and one gated risky-user dismissal write; every other operation is scheduled, blocked, deprecated or excluded`,
111
+ report: "docs/coverage.md",
112
+ },
113
+ integration: "Install skills/mg-axi/SKILL.md explicitly through your agent's skill installation mechanism; setup only shows guidance and installs no skills or hooks",
114
+ help: ["node dist/bin/mg-axi.js doctor", "node dist/bin/mg-axi.js entra user list --help", "node dist/bin/mg-axi.js api get --help"],
115
+ };
116
+ }
@@ -0,0 +1,2 @@
1
+ import { readFileSync } from "node:fs";
2
+ export const VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;