# Directory Structure
```
├── .gitignore
├── LICENSE
├── NOTES.md
├── package.json
├── README.md
├── screenshot.png
├── scripts
│ └── create-service-account.js
├── src
│ ├── auth.ts
│ ├── config.ts
│ ├── server.ts
│ └── tools
│ ├── common.ts
│ ├── get-accounts.ts
│ ├── get-folder-contents.ts
│ ├── get-issue-comments.ts
│ ├── get-issue-root-causes.ts
│ ├── get-issue-types.ts
│ ├── get-issues.ts
│ ├── get-item-versions.ts
│ ├── get-projects.ts
│ └── index.ts
├── tsconfig.json
└── yarn.lock
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | build
3 | .env
4 | *.log
5 | .DS_Store
6 | Thumbs.db
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | > IMPORTANT: This project has been moved to https://github.com/autodesk-platform-services/aps-mcp-server-nodejs.
2 |
3 | ---
4 |
5 | <br/>
6 | <br/>
7 | <br/>
8 | <br/>
9 | <br/>
10 | <br/>
11 | <br/>
12 | <br/>
13 |
14 | # aps-mcp-server
15 |
16 | Experimental [Model Context Protocol](https://modelcontextprotocol.io) server build with Node.js, providing access to [Autodesk Platform Services](https://aps.autodesk.com) API, with fine-grained access control using the new _Secure Service Accounts_ feature.
17 |
18 | 
19 |
20 | [YouTube Video](https://youtu.be/6DRSR9HlIds)
21 |
22 | ## Development
23 |
24 | ### Prerequisites
25 |
26 | - [Node.js](https://nodejs.org)
27 | - [APS app credentials](https://aps.autodesk.com/en/docs/oauth/v2/tutorials/create-app) (must be a _Server-to-Server_ application type)
28 | - [Provisioned access to ACC or BIM360](https://get-started.aps.autodesk.com/#provision-access-in-other-products)
29 |
30 | ### Setup
31 |
32 | #### Server
33 |
34 | - Clone this repository
35 | - Install dependencies: `yarn install`
36 | - Build the TypeScript code: `yarn run build`
37 | - Create a _.env_ file in the root folder of this project, and add your APS credentials:
38 | - `APS_CLIENT_ID` - your APS application client ID
39 | - `APS_CLIENT_SECRET` - your APS application client secret
40 | - Create a new service account: `npx create-service-account <username> <first name> <last name>`, for example, `npx create-service-account ssa-test-user John Doe`
41 | - This script will output a bunch of environment variables with information about the new account:
42 | - `APS_SA_ID` - your service account ID
43 | - `APS_SA_EMAIL` - your service account email
44 | - `APS_SA_KEY_ID` - your service account key ID
45 | - `APS_SA_PRIVATE_KEY` - your service account private key
46 | - Add these environment variables to your _.env_ file
47 |
48 | #### Autodesk Construction Cloud
49 |
50 | - Register your APS application client ID as a custom integration
51 | - Invite the service account email as a new member to your ACC project(s)
52 |
53 | ### Use with Inspector
54 |
55 | - Run the [Model Context Protocol Inspector](https://modelcontextprotocol.io/docs/tools/inspector): `yarn run inspect`
56 | - Open http://localhost:5173
57 | - Hit `Connect` to start this MCP server and connect to it
58 |
59 | ### Use with Claude Desktop
60 |
61 | - Make sure you have [Claude Desktop](https://claude.ai/download) installed
62 | - Create a Claude Desktop config file if you don't have one yet:
63 | - On macOS: _~/Library/Application Support/Claude/claude\_desktop\_config.json_
64 | - On Windows: _%APPDATA%\Claude\claude\_desktop\_config.json_
65 | - Add this MCP server to the config, using the absolute path of the _build/server.js_ file on your system, for example:
66 | ```json
67 | {
68 | "mcpServers": {
69 | "autodesk-platform-services": {
70 | "command": "node",
71 | "args": [
72 | "/absolute/path/to/aps-mcp-server/build/server.js"
73 | ]
74 | }
75 | }
76 | }
77 | ```
78 | - Open Claude Desktop, and try some of the following test prompt:
79 | - What ACC projects do I have access to?
80 | - Give me a visual dashboard of all issues in project XYZ
81 |
82 | > For more details on how to add MCP servers to Claude Desktop, see the [official documentation](https://modelcontextprotocol.io/quickstart/user).
83 |
84 | ### Use with Visual Studio Code & Copilot
85 |
86 | - Make sure you have [enabled MCP servers in Visual Studio Code](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_enable-mcp-support-in-vs-code)
87 | - Create _.vscode/mcp.json_ file in your workspace, and add the following JSON to it:
88 |
89 | ```json
90 | {
91 | "servers": {
92 | "Autodesk Platform Services": {
93 | "type": "stdio",
94 | "command": "node",
95 | "args": [
96 | "/absolute/path/to/aps-mcp-server/build/server.js"
97 | ]
98 | }
99 | }
100 | }
101 | ```
102 |
103 | > For more details on how to add MCP servers to Visual Studio Code, see the [documentation](https://code.visualstudio.com/docs/copilot/chat/mcp-servers)
104 |
105 | ### Use with Cursor
106 |
107 | - Create _.cursor/mcp.json_ file in your workspace, and add the following JSON to it:
108 |
109 | ```json
110 | {
111 | "mcpServers": {
112 | "Autodesk Platform Services": {
113 | "command": "node",
114 | "args": [
115 | "/Users/brozp/Code/Temp/aps-mcp-server-node/build/server.js"
116 | ]
117 | }
118 | }
119 | }
120 | ```
121 |
122 | > For more details on how to add MCP servers to Cursor, see the [documentation](https://docs.cursor.com/context/model-context-protocol)
123 |
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": [
14 | "src/**/*"
15 | ],
16 | "exclude": [
17 | "node_modules"
18 | ]
19 | }
```
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | export { getAccounts } from "./get-accounts.js";
2 | export { getProjects } from "./get-projects.js";
3 | export { getFolderContents } from "./get-folder-contents.js";
4 | export { getItemVersions } from "./get-item-versions.js";
5 | export { getIssues } from "./get-issues.js";
6 | export { getIssueTypes } from "./get-issue-types.js";
7 | export { getIssueRootCauses } from "./get-issue-root-causes.js";
8 | export { getIssueComments } from "./get-issue-comments.js";
```
--------------------------------------------------------------------------------
/NOTES.md:
--------------------------------------------------------------------------------
```markdown
1 | # TODO
2 |
3 | ## Authentication
4 |
5 | Currently there's a [draft proposal](https://spec.modelcontextprotocol.io/specification/draft/basic/authorization/#22-basic-oauth-21-authorization) for adding OAuth 2.0 to Model Context Protocol. For now we'll use Secure Service Accounts.
6 |
7 | ## Fetch
8 |
9 | For some reason, Claude Desktop is not able to use this server when it uses the built-in [fetch](https://nodejs.org/dist/latest-v22.x/docs/api/globals.html#fetch) method, perhaps because it's using an older version of Node.js. For now we're using the [node-fetch](https://www.npmjs.com/package/node-fetch) NPM module.
```
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
```typescript
1 | import path from "node:path";
2 | import url from "node:url";
3 | import dotenv from "dotenv";
4 |
5 | const __filename = url.fileURLToPath(import.meta.url);
6 | const __dirname = path.dirname(__filename);
7 | dotenv.config({ path: path.resolve(__dirname, "..", ".env") });
8 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID } = process.env;
9 | const APS_SA_PRIVATE_KEY = process.env.APS_SA_PRIVATE_KEY ? Buffer.from(process.env.APS_SA_PRIVATE_KEY, "base64").toString("utf-8") : undefined;
10 |
11 | export {
12 | APS_CLIENT_ID,
13 | APS_CLIENT_SECRET,
14 | APS_SA_ID,
15 | APS_SA_EMAIL,
16 | APS_SA_KEY_ID,
17 | APS_SA_PRIVATE_KEY
18 | }
```
--------------------------------------------------------------------------------
/src/tools/get-accounts.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { DataManagementClient } from "@aps_sdk/data-management";
2 | import { getAccessToken } from "./common.js";
3 | import type { Tool } from "./common.js";
4 |
5 | const schema = {};
6 |
7 | export const getAccounts: Tool<typeof schema> = {
8 | title: "get-accounts",
9 | description: "List all available Autodesk Construction Cloud accounts",
10 | schema,
11 | callback: async () => {
12 | const accessToken = await getAccessToken(["data:read"]);
13 | const dataManagementClient = new DataManagementClient();
14 | const hubs = await dataManagementClient.getHubs({ accessToken });
15 | if (!hubs.data) {
16 | throw new Error("No accounts found");
17 | }
18 | return {
19 | content: hubs.data.map((hub) => ({
20 | type: "text",
21 | text: JSON.stringify({ id: hub.id, name: hub.attributes?.name })
22 | }))
23 | };
24 | }
25 | };
```
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import * as tools from "./tools/index.js";
4 | import { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY } from "./config.js";
5 |
6 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !APS_SA_ID || !APS_SA_EMAIL || !APS_SA_KEY_ID || !APS_SA_PRIVATE_KEY) {
7 | console.error("Missing one or more required environment variables: APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY");
8 | process.exit(1);
9 | }
10 |
11 | const server = new McpServer({ name: "autodesk-platform-services", version: "0.0.1" });
12 | for (const tool of Object.values(tools)) {
13 | server.tool(tool.title, tool.description, tool.schema, tool.callback);
14 | }
15 |
16 | try {
17 | await server.connect(new StdioServerTransport());
18 | } catch (err) {
19 | console.error("Server error:", err);
20 | }
```
--------------------------------------------------------------------------------
/src/tools/get-item-versions.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { DataManagementClient } from "@aps_sdk/data-management";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | projectId: z.string().nonempty(),
8 | itemId: z.string().nonempty()
9 | };
10 |
11 | export const getItemVersions: Tool<typeof schema> = {
12 | title: "get-item-versions",
13 | description: "List all versions of a document in Autodesk Construction Cloud",
14 | schema,
15 | callback: async ({ projectId, itemId }) => {
16 | // TODO: add pagination support
17 | const accessToken = await getAccessToken(["data:read"]);
18 | const dataManagementClient = new DataManagementClient();
19 | const versions = await dataManagementClient.getItemVersions(projectId, itemId, { accessToken });
20 | if (!versions.data) {
21 | throw new Error("No versions found");
22 | }
23 | return {
24 | content: versions.data.map((version) => ({ type: "text", text: JSON.stringify(version) }))
25 | };
26 | }
27 | };
```
--------------------------------------------------------------------------------
/src/tools/get-issues.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { IssuesClient } from "@aps_sdk/construction-issues";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | projectId: z.string().nonempty()
8 | };
9 |
10 | export const getIssues: Tool<typeof schema> = {
11 | title: "get-issues",
12 | description: "List all available projects in an Autodesk Construction Cloud account",
13 | schema,
14 | callback: async ({ projectId }) => {
15 | // TODO: add pagination support
16 | const accessToken = await getAccessToken(["data:read"]);
17 | const issuesClient = new IssuesClient();
18 | projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
19 | const issues = await issuesClient.getIssues(projectId, { accessToken });
20 | if (!issues.results) {
21 | throw new Error("No issues found");
22 | }
23 | return {
24 | content: issues.results.map((issue) => ({ type: "text", text: JSON.stringify(issue) }))
25 | };
26 | }
27 | };
```
--------------------------------------------------------------------------------
/src/tools/get-issue-types.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { IssuesClient } from "@aps_sdk/construction-issues";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | projectId: z.string().nonempty()
8 | };
9 |
10 | export const getIssueTypes: Tool<typeof schema> = {
11 | title: "get-issue-types",
12 | description: "List all issue types in an Autodesk Construction Cloud project",
13 | schema,
14 | callback: async ({ projectId }) => {
15 | // TODO: add pagination support
16 | const accessToken = await getAccessToken(["data:read"]);
17 | const issuesClient = new IssuesClient();
18 | projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
19 | const issueTypes = await issuesClient.getIssuesTypes(projectId, { accessToken });
20 | if (!issueTypes.results) {
21 | throw new Error("No issue types found");
22 | }
23 | return {
24 | content: issueTypes.results.map((issue) => ({ type: "text", text: JSON.stringify(issue) }))
25 | };
26 | }
27 | };
```
--------------------------------------------------------------------------------
/src/tools/get-projects.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { DataManagementClient } from "@aps_sdk/data-management";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | accountId: z.string().nonempty()
8 | };
9 |
10 | export const getProjects: Tool<typeof schema> = {
11 | title: "get-projects",
12 | description: "List all available projects in an Autodesk Construction Cloud account",
13 | schema,
14 | callback: async ({ accountId }) => {
15 | // TODO: add pagination support
16 | const accessToken = await getAccessToken(["data:read"]);
17 | const dataManagementClient = new DataManagementClient();
18 | const projects = await dataManagementClient.getHubProjects(accountId, { accessToken });
19 | if (!projects.data) {
20 | throw new Error("No projects found");
21 | }
22 | return {
23 | content: projects.data.map((project) => ({
24 | type: "text",
25 | text: JSON.stringify({ id: project.id, name: project.attributes?.name })
26 | }))
27 | };
28 | }
29 | };
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "aps-mcp-server",
3 | "version": "0.0.1",
4 | "type": "module",
5 | "private": true,
6 | "description": "Experimental [Model Context Protocol](https://modelcontextprotocol.io) server providing access to [Autodesk Platform Services](https://aps.autodesk.com) API.",
7 | "author": "Petr Broz <[email protected]>",
8 | "bin": {
9 | "aps-mcp-server": "./build/server.js",
10 | "create-service-account": "./scripts/create-service-account.js"
11 | },
12 | "scripts": {
13 | "build": "tsc",
14 | "inspect": "mcp-inspector node ./build/server.js"
15 | },
16 | "files": [
17 | "build"
18 | ],
19 | "dependencies": {
20 | "@aps_sdk/authentication": "^1.0.0",
21 | "@aps_sdk/construction-issues": "^1.1.0",
22 | "@aps_sdk/data-management": "^1.0.1",
23 | "@modelcontextprotocol/sdk": "^1.7.0",
24 | "dotenv": "^16.4.7",
25 | "jsonwebtoken": "^9.0.2",
26 | "node-fetch": "^3.3.2",
27 | "zod": "^3.24.2"
28 | },
29 | "devDependencies": {
30 | "@modelcontextprotocol/inspector": "^0.6.0",
31 | "@types/jsonwebtoken": "^9.0.9",
32 | "@types/node": "^22.13.10",
33 | "typescript": "^5.8.2"
34 | }
35 | }
36 |
```
--------------------------------------------------------------------------------
/src/tools/get-issue-comments.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { IssuesClient } from "@aps_sdk/construction-issues";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | projectId: z.string().nonempty(),
8 | issueId: z.string().nonempty()
9 | };
10 |
11 | export const getIssueComments: Tool<typeof schema> = {
12 | title: "get-issue-comments",
13 | description: "Retrieves a list of comments associated with an issue in Autodesk Construction Cloud.",
14 | schema,
15 | callback: async ({ projectId, issueId }) => {
16 | // TODO: add pagination support
17 | const accessToken = await getAccessToken(["data:read"]);
18 | const issuesClient = new IssuesClient();
19 | projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
20 | const comments = await issuesClient.getComments(projectId, issueId, { accessToken})
21 | if (!comments.results) {
22 | throw new Error("No comments found");
23 | }
24 | return {
25 | content: comments.results.map((comment) => ({ type: "text", text: JSON.stringify(comment) }))
26 | };
27 | }
28 | };
```
--------------------------------------------------------------------------------
/src/tools/get-issue-root-causes.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { IssuesClient } from "@aps_sdk/construction-issues";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | projectId: z.string().nonempty()
8 | };
9 |
10 | export const getIssueRootCauses: Tool<typeof schema> = {
11 | title: "get-issue-root-causes",
12 | description: "Retrieves a list of supported root cause categories and root causes that you can allocate to an issue in Autodesk Construction Cloud.",
13 | schema,
14 | callback: async ({ projectId }) => {
15 | // TODO: add pagination support
16 | const accessToken = await getAccessToken(["data:read"]);
17 | const issuesClient = new IssuesClient();
18 | projectId = projectId.replace("b.", ""); // the projectId should not contain the "b." prefix
19 | const rootCauses = await issuesClient.getRootCauseCategories(projectId, { accessToken });
20 | if (!rootCauses.results) {
21 | throw new Error("No root causes found");
22 | }
23 | return {
24 | content: rootCauses.results.map((rootCause) => ({ type: "text", text: JSON.stringify(rootCause) }))
25 | };
26 | }
27 | };
```
--------------------------------------------------------------------------------
/src/tools/common.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { ZodRawShape } from "zod";
2 | import { ToolCallback } from "@modelcontextprotocol/sdk/server/mcp.js";
3 | import { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY } from "../config.js";
4 | import { getServiceAccountAccessToken } from "../auth.js";
5 |
6 | export interface Tool<Args extends ZodRawShape> {
7 | title: string;
8 | description: string;
9 | schema: Args;
10 | callback: ToolCallback<Args>;
11 | }
12 |
13 | const credentialsCache = new Map<string, { accessToken: string, expiresAt: number }>();
14 |
15 | export async function getAccessToken(scopes: string[]): Promise<string> {
16 | const cacheKey = scopes.join("+");
17 | let credentials = credentialsCache.get(cacheKey);
18 | if (!credentials || credentials.expiresAt < Date.now()) {
19 | const { access_token, expires_in } = await getServiceAccountAccessToken(APS_CLIENT_ID!, APS_CLIENT_SECRET!, APS_SA_ID!, APS_SA_KEY_ID!, APS_SA_PRIVATE_KEY!, scopes);
20 | credentials = {
21 | accessToken: access_token,
22 | expiresAt: Date.now() + expires_in * 1000
23 | };
24 | credentialsCache.set(cacheKey, credentials);
25 | }
26 | return credentials.accessToken;
27 | }
```
--------------------------------------------------------------------------------
/src/tools/get-folder-contents.ts:
--------------------------------------------------------------------------------
```typescript
1 | import { z } from "zod";
2 | import { DataManagementClient } from "@aps_sdk/data-management";
3 | import { getAccessToken } from "./common.js";
4 | import type { Tool } from "./common.js";
5 |
6 | const schema = {
7 | accountId: z.string().nonempty(),
8 | projectId: z.string().nonempty(),
9 | folderId: z.string().optional()
10 | };
11 |
12 | export const getFolderContents: Tool<typeof schema> = {
13 | title: "get-folder-contents",
14 | description: "List contents of a project or a specific subfolder in Autodesk Construction Cloud",
15 | schema,
16 | callback: async ({ accountId, projectId, folderId }) => {
17 | // TODO: add pagination support
18 | const accessToken = await getAccessToken(["data:read"]);
19 | const dataManagementClient = new DataManagementClient();
20 | const contents = folderId
21 | ? await dataManagementClient.getFolderContents(projectId, folderId, { accessToken })
22 | : await dataManagementClient.getProjectTopFolders(accountId, projectId, { accessToken });
23 | if (!contents.data) {
24 | throw new Error("No contents found");
25 | }
26 | return {
27 | content: contents.data.map((item) => ({ type: "text", text: JSON.stringify(item) }))
28 | };
29 | }
30 | };
```
--------------------------------------------------------------------------------
/scripts/create-service-account.js:
--------------------------------------------------------------------------------
```javascript
1 | #!/usr/bin/env node
2 |
3 | import process from "node:process";
4 | import { getClientCredentialsAccessToken, createServiceAccount, createServiceAccountPrivateKey } from "../build/auth.js";
5 | import { APS_CLIENT_ID, APS_CLIENT_SECRET } from "../build/config.js";
6 |
7 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET) {
8 | console.error("Please set the APS_CLIENT_ID and APS_CLIENT_SECRET environment variables.");
9 | process.exit(1);
10 | }
11 | const [,, userName, firstName, lastName] = process.argv;
12 | if (!userName || !firstName || !lastName) {
13 | console.error("Usage: node create-service-account.js <userName> <firstName> <lastName>");
14 | console.error("Example: node create-service-account.js test-robot Rob Robot");
15 | process.exit(1);
16 | }
17 |
18 | try {
19 | const credentials = await getClientCredentialsAccessToken(APS_CLIENT_ID, APS_CLIENT_SECRET, ["application:service_account:write", "application:service_account_key:write"]);
20 | const { serviceAccountId, email } = await createServiceAccount(userName, firstName, lastName, credentials.access_token);
21 | const { kid, privateKey } = await createServiceAccountPrivateKey(serviceAccountId, credentials.access_token);
22 | console.log("Service account created successfully!");
23 | console.log("Invite the following user to your project:", email);
24 | console.log("Include the following environment variables to your application:");
25 | console.log(`APS_SA_ID="${serviceAccountId}"`);
26 | console.log(`APS_SA_EMAIL="${email}"`);
27 | console.log(`APS_SA_KEY_ID="${kid}"`);
28 | console.log(`APS_SA_PRIVATE_KEY="${Buffer.from(privateKey).toString("base64")}"`);
29 | } catch (err) {
30 | console.error(err);
31 | process.exit(1);
32 | }
```
--------------------------------------------------------------------------------
/src/auth.ts:
--------------------------------------------------------------------------------
```typescript
1 | import fetch from "node-fetch";
2 | import jwt from "jsonwebtoken";
3 |
4 | interface Credentials {
5 | access_token: string;
6 | token_type: string;
7 | expires_in: number;
8 | }
9 |
10 | /**
11 | * Generates an access token for APS using specific grant type.
12 | *
13 | * @param clientId The client ID provided by Autodesk.
14 | * @param clientSecret The client secret provided by Autodesk.
15 | * @param grantType The grant type for the access token.
16 | * @param scopes An array of scopes for which the token is requested.
17 | * @param assertion The JWT assertion for the access token.
18 | * @returns A promise that resolves to the access token response object.
19 | * @throws If the request for the access token fails.
20 | */
21 | async function getAccessToken(clientId: string, clientSecret: string, grantType: string, scopes: string[], assertion?: string): Promise<Credentials> {
22 | const headers = {
23 | "Accept": "application/json",
24 | "Authorization": `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
25 | "Content-Type": "application/x-www-form-urlencoded"
26 | };
27 | const body = new URLSearchParams({
28 | "grant_type": grantType,
29 | "scope": scopes.join(" ")
30 | });
31 | if (assertion) {
32 | body.append("assertion", assertion);
33 | }
34 | const response = await fetch("https://developer.api.autodesk.com/authentication/v2/token", { method: "POST", headers, body });
35 | if (!response.ok) {
36 | throw new Error(`Could not generate access token: ${await response.text()}`);
37 | }
38 | const credentials = await response.json() as Credentials;
39 | return credentials;
40 | }
41 |
42 | /**
43 | * Creates a JWT assertion for OAuth 2.0 authentication.
44 | *
45 | * @param clientId The client ID of the application.
46 | * @param serviceAccountId The service account ID.
47 | * @param serviceAccountKeyId The key ID of the service account.
48 | * @param serviceAccountPrivateKey The private key of the service account.
49 | * @param scopes The scopes for the access token.
50 | * @returns The signed JWT assertion.
51 | */
52 | function createAssertion(clientId: string, serviceAccountId: string, serviceAccountKeyId: string, serviceAccountPrivateKey: string, scopes: string[]) {
53 | // TODO: validate inputs
54 | const payload = {
55 | iss: clientId,
56 | sub: serviceAccountId,
57 | aud: "https://developer.api.autodesk.com/authentication/v2/token",
58 | exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
59 | scope: scopes
60 | };
61 | const options = {
62 | algorithm: "RS256" as jwt.Algorithm,
63 | header: { alg: "RS256", kid: serviceAccountKeyId }
64 | };
65 | return jwt.sign(payload, serviceAccountPrivateKey, options);
66 | }
67 |
68 | /**
69 | * Generates an access token for APS using client credentials ("two-legged") flow.
70 | *
71 | * @param clientId The client ID provided by Autodesk.
72 | * @param clientSecret The client secret provided by Autodesk.
73 | * @param scopes An array of scopes for which the token is requested.
74 | * @returns A promise that resolves to the access token response object.
75 | * @throws If the request for the access token fails.
76 | */
77 | export async function getClientCredentialsAccessToken(clientId: string, clientSecret: string, scopes: string[]) {
78 | return getAccessToken(clientId, clientSecret, "client_credentials", scopes);
79 | }
80 |
81 | /**
82 | * Retrieves an access token for a service account using client credentials and JWT assertion.
83 | *
84 | * @param clientId The client ID for the OAuth application.
85 | * @param clientSecret The client secret for the OAuth application.
86 | * @param serviceAccountId The ID of the service account.
87 | * @param serviceAccountKeyId The key ID of the service account.
88 | * @param serviceAccountPrivateKey The private key of the service account.
89 | * @param scopes An array of scopes for the access token.
90 | * @returns A promise that resolves to the access token response object.
91 | * @throws If the access token could not be retrieved.
92 | */
93 | export async function getServiceAccountAccessToken(clientId: string, clientSecret: string, serviceAccountId: string, serviceAccountKeyId: string, serviceAccountPrivateKey: string, scopes: string[]) {
94 | const assertion = createAssertion(clientId, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes);
95 | return getAccessToken(clientId, clientSecret, "urn:ietf:params:oauth:grant-type:jwt-bearer", scopes, assertion);
96 | }
97 |
98 | /**
99 | * Creates a new service account with the given name.
100 | *
101 | * @param name The name of the service account to create (must be between 5 and 64 characters long).
102 | * @param firstName The first name of the service account.
103 | * @param lastName The last name of the service account.
104 | * @param accessToken The access token for authentication.
105 | * @returns A promise that resolves to the created service account response.
106 | * @throws If the request to create the service account fails.
107 | */
108 | export async function createServiceAccount(name: string, firstName: string, lastName: string, accessToken: string) {
109 | const headers = {
110 | "Accept": "application/json",
111 | "Authorization": `Bearer ${accessToken}`,
112 | "Content-Type": "application/json"
113 | };
114 | const body = JSON.stringify({ name, firstName, lastName });
115 | const response = await fetch("https://developer.api.autodesk.com/authentication/v2/service-accounts", { method: "POST", headers, body });
116 | if (!response.ok) {
117 | throw new Error(`Could not create service account: ${await response.text()}`);
118 | }
119 | return response.json();
120 | }
121 |
122 | /**
123 | * Creates a private key for a given service account.
124 | *
125 | * @param serviceAccountId - The ID of the service account for which to create a private key.
126 | * @param accessToken - The access token used for authorization.
127 | * @returns A promise that resolves to the private key details.
128 | * @throws If the request to create the private key fails.
129 | */
130 | export async function createServiceAccountPrivateKey(serviceAccountId: string, accessToken: string) {
131 | const headers = {
132 | "Accept": "application/json",
133 | "Authorization": `Bearer ${accessToken}`
134 | };
135 | const response = await fetch(`https://developer.api.autodesk.com/authentication/v2/service-accounts/${serviceAccountId}/keys`, { method: "POST", headers });
136 | if (!response.ok) {
137 | throw new Error(`Could not create service account private key: ${await response.text()}`);
138 | }
139 | return response.json();
140 | }
```