Update version to 0.2.1 and add create-pca script for Azure app management

This commit is contained in:
2026-03-05 22:03:10 +01:00
parent b88b35cb90
commit 21b8179d40
2 changed files with 270 additions and 2 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@slawek/sk-az-tools",
"version": "0.2.0",
"version": "0.2.1",
"type": "module",
"files": [
"dist",
@@ -11,7 +11,8 @@
"clean": "rm -rf dist",
"build": "npm run clean && tsc && chmod +x dist/cli.js",
"build:watch": "tsc --watch",
"prepublishOnly": "npm run build"
"prepublishOnly": "npm run build",
"create-pca": "node dist/create-pca.js"
},
"engines": {
"node": ">=24.0.0"

267
src/create-pca.ts Normal file
View File

@@ -0,0 +1,267 @@
#!/usr/bin/env node
// SPDX-License-Identifier: MIT
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import readline from "node:readline";
import { spawnSync } from "node:child_process";
import { parseArgs } from "node:util";
type RunAzOptions = {
quiet?: boolean;
allowFailure?: boolean;
};
type RunAzResult = {
status: number;
stdout: string;
stderr: string;
};
function runAz(args: string[], options: RunAzOptions = {}): RunAzResult {
const result = spawnSync("az", args, {
encoding: "utf8",
stdio: options.quiet
? ["ignore", "ignore", "ignore"]
: ["ignore", "pipe", "pipe"],
});
if (result.error) {
throw result.error;
}
if (result.status !== 0 && options.allowFailure !== true) {
throw new Error(
(result.stderr || "").trim() || `az ${args.join(" ")} failed`,
);
}
return {
status: result.status ?? 1,
stdout: (result.stdout || "").trim(),
stderr: (result.stderr || "").trim(),
};
}
async function main(): Promise<void> {
const usageText = `Usage: ${path.basename(process.argv[1])} [options] <app-name>
Options:
-c, --config <path> Write JSON config to file (optional)
-h, --help Show this help message and exit`;
let values: Record<string, string | boolean | undefined>;
let positionals: string[];
try {
({ values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
help: { type: "boolean", short: "h" },
config: { type: "string", short: "c" },
},
strict: true,
allowPositionals: true,
}));
} catch (err) {
console.error(`Error: ${(err as Error).message}`);
console.error(usageText);
process.exit(1);
}
if (values.help) {
console.log(usageText);
process.exit(0);
}
if (positionals.length > 1) {
console.error(
"Error: Too many positional arguments. Only one app name positional argument is allowed.",
);
console.error(usageText);
process.exit(1);
}
const appName = positionals[0] || "";
const configPath = typeof values.config === "string" ? values.config : "";
if (!appName) {
console.error("Error: Application name is required.");
console.error(usageText);
process.exit(1);
}
let appId = runAz([
"ad",
"app",
"list",
"--display-name",
appName,
"--query",
"[0].appId",
"-o",
"tsv",
]).stdout;
if (appId) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stderr,
});
const answer = await new Promise<string>((resolve) => {
rl.question(
`Application '${appName}' already exists. Update it? [y/N]: `,
(answerValue) => {
rl.close();
resolve(answerValue.trim());
},
);
});
if (!/^(yes|y)$/i.test(answer)) {
console.error("Canceled.");
process.exit(0);
}
}
if (!appId) {
appId = runAz([
"ad",
"app",
"create",
"--display-name",
appName,
"--query",
"appId",
"-o",
"tsv",
]).stdout;
if (!appId) {
console.error(`Error: Failed to create application '${appName}'.`);
process.exit(1);
}
}
const requiredResourceAccess = [
{
resourceAppId: "00000003-0000-0000-c000-000000000000",
resourceAccess: [
{ id: "0e263e50-5827-48a4-b97c-d940288653c7", type: "Scope" },
],
},
{
resourceAppId: "499b84ac-1321-427f-aa17-267ca6975798",
resourceAccess: [
{ id: "ee69721e-6c3a-468f-a9ec-302d16a4c599", type: "Scope" },
],
},
{
resourceAppId: "797f4846-ba00-4fd7-ba43-dac1f8f63013",
resourceAccess: [
{ id: "41094075-9dad-400e-a0bd-54e686782033", type: "Scope" },
],
},
];
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pca-"));
const requiredResourceAccessFile = path.join(
tempDir,
"required-resource-accesses.json",
);
try {
fs.writeFileSync(
requiredResourceAccessFile,
JSON.stringify(requiredResourceAccess, null, 2),
"utf8",
);
try {
runAz(
[
"ad",
"app",
"update",
"--id",
appId,
"--sign-in-audience",
"AzureADMyOrg",
"--is-fallback-public-client",
"true",
"--required-resource-accesses",
`@${requiredResourceAccessFile}`,
"--public-client-redirect-uris",
"http://localhost",
`msal${appId}://auth`,
"--enable-access-token-issuance",
"true",
"--enable-id-token-issuance",
"true",
],
{ quiet: true },
);
} catch {
console.error(
`Error: Failed to configure application '${appName}' (${appId}).`,
);
process.exit(1);
}
} finally {
fs.rmSync(tempDir, { recursive: true, force: true });
}
runAz(["ad", "sp", "create", "--id", appId], {
quiet: true,
allowFailure: true,
});
const adminConsentResult = runAz(
["ad", "app", "permission", "admin-consent", "--id", appId],
{ quiet: true, allowFailure: true },
);
if (adminConsentResult.status !== 0) {
console.warn(
`Warning: Failed to grant admin consent for '${appName}' (${appId}). Continuing without failing.`,
);
if (adminConsentResult.stderr) {
console.warn(adminConsentResult.stderr);
}
}
const tenantId = runAz([
"account",
"show",
"--query",
"tenantId",
"-o",
"tsv",
]).stdout;
if (!tenantId) {
console.error(
"Error: Failed to resolve tenantId from current Azure CLI context.",
);
process.exit(1);
}
const configTemplate = JSON.stringify(
{
tenantId,
clientId: appId,
},
null,
2,
);
if (configPath) {
const targetPath = path.resolve(configPath);
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
fs.writeFileSync(targetPath, `${configTemplate}\n`, "utf8");
}
console.log(configTemplate);
}
main().catch((err) => {
console.error(`Error: ${(err as Error).message}`);
process.exit(1);
});