From 21b469c11892ce730fbe90f785f8f0b59f7c90f1 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Thu, 5 Feb 2026 05:55:08 +0100 Subject: [PATCH] Reorganized the module structure. --- package.json | 6 +- src/azure/client-auth.js | 37 +++++++++++ src/{azure.d.ts => azure/index.d.ts} | 0 src/azure/index.js | 9 +++ src/{azure.js => azure/pca-auth.js} | 58 ++--------------- src/{devops.d.ts => devops/index.d.ts} | 0 src/{devops.js => devops/index.js} | 2 +- src/graph.js | 87 -------------------------- src/graph/app.js | 32 ++++++++++ src/graph/auth.js | 27 ++++++++ src/{graph.d.ts => graph/index.d.ts} | 0 src/graph/index.js | 3 + src/graph/sp.js | 25 ++++++++ 13 files changed, 142 insertions(+), 144 deletions(-) create mode 100644 src/azure/client-auth.js rename src/{azure.d.ts => azure/index.d.ts} (100%) create mode 100644 src/azure/index.js rename src/{azure.js => azure/pca-auth.js} (68%) rename src/{devops.d.ts => devops/index.d.ts} (100%) rename src/{devops.js => devops/index.js} (95%) delete mode 100644 src/graph.js create mode 100644 src/graph/app.js create mode 100644 src/graph/auth.js rename src/{graph.d.ts => graph/index.d.ts} (100%) create mode 100644 src/graph/index.js create mode 100644 src/graph/sp.js diff --git a/package.json b/package.json index 8fbdcb6..f742528 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ }, "exports": { ".": "./src/index.js", - "./azure": "./src/azure.js", - "./graph": "./src/graph.js", - "./devops": "./src/devops.js" + "./azure": "./src/azure/index.js", + "./graph": "./src/graph/index.js", + "./devops": "./src/devops/index.js" } } diff --git a/src/azure/client-auth.js b/src/azure/client-auth.js new file mode 100644 index 0000000..bc83360 --- /dev/null +++ b/src/azure/client-auth.js @@ -0,0 +1,37 @@ +import { DefaultAzureCredential, ClientSecretCredential, DeviceCodeCredential } from "@azure/identity"; + +export async function getCredential(credentialType, options) { + switch (credentialType) { + case "d": + case "default": + return new DefaultAzureCredential(); + case "cs": + case "clientSecret": + if (!options.tenantId || !options.clientId || !options.clientSecret) { + throw new Error( + "tenantId, clientId, and clientSecret are required for ClientSecretCredential", + ); + } + return new ClientSecretCredential( + options.tenantId, + options.clientId, + options.clientSecret, + ); + case "dc": + case "deviceCode": + if (!options.tenantId || !options.clientId) { + throw new Error( + "tenantId and clientId are required for DeviceCodeCredential", + ); + } + return new DeviceCodeCredential({ + tenantId: options.tenantId, + clientId: options.clientId, + userPromptCallback: (info) => { + console.log(info.message); + }, + }); + default: + throw new Error(`Unsupported credential type: ${credentialType}`); + } +} diff --git a/src/azure.d.ts b/src/azure/index.d.ts similarity index 100% rename from src/azure.d.ts rename to src/azure/index.d.ts diff --git a/src/azure/index.js b/src/azure/index.js new file mode 100644 index 0000000..af61327 --- /dev/null +++ b/src/azure/index.js @@ -0,0 +1,9 @@ +/** + * @module azure + * + * This module provides authentication functionalities for Azure services. + * + */ + +export { getCredential } from "./client-auth.js"; +export { loginInteractive } from "./pca-auth.js"; diff --git a/src/azure.js b/src/azure/pca-auth.js similarity index 68% rename from src/azure.js rename to src/azure/pca-auth.js index 8575540..97e66ee 100644 --- a/src/azure.js +++ b/src/azure/pca-auth.js @@ -1,4 +1,4 @@ -// azure.js + import http from "node:http"; import { URL } from "node:url"; import open, { apps } from "open"; @@ -6,51 +6,8 @@ import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import os from "node:os"; -import { - PublicClientApplication, - ConfidentialClientApplication, -} from "@azure/msal-node"; -import { - DefaultAzureCredential, - ClientSecretCredential, - DeviceCodeCredential, -} from "@azure/identity"; -export async function getCredential(credentialType, options) { - switch (credentialType) { - case "d": - case "default": - return new DefaultAzureCredential(); - case "cs": - case "clientSecret": - if (!options.tenantId || !options.clientId || !options.clientSecret) { - throw new Error( - "tenantId, clientId, and clientSecret are required for ClientSecretCredential", - ); - } - return new ClientSecretCredential( - options.tenantId, - options.clientId, - options.clientSecret, - ); - case "dc": - case "deviceCode": - if (!options.tenantId || !options.clientId) { - throw new Error( - "tenantId and clientId are required for DeviceCodeCredential", - ); - } - return new DeviceCodeCredential({ - tenantId: options.tenantId, - clientId: options.clientId, - userPromptCallback: (info) => { - console.log(info.message); - }, - }); - default: - throw new Error(`Unsupported credential type: ${credentialType}`); - } -} +import { PublicClientApplication } from "@azure/msal-node"; function fileCachePlugin(cachePath) { return { @@ -63,6 +20,7 @@ function fileCachePlugin(cachePath) { if (ctx.cacheHasChanged) { fs.mkdirSync(path.dirname(cachePath), { recursive: true }); fs.writeFileSync(cachePath, ctx.tokenCache.serialize()); + fs.chmodSync(cachePath, 0o600); // Owner read/write only } }, }; @@ -78,21 +36,15 @@ function generatePkce() { return { verifier, challenge, challengeMethod: "S256" }; } -export async function loginInteractive({ appName, tenantId, clientId, scopes }) { +export async function loginInteractive({ tenantId, clientId, scopes }) { if (!tenantId) throw new Error("tenantId is required"); if (!clientId) throw new Error("clientId is required"); if (!Array.isArray(scopes) || scopes.length === 0) throw new Error("scopes[] is required"); - // Make app name lowercase with all non-alphanumeric characters removed - // spaces replaced with dashes and all letters converted to lowercase - const sanitizedAppName = (appName || "Azure Node Login") - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-"); - const cachePath = path.join( os.homedir(), - `.config/${sanitizedAppName}`, + `.config/sk-az-tools`, `${clientId}-token-cache.json`, ); diff --git a/src/devops.d.ts b/src/devops/index.d.ts similarity index 100% rename from src/devops.d.ts rename to src/devops/index.d.ts diff --git a/src/devops.js b/src/devops/index.js similarity index 95% rename from src/devops.js rename to src/devops/index.js index 18caeb4..3b75dd1 100644 --- a/src/devops.js +++ b/src/devops/index.js @@ -2,7 +2,7 @@ * A DevOps helpers module. */ -import { loginInteractive } from "./azure.js"; +import { loginInteractive } from "../azure/index.js"; import * as azdev from "azure-devops-node-api"; const AZURE_DEVOPS_SCOPES = ["https://app.vssps.visualstudio.com/.default"]; diff --git a/src/graph.js b/src/graph.js deleted file mode 100644 index 99bdcc9..0000000 --- a/src/graph.js +++ /dev/null @@ -1,87 +0,0 @@ -import { Client } from "@microsoft/microsoft-graph-client"; -import { loginInteractive } from "./azure.js"; - -/** - * Initialize and return a Microsoft Graph client - * along with the authentication token. - * - * @param { Object } options - Options for authentication - * @param { string } options.tenantId - The Azure AD tenant ID - * @param { string } options.clientId - The Azure AD client ID - * @returns { Object } An object containing the Graph API token and client - */ -export async function getGraphClient({ tenantId, clientId }) { - const graphApiToken = await loginInteractive({ - tenantId, - clientId, - scopes: ["https://graph.microsoft.com/.default"], - }); - - const client = Client.init({ - authProvider: (done) => { - done(null, graphApiToken.accessToken); - }, - }); - - return { graphApiToken, client }; -} - - -/** - * Get an Azure application by its display name. - * - * @param { Object } client - * @param { string } appName - * @returns - */ -export async function getApp(client, appName) { - const result = await client - .api("/applications") - .filter(`displayName eq '${appName}'`) - .get(); - - // Return the first application found or null if none exists - return result.value.length > 0 ? result.value[0] : null; -} - -export async function getServicePrincipal(client, appId) { - const result = await client - .api("/servicePrincipals") - .filter(`appId eq '${appId}'`) - .get(); - - // Return the first service principal found or null if none exists - return result.value.length > 0 ? result.value[0] : null; -} - -export async function createApp(client, appName) { - const app = await client.api("/applications").post({ - displayName: appName, - }); - - if (!app || !app.appId) { - throw new Error("Failed to create application"); - } - - return app; -} - -export async function createSp(client, appId) { - const sp = await client.api("/servicePrincipals").post({ - appId, - }); - - if (!sp || !sp.id) { - throw new Error("Failed to create service principal"); - } - - return sp; -} - -export async function deleteSp(client, spId) { - await client.api(`/servicePrincipals/${spId}`).delete(); -} - -export async function deleteApp(client, appObjectId) { - await client.api(`/applications/${appObjectId}`).delete(); -} diff --git a/src/graph/app.js b/src/graph/app.js new file mode 100644 index 0000000..a92c862 --- /dev/null +++ b/src/graph/app.js @@ -0,0 +1,32 @@ +/** + * Get an Azure application by its display name. + * + * @param { Object } client + * @param { string } appName + * @returns + */ +export async function getApp(client, appName) { + const result = await client + .api("/applications") + .filter(`displayName eq '${appName}'`) + .get(); + + // Return the first application found or null if none exists + return result.value.length > 0 ? result.value[0] : null; +} + +export async function createApp(client, appName) { + const app = await client.api("/applications").post({ + displayName: appName, + }); + + if (!app || !app.appId) { + throw new Error("Failed to create application"); + } + + return app; +} + +export async function deleteApp(client, appObjectId) { + await client.api(`/applications/${appObjectId}`).delete(); +} diff --git a/src/graph/auth.js b/src/graph/auth.js new file mode 100644 index 0000000..d773bb0 --- /dev/null +++ b/src/graph/auth.js @@ -0,0 +1,27 @@ +import { loginInteractive } from "../azure/index.js"; +import { Client } from "@microsoft/microsoft-graph-client"; + +/** + * Initialize and return a Microsoft Graph client + * along with the authentication token. + * + * @param { Object } options - Options for authentication + * @param { string } options.tenantId - The Azure AD tenant ID + * @param { string } options.clientId - The Azure AD client ID + * @returns { Object } An object containing the Graph API token and client + */ +export async function getGraphClient({ tenantId, clientId }) { + const graphApiToken = await loginInteractive({ + tenantId, + clientId, + scopes: ["https://graph.microsoft.com/.default"], + }); + + const client = Client.init({ + authProvider: (done) => { + done(null, graphApiToken.accessToken); + }, + }); + + return { graphApiToken, client }; +} diff --git a/src/graph.d.ts b/src/graph/index.d.ts similarity index 100% rename from src/graph.d.ts rename to src/graph/index.d.ts diff --git a/src/graph/index.js b/src/graph/index.js new file mode 100644 index 0000000..5db7376 --- /dev/null +++ b/src/graph/index.js @@ -0,0 +1,3 @@ +export * from "./auth.js"; +export * from "./app.js"; +export * from "./sp.js"; diff --git a/src/graph/sp.js b/src/graph/sp.js new file mode 100644 index 0000000..dbce258 --- /dev/null +++ b/src/graph/sp.js @@ -0,0 +1,25 @@ +export async function getServicePrincipal(client, appId) { + const result = await client + .api("/servicePrincipals") + .filter(`appId eq '${appId}'`) + .get(); + + // Return the first service principal found or null if none exists + return result.value.length > 0 ? result.value[0] : null; +} + +export async function createSp(client, appId) { + const sp = await client.api("/servicePrincipals").post({ + appId, + }); + + if (!sp || !sp.id) { + throw new Error("Failed to create service principal"); + } + + return sp; +} + +export async function deleteSp(client, spId) { + await client.api(`/servicePrincipals/${spId}`).delete(); +}