Migrated to TypeScript.

This commit is contained in:
2026-03-05 21:29:58 +01:00
parent 5aacde4f67
commit cb41e7dec1
26 changed files with 659 additions and 430 deletions

View File

@@ -1,51 +1,77 @@
// SPDX-License-Identifier: MIT
/**
* Get an Azure application by its display name.
*
* @param { Object } client
* @param { string } displayName
* @returns { Promise<Object|null> }
*/
export async function getApp(client, displayName) {
type GraphObject = Record<string, unknown>;
type GraphResult<T = GraphObject> = {
value?: T[];
};
type AppQueryOptions = {
displayName?: string;
appId?: string;
};
type RequiredResourceAccessItem = {
type?: string;
id?: string;
};
type RequiredResourceAccess = {
resourceAppId?: string;
resourceAccess?: RequiredResourceAccessItem[];
};
type GraphPermission = {
id?: string;
value?: string;
displayName?: string;
adminConsentDisplayName?: string;
userConsentDisplayName?: string;
isEnabled?: boolean;
};
type ServicePrincipal = {
id?: string;
appId?: string;
displayName?: string;
oauth2PermissionScopes?: GraphPermission[];
appRoles?: GraphPermission[];
};
type ResourcePermissionsOptions = {
appId?: string;
displayName?: string;
};
export async function getApp(client: any, displayName: string): Promise<GraphObject | null> {
const result = await client
.api("/applications")
.filter(`displayName eq '${displayName}'`)
.get();
.get() as GraphResult;
// Return the first application found or null if none exists
return result.value.length > 0 ? result.value[0] : null;
return Array.isArray(result.value) && result.value.length > 0 ? result.value[0] : null;
}
export async function createApp(client, displayName) {
export async function createApp(client: any, displayName: string): Promise<GraphObject> {
const app = await client.api("/applications").post({
displayName,
});
}) as GraphObject;
if (!app || !app.appId) {
if (!app || typeof app.appId !== "string") {
throw new Error("Failed to create application");
}
return app;
}
export async function deleteApp(client, appObjectId) {
export async function deleteApp(client: any, appObjectId: string): Promise<void> {
await client.api(`/applications/${appObjectId}`).delete();
}
/**
* List Azure applications, optionally filtered by display name and/or app ID.
*
* @param { Object } client
* @param { Object } [options]
* @param { string } [options.displayName]
* @param { string } [options.appId]
* @returns { Promise<Array> }
*/
export async function listApps(client, options = {}) {
export async function listApps(client: any, options: AppQueryOptions = {}): Promise<GraphObject[]> {
const { displayName, appId } = options;
let request = client.api("/applications");
const filters = [];
const filters: string[] = [];
if (displayName) {
filters.push(`displayName eq '${displayName}'`);
@@ -58,18 +84,11 @@ export async function listApps(client, options = {}) {
request = request.filter(filters.join(" and "));
}
const result = await request.get();
const result = await request.get() as GraphResult;
return Array.isArray(result?.value) ? result.value : [];
}
/**
* List required resource access configuration for an application by appId.
*
* @param { Object } client
* @param { string } appId
* @returns { Promise<Array> }
*/
export async function listAppPermissions(client, appId) {
export async function listAppPermissions(client: any, appId: string): Promise<RequiredResourceAccess[]> {
if (!appId) {
throw new Error("appId is required");
}
@@ -78,7 +97,7 @@ export async function listAppPermissions(client, appId) {
.api("/applications")
.filter(`appId eq '${appId}'`)
.select("id,appId,displayName,requiredResourceAccess")
.get();
.get() as GraphResult<GraphObject>;
const app = Array.isArray(result?.value) && result.value.length > 0
? result.value[0]
@@ -88,19 +107,13 @@ export async function listAppPermissions(client, appId) {
return [];
}
return Array.isArray(app.requiredResourceAccess)
? app.requiredResourceAccess
const requiredResourceAccess = app.requiredResourceAccess;
return Array.isArray(requiredResourceAccess)
? requiredResourceAccess as RequiredResourceAccess[]
: [];
}
/**
* List required resource access in a resolved, human-readable form.
*
* @param { Object } client
* @param { string } appId
* @returns { Promise<Array> }
*/
export async function listAppPermissionsResolved(client, appId) {
export async function listAppPermissionsResolved(client: any, appId: string): Promise<Array<Record<string, unknown>>> {
const requiredResourceAccess = await listAppPermissions(client, appId);
if (!Array.isArray(requiredResourceAccess) || requiredResourceAccess.length === 0) {
return [];
@@ -109,7 +122,7 @@ export async function listAppPermissionsResolved(client, appId) {
const resourceAppIds = [...new Set(
requiredResourceAccess
.map((entry) => entry?.resourceAppId)
.filter(Boolean),
.filter((value): value is string => typeof value === "string" && value.length > 0),
)];
const resourceDefinitions = await Promise.all(resourceAppIds.map(async (resourceAppId) => {
@@ -117,17 +130,21 @@ export async function listAppPermissionsResolved(client, appId) {
.api("/servicePrincipals")
.filter(`appId eq '${resourceAppId}'`)
.select("appId,displayName,oauth2PermissionScopes,appRoles")
.get();
.get() as GraphResult<ServicePrincipal>;
const sp = Array.isArray(result?.value) && result.value.length > 0
? result.value[0]
: null;
const scopesById = new Map(
(sp?.oauth2PermissionScopes ?? []).map((scope) => [scope.id, scope]),
(sp?.oauth2PermissionScopes ?? [])
.filter((scope) => typeof scope.id === "string")
.map((scope) => [scope.id as string, scope]),
);
const rolesById = new Map(
(sp?.appRoles ?? []).map((role) => [role.id, role]),
(sp?.appRoles ?? [])
.filter((role) => typeof role.id === "string")
.map((role) => [role.id as string, role]),
);
return {
@@ -142,9 +159,10 @@ export async function listAppPermissionsResolved(client, appId) {
resourceDefinitions.map((entry) => [entry.resourceAppId, entry]),
);
const rows = [];
const rows: Array<Record<string, unknown>> = [];
for (const resourceEntry of requiredResourceAccess) {
const resourceMeta = byResourceAppId.get(resourceEntry.resourceAppId);
const resourceAppId = resourceEntry.resourceAppId ?? "";
const resourceMeta = byResourceAppId.get(resourceAppId);
const resourceAccessItems = Array.isArray(resourceEntry?.resourceAccess)
? resourceEntry.resourceAccess
: [];
@@ -153,8 +171,8 @@ export async function listAppPermissionsResolved(client, appId) {
const permissionType = item?.type ?? null;
const permissionId = item?.id ?? null;
const resolved = permissionType === "Scope"
? resourceMeta?.scopesById.get(permissionId)
: resourceMeta?.rolesById.get(permissionId);
? resourceMeta?.scopesById.get(permissionId ?? "")
: resourceMeta?.rolesById.get(permissionId ?? "");
rows.push({
resourceAppId: resourceEntry.resourceAppId ?? null,
@@ -174,14 +192,7 @@ export async function listAppPermissionsResolved(client, appId) {
return rows;
}
/**
* List delegated OAuth2 permission grants for an application by appId.
*
* @param { Object } client
* @param { string } appId
* @returns { Promise<Array> }
*/
export async function listAppGrants(client, appId) {
export async function listAppGrants(client: any, appId: string): Promise<GraphObject[]> {
if (!appId) {
throw new Error("appId is required");
}
@@ -190,7 +201,7 @@ export async function listAppGrants(client, appId) {
.api("/servicePrincipals")
.filter(`appId eq '${appId}'`)
.select("id,appId,displayName")
.get();
.get() as GraphResult<ServicePrincipal>;
const servicePrincipal = Array.isArray(spResult?.value) && spResult.value.length > 0
? spResult.value[0]
@@ -203,21 +214,12 @@ export async function listAppGrants(client, appId) {
const grantsResult = await client
.api("/oauth2PermissionGrants")
.filter(`clientId eq '${servicePrincipal.id}'`)
.get();
.get() as GraphResult;
return Array.isArray(grantsResult?.value) ? grantsResult.value : [];
}
/**
* List available delegated scopes and app roles for a resource app.
*
* @param { Object } client
* @param { Object } options
* @param { string } [options.appId]
* @param { string } [options.displayName]
* @returns { Promise<Array> }
*/
export async function listResourcePermissions(client, options = {}) {
export async function listResourcePermissions(client: any, options: ResourcePermissionsOptions = {}): Promise<Array<Record<string, unknown>>> {
const { appId, displayName } = options;
if (!appId && !displayName) {
throw new Error("appId or displayName is required");
@@ -233,9 +235,9 @@ export async function listResourcePermissions(client, options = {}) {
request = request.filter(`displayName eq '${displayName}'`);
}
const result = await request.get();
const result = await request.get() as GraphResult<ServicePrincipal>;
const servicePrincipals = Array.isArray(result?.value) ? result.value : [];
const rows = [];
const rows: Array<Record<string, unknown>> = [];
for (const sp of servicePrincipals) {
for (const scope of sp?.oauth2PermissionScopes ?? []) {

View File

@@ -1,29 +0,0 @@
// SPDX-License-Identifier: MIT
import { Client } from "@microsoft/microsoft-graph-client";
import { acquireResourceTokenFromLogin } from "../azure/index.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 { Promise<{ graphApiToken: Object, client: Object }> } An object containing the Graph API token and client
*/
export async function getGraphClient({ tenantId, clientId }) {
const graphApiToken = await acquireResourceTokenFromLogin({
tenantId,
clientId,
resource: "graph",
});
const client = Client.init({
authProvider: (done) => {
done(null, graphApiToken.accessToken);
},
});
return { graphApiToken, client };
}

30
src/graph/auth.ts Normal file
View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
import { Client } from "@microsoft/microsoft-graph-client";
import { acquireResourceTokenFromLogin } from "../azure/index.ts";
type GraphClientOptions = {
tenantId?: string;
clientId?: string;
};
type GraphApiToken = {
accessToken: string;
[key: string]: unknown;
};
export async function getGraphClient({ tenantId, clientId }: GraphClientOptions): Promise<{ graphApiToken: GraphApiToken; client: any }> {
const graphApiToken = await acquireResourceTokenFromLogin({
tenantId,
clientId,
resource: "graph",
}) as GraphApiToken;
const client = Client.init({
authProvider: (done) => {
done(null, graphApiToken.accessToken);
},
});
return { graphApiToken, client };
}

View File

@@ -1 +0,0 @@
//

View File

@@ -1,5 +0,0 @@
// SPDX-License-Identifier: MIT
export * from "./auth.js";
export * from "./app.js";
export * from "./sp.js";

5
src/graph/index.ts Normal file
View File

@@ -0,0 +1,5 @@
// SPDX-License-Identifier: MIT
export * from "./auth.ts";
export * from "./app.ts";
export * from "./sp.ts";

View File

@@ -1,27 +0,0 @@
// SPDX-License-Identifier: MIT
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();
}

30
src/graph/sp.ts Normal file
View File

@@ -0,0 +1,30 @@
// SPDX-License-Identifier: MIT
type GraphResult<T = Record<string, unknown>> = {
value?: T[];
};
export async function getServicePrincipal(client: any, appId: string): Promise<Record<string, unknown> | null> {
const result = await client
.api("/servicePrincipals")
.filter(`appId eq '${appId}'`)
.get() as GraphResult;
return Array.isArray(result.value) && result.value.length > 0 ? result.value[0] : null;
}
export async function createSp(client: any, appId: string): Promise<Record<string, unknown>> {
const sp = await client.api("/servicePrincipals").post({
appId,
}) as Record<string, unknown>;
if (!sp || typeof sp.id !== "string") {
throw new Error("Failed to create service principal");
}
return sp;
}
export async function deleteSp(client: any, spId: string): Promise<void> {
await client.api(`/servicePrincipals/${spId}`).delete();
}