#!/usr/bin/env node 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"; function runAz(args, options = {}) { 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() { const usageText = `Usage: ${path.basename(process.argv[1])} [options] [app-name] Options: -n, --app-name Application display name (optional if positional app-name is provided) -c, --config Write config template to file (optional) -h, --help Show this help message and exit`; let values; let positionals; try { ({ values, positionals } = parseArgs({ args: process.argv.slice(2), options: { help: { type: "boolean", short: "h" }, "app-name": { type: "string", short: "n" }, config: { type: "string", short: "c" }, }, strict: true, allowPositionals: true, })); } catch (err) { console.error(`Error: ${err.message}`); console.log(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.log(usageText); process.exit(1); } const appName = values["app-name"] || positionals[0] || ""; const configPath = values.config || ""; if (!appName) { console.error("Error: Application name is required."); console.log(usageText); process.exit(1); } let appId = runAz([ "ad", "app", "list", "--display-name", appName, "--query", "[0].appId", "-o", "tsv", ]).stdout; let userConfirmation = ""; if (appId) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); userConfirmation = await new Promise((resolve) => { rl.question( `Application '${appName}' already exists. Update it? [y/N]: `, (answer) => { rl.close(); resolve(answer.trim()); }, ); }); if (!/^(yes|y)$/i.test(userConfirmation)) { console.log("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.error( `Error: Failed to grant admin consent for '${appName}' (${appId}).`, ); process.exit(1); } 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); } if (userConfirmation) { console.log(`Updated application '${appName}'`); } else { console.log(`Created application '${appName}'`); } console.log(`appId: ${appId}`); console.log(`export const config = { "appName": "${appName}", "tenantId": "${tenantId}", "clientId": "${appId}" };`); } main().catch((err) => { console.error(`Error: ${err.message}`); process.exit(1); });