10 | router.get('/token', cors(), async (req, res, next) => {
11 | try {
12 | if (!req.query.share) {
13 | throw new Error(`Missing 'share' query parameter.`);
14 | }
15 | const [ownerId, shareId] = decryptShareCode(req.query.share);
16 | const shares = await listShares(ownerId);
17 | const share = shares.find(s => s.id === shareId);
18 | if (!share) {
19 | res.status(403).end();
20 | } else {
21 | const payload = {
22 | grant_type: 'client_credentials',
23 | scope: 'data:read:' + Buffer.from(share.urn, 'base64').toString()
24 | };
25 | const headers = {
26 | 'Authorization': 'Basic ' + Buffer.from(APS_CLIENT_ID + ':' + APS_CLIENT_SECRET).toString('base64'),
27 | 'Content-Type': 'application/x-www-form-urlencoded'
28 | };
29 | const { data } = await axios.post('https://developer.api.autodesk.com/authentication/v2/token', payload, { headers });
30 | data.urn = share.urn;
31 | res.json(data);
32 | }
33 | } catch (err) {
34 | next(err);
35 | }
36 | });
37 |
38 | module.exports = router;
39 |
--------------------------------------------------------------------------------
/services/aps-shares-app/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/services/aps-shares-app/screenshot.png
--------------------------------------------------------------------------------
/services/aps-shares-app/shares.js:
--------------------------------------------------------------------------------
1 | const crypto = require('crypto');
2 | const axios = require('axios').default;
3 | const { SdkManagerBuilder } = require('@aps_sdk/autodesk-sdkmanager');
4 | const { AuthenticationClient, Scopes } = require('@aps_sdk/authentication');
5 | const { OssClient, CreateBucketXAdsRegionEnum, CreateBucketsPayloadPolicyKeyEnum, CreateSignedResourceAccessEnum } = require('@aps_sdk/oss');
6 | const { APS_CLIENT_ID, APS_CLIENT_SECRET , APS_BUCKET_KEY, SERVER_SESSION_SECRET } = require('./config.js');
7 |
8 | const sdkManager = SdkManagerBuilder.create().build();
9 | const authenticationClient = new AuthenticationClient(sdkManager);
10 | const ossClient = new OssClient(sdkManager);
11 |
12 | let _credentials = null;
13 | async function getAccessToken() {
14 | if (!_credentials || _credentials.expires_at < Date.now()) {
15 | _credentials = await authenticationClient.getTwoLeggedToken(APS_CLIENT_ID, APS_CLIENT_SECRET, [Scopes.BucketCreate, Scopes.BucketRead, Scopes.DataCreate, Scopes.DataWrite, Scopes.DataRead]);
16 | _credentials.expires_at = Date.now() + _credentials.expires_in * 1000;
17 | }
18 | return _credentials.access_token;
19 | }
20 |
21 | async function ensureBucketExists(bucketKey) {
22 | const token = await getAccessToken();
23 | try {
24 | await ossClient.getBucketDetails(token, bucketKey);
25 | } catch (err) {
26 | if (err.axiosError.response.status === 404) {
27 | await ossClient.createBucket(token, CreateBucketXAdsRegionEnum.Us, {
28 | bucketKey,
29 | policyKey: CreateBucketsPayloadPolicyKeyEnum.Persistent
30 | });
31 | } else {
32 | throw err;
33 | }
34 | }
35 | }
36 |
37 | async function listShares(ownerId) {
38 | await ensureBucketExists(APS_BUCKET_KEY);
39 | const token = await getAccessToken();
40 | try {
41 | const { signedUrl } = await ossClient.createSignedResource(token, APS_BUCKET_KEY, ownerId, { access: CreateSignedResourceAccessEnum.Read });
42 | const { data: shares } = await axios.get(signedUrl);
43 | return shares;
44 | } catch (err) {
45 | if (err.axiosError.response.status === 404) {
46 | return [];
47 | } else {
48 | throw err;
49 | }
50 | }
51 | }
52 |
53 | async function updateShares(ownerId, func) {
54 | let shares = await listShares(ownerId);
55 | shares = func(shares);
56 | const token = await getAccessToken();
57 | const { signedUrl } = await ossClient.createSignedResource(token, APS_BUCKET_KEY, ownerId, { access: CreateSignedResourceAccessEnum.Write });
58 | const { data } = await axios.put(signedUrl, JSON.stringify(shares));
59 | return data;
60 | }
61 |
62 | async function createShare(ownerId, urn, description) {
63 | const id = crypto.randomUUID();
64 | const code = encryptShareCode(ownerId, id);
65 | const share = { id, ownerId, code, created: new Date(), urn, description };
66 | await updateShares(ownerId, shares => [...shares, share]);
67 | return share;
68 | }
69 |
70 | async function deleteShare(ownerId, shareId) {
71 | await updateShares(ownerId, shares => shares.filter(share => share.id !== shareId));
72 | }
73 |
74 | function encryptShareCode(ownerId, shareId) {
75 | const cipher = crypto.createCipher('aes-128-ecb', SERVER_SESSION_SECRET, {});
76 | return cipher.update(`${ownerId}/${shareId}`, 'utf8', 'hex') + cipher.final('hex');
77 | }
78 |
79 | function decryptShareCode(code) {
80 | const decipher = crypto.createDecipher('aes-128-ecb', SERVER_SESSION_SECRET);
81 | const decrypted = decipher.update(code, 'hex', 'utf8') + decipher.final('utf8');
82 | if (!decrypted.match(/^[a-zA-Z0-9]+\/[0-9a-fA-F\-]+$/)) {
83 | throw new Error('Invalid share code.');
84 | }
85 | return decrypted.split('/');
86 | }
87 |
88 | module.exports = {
89 | listShares,
90 | createShare,
91 | deleteShare,
92 | encryptShareCode,
93 | decryptShareCode
94 | };
95 |
--------------------------------------------------------------------------------
/services/aps-shares-app/views/error.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Autodesk Platform Services: Shares App
8 |
10 |
11 |
12 |
13 |
28 |
29 |
30 | Error
31 |
32 | <%= error.message %>
33 |
34 |
35 |
36 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/services/aps-shares-app/views/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Autodesk Platform Services: Shares App
8 |
10 |
11 |
12 |
13 |
14 |
15 |
30 |
31 |
32 |
33 | <% if (user) { %>
34 |
35 |
38 |
39 | <% } %>
40 |
41 | <% if (shares) { %>
42 | Shares
43 |
44 |
45 |
46 | ID
47 | Created
48 | Description
49 | Actions
50 |
51 |
52 |
53 | <% for (const share of shares) { %>
54 |
55 | <%= share.id %>
56 | <%= share.created %>
57 | <%= share.description %>
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | <% } %>
71 |
72 |
73 | <% } %>
74 |
75 |
76 |
77 |
118 |
119 |
120 |
121 |
122 |
123 | APS Shares App
124 |
125 |
126 |
127 |
128 |
129 |
130 |
163 |
166 |
167 |
168 |
--------------------------------------------------------------------------------
/services/ssa-auth-app/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env*
3 | *.log
4 | .DS_Store
5 | Thumbs.db
6 |
--------------------------------------------------------------------------------
/services/ssa-auth-app/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Autodesk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/services/ssa-auth-app/README.md:
--------------------------------------------------------------------------------
1 | # SSA Auth App
2 |
3 | Simple web application providing authentication for accessing project and design data in [Autodesk Construction Cloud](https://construction.autodesk.com) using _Secure Service Accounts_. One of the use cases of this application is the generation of access tokens for [APS Viewer](https://aps.autodesk.com/en/docs/viewer/v7/developers_guide/overview/) hosted within Power BI reports.
4 |
5 | ## Development
6 |
7 | ### Prerequisites
8 |
9 | - [APS app credentials](https://forge.autodesk.com/en/docs/oauth/v2/tutorials/create-app)
10 | - [Provision access to ACC or BIM360](https://tutorials.autodesk.io/#provision-access-in-other-products)
11 | - [Node.js](https://nodejs.org) (ideally the _Long Term Support_ version)
12 | - Terminal (for example, [Windows Command Prompt](https://en.wikipedia.org/wiki/Cmd.exe) or [macOS Terminal](https://support.apple.com/guide/terminal/welcome/mac))
13 |
14 | ### Running locally
15 |
16 | 1. Clone this repository
17 | 2. Install dependencies: `npm install`
18 | 3. Create a _.env_ file in the root folder of this project, and add your APS credentials:
19 |
20 | ```
21 | APS_CLIENT_ID="your client id"
22 | APS_CLIENT_SECRET="your client secret"
23 | ```
24 |
25 | 4. Create a new service account: `npx create-service-account `
26 | - This script will output an email of the newly created service account, and a bunch of environment variables
27 | 5. Add the service account email as a new member to your ACC projects as needed
28 | 6. Add or overwrite the new environment variables in your _.env_ file
29 |
30 | ```
31 | APS_SA_ID="your service account id"
32 | APS_SA_EMAIL="your service account email"
33 | APS_SA_KEY_ID="your service account key id"
34 | APS_SA_PRIVATE_KEY="your service account private key"
35 | ```
36 |
37 | 7. Start the server: `npm start`
38 | 8. Open http://localhost:3000, and follow the instructions there
39 |
40 | ## Troubleshooting
41 |
42 | Please contact us via https://aps.autodesk.com/get-help.
43 |
44 | ## License
45 |
46 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details.
47 |
--------------------------------------------------------------------------------
/services/ssa-auth-app/lib/auth.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | async function _post(endpoint, headers, body) {
4 | const response = await fetch("https://developer.api.autodesk.com" + endpoint, { method: "POST", headers, body });
5 | if (!response.ok) {
6 | throw new Error(`POST ${endpoint} error: ${response.status} ${response.statusText}\n${await response.text()}`);
7 | }
8 | return response.json();
9 | }
10 |
11 | /**
12 | * Generates an access token for APS using specific grant type.
13 | *
14 | * @param {string} clientId - The client ID provided by Autodesk.
15 | * @param {string} clientSecret - The client secret provided by Autodesk.
16 | * @param {string} grantType - The grant type for the access token.
17 | * @param {string[]} scopes - An array of scopes for which the token is requested.
18 | * @param {string} [assertion] - The JWT assertion for the access token.
19 | * @returns {Promise<{ access_token: string; token_type: string; expires_in: number; }>} A promise that resolves to the access token response object.
20 | * @throws {Error} If the request for the access token fails.
21 | */
22 | async function getAccessToken(clientId, clientSecret, grantType, scopes, assertion) {
23 | const headers = {
24 | "Accept": "application/json",
25 | "Authorization": `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
26 | "Content-Type": "application/x-www-form-urlencoded"
27 | };
28 | const body = new URLSearchParams({
29 | "grant_type": grantType,
30 | "scope": scopes.join(" "),
31 | "assertion": assertion
32 | });
33 | return _post("/authentication/v2/token", headers, body);
34 | }
35 |
36 | /**
37 | * Creates a JWT assertion for OAuth 2.0 authentication.
38 | *
39 | * @param {string} clientId - The client ID of the application.
40 | * @param {string} serviceAccountId - The service account ID.
41 | * @param {string} serviceAccountKeyId - The key ID of the service account.
42 | * @param {string} serviceAccountPrivateKey - The private key of the service account.
43 | * @param {Array} scopes - The scopes for the access token.
44 | * @returns {string} - The signed JWT assertion.
45 | */
46 | function createAssertion(clientId, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes) {
47 | const payload = {
48 | iss: clientId, // Issuer
49 | sub: serviceAccountId, // Subject
50 | aud: "https://developer.api.autodesk.com/authentication/v2/token", // Audience
51 | exp: Math.floor(Date.now() / 1000) + 300, // Expiration time (5 minutes from now)
52 | scope: scopes
53 | };
54 | const options = {
55 | algorithm: "RS256", // Signing algorithm
56 | header: { alg: "RS256", kid: serviceAccountKeyId } // Header with key ID
57 | };
58 | return jwt.sign(payload, serviceAccountPrivateKey, options);
59 | }
60 |
61 | /**
62 | * Generates an access token for APS using client credentials ("two-legged") flow.
63 | *
64 | * @param {string} clientId - The client ID provided by Autodesk.
65 | * @param {string} clientSecret - The client secret provided by Autodesk.
66 | * @param {string[]} scopes - An array of scopes for which the token is requested.
67 | * @returns {Promise<{ access_token: string; token_type: string; expires_in: number; }>} A promise that resolves to the access token response object.
68 | * @throws {Error} If the request for the access token fails.
69 | */
70 | export async function getClientCredentialsAccessToken(clientId, clientSecret, scopes) {
71 | return getAccessToken(clientId, clientSecret, "client_credentials", scopes);
72 | }
73 |
74 | /**
75 | * Retrieves an access token for a service account using client credentials and JWT assertion.
76 | *
77 | * @param {string} clientId - The client ID for the OAuth application.
78 | * @param {string} clientSecret - The client secret for the OAuth application.
79 | * @param {string} serviceAccountId - The ID of the service account.
80 | * @param {string} serviceAccountKeyId - The key ID of the service account.
81 | * @param {string} serviceAccountPrivateKey - The private key of the service account.
82 | * @param {string[]} scopes - An array of scopes for the access token.
83 | * @returns {Promise<{ access_token: string; token_type: string; expires_in: number; }>} A promise that resolves to the access token response object.
84 | * @throws {Error} If the access token could not be retrieved.
85 | */
86 | export async function getServiceAccountAccessToken(clientId, clientSecret, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes) {
87 | const assertion = createAssertion(clientId, serviceAccountId, serviceAccountKeyId, serviceAccountPrivateKey, scopes);
88 | return getAccessToken(clientId, clientSecret, "urn:ietf:params:oauth:grant-type:jwt-bearer", scopes, assertion);
89 | }
90 |
91 | /**
92 | * Creates a new service account with the given name.
93 | *
94 | * @param {string} name - The name of the service account to create (must be between 5 and 64 characters long).
95 | * @param {string} firstName - The first name of the service account user.
96 | * @param {string} lastName - The last name of the service account user.
97 | * @param {string} accessToken - The access token for authentication.
98 | * @returns {Promise<{ serviceAccountId: string; email: string; }>} A promise that resolves to the created service account response.
99 | * @throws {Error} If the request to create the service account fails.
100 | */
101 | export async function createServiceAccount(name, firstName, lastName, accessToken) {
102 | const headers = {
103 | "Accept": "application/json",
104 | "Authorization": `Bearer ${accessToken}`,
105 | "Content-Type": "application/json"
106 | };
107 | const body = JSON.stringify({ name, firstName, lastName });
108 | return _post("/authentication/v2/service-accounts", headers, body);
109 | }
110 |
111 | /**
112 | * Creates a private key for a given service account.
113 | *
114 | * @param {string} serviceAccountId - The ID of the service account for which to create a private key.
115 | * @param {string} accessToken - The access token used for authorization.
116 | * @returns {Promise<{ kid: string; privateKey: string; }>} A promise that resolves to the private key details.
117 | * @throws {Error} If the request to create the private key fails.
118 | */
119 | export async function createServiceAccountPrivateKey(serviceAccountId, accessToken) {
120 | const headers = {
121 | "Accept": "application/json",
122 | "Authorization": `Bearer ${accessToken}`
123 | };
124 | return _post(`/authentication/v2/service-accounts/${serviceAccountId}/keys`, headers);
125 | }
--------------------------------------------------------------------------------
/services/ssa-auth-app/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ssa-auth-app",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {
6 | "": {
7 | "name": "ssa-auth-app",
8 | "dependencies": {
9 | "@fastify/cors": "^11.0.1",
10 | "dotenv": "^16.4.5",
11 | "fastify": "^5.0.0",
12 | "jsonwebtoken": "^9.0.2"
13 | },
14 | "bin": {
15 | "create-service-account": "tools/create-service-account.js"
16 | }
17 | },
18 | "node_modules/@fastify/ajv-compiler": {
19 | "version": "4.0.1",
20 | "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.1.tgz",
21 | "integrity": "sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==",
22 | "dependencies": {
23 | "ajv": "^8.12.0",
24 | "ajv-formats": "^3.0.1",
25 | "fast-uri": "^3.0.0"
26 | }
27 | },
28 | "node_modules/@fastify/cors": {
29 | "version": "11.0.1",
30 | "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.1.tgz",
31 | "integrity": "sha512-dmZaE7M1f4SM8ZZuk5RhSsDJ+ezTgI7v3HHRj8Ow9CneczsPLZV6+2j2uwdaSLn8zhTv6QV0F4ZRcqdalGx1pQ==",
32 | "funding": [
33 | {
34 | "type": "github",
35 | "url": "https://github.com/sponsors/fastify"
36 | },
37 | {
38 | "type": "opencollective",
39 | "url": "https://opencollective.com/fastify"
40 | }
41 | ],
42 | "license": "MIT",
43 | "dependencies": {
44 | "fastify-plugin": "^5.0.0",
45 | "toad-cache": "^3.7.0"
46 | }
47 | },
48 | "node_modules/@fastify/error": {
49 | "version": "4.0.0",
50 | "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.0.0.tgz",
51 | "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA=="
52 | },
53 | "node_modules/@fastify/fast-json-stringify-compiler": {
54 | "version": "5.0.1",
55 | "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.1.tgz",
56 | "integrity": "sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==",
57 | "dependencies": {
58 | "fast-json-stringify": "^6.0.0"
59 | }
60 | },
61 | "node_modules/@fastify/merge-json-schemas": {
62 | "version": "0.1.1",
63 | "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz",
64 | "integrity": "sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==",
65 | "dependencies": {
66 | "fast-deep-equal": "^3.1.3"
67 | }
68 | },
69 | "node_modules/abstract-logging": {
70 | "version": "2.0.1",
71 | "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
72 | "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
73 | },
74 | "node_modules/ajv": {
75 | "version": "8.17.1",
76 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
77 | "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
78 | "dependencies": {
79 | "fast-deep-equal": "^3.1.3",
80 | "fast-uri": "^3.0.1",
81 | "json-schema-traverse": "^1.0.0",
82 | "require-from-string": "^2.0.2"
83 | },
84 | "funding": {
85 | "type": "github",
86 | "url": "https://github.com/sponsors/epoberezkin"
87 | }
88 | },
89 | "node_modules/ajv-formats": {
90 | "version": "3.0.1",
91 | "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
92 | "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
93 | "dependencies": {
94 | "ajv": "^8.0.0"
95 | },
96 | "peerDependencies": {
97 | "ajv": "^8.0.0"
98 | },
99 | "peerDependenciesMeta": {
100 | "ajv": {
101 | "optional": true
102 | }
103 | }
104 | },
105 | "node_modules/atomic-sleep": {
106 | "version": "1.0.0",
107 | "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
108 | "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
109 | "engines": {
110 | "node": ">=8.0.0"
111 | }
112 | },
113 | "node_modules/avvio": {
114 | "version": "9.1.0",
115 | "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz",
116 | "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==",
117 | "dependencies": {
118 | "@fastify/error": "^4.0.0",
119 | "fastq": "^1.17.1"
120 | }
121 | },
122 | "node_modules/buffer-equal-constant-time": {
123 | "version": "1.0.1",
124 | "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
125 | "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="
126 | },
127 | "node_modules/cookie": {
128 | "version": "0.7.1",
129 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
130 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
131 | "engines": {
132 | "node": ">= 0.6"
133 | }
134 | },
135 | "node_modules/dotenv": {
136 | "version": "16.4.5",
137 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
138 | "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
139 | "engines": {
140 | "node": ">=12"
141 | },
142 | "funding": {
143 | "url": "https://dotenvx.com"
144 | }
145 | },
146 | "node_modules/ecdsa-sig-formatter": {
147 | "version": "1.0.11",
148 | "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
149 | "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
150 | "dependencies": {
151 | "safe-buffer": "^5.0.1"
152 | }
153 | },
154 | "node_modules/fast-decode-uri-component": {
155 | "version": "1.0.1",
156 | "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
157 | "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="
158 | },
159 | "node_modules/fast-deep-equal": {
160 | "version": "3.1.3",
161 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
162 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
163 | },
164 | "node_modules/fast-json-stringify": {
165 | "version": "6.0.0",
166 | "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.0.tgz",
167 | "integrity": "sha512-FGMKZwniMTgZh7zQp9b6XnBVxUmKVahQLQeRQHqwYmPDqDhcEKZ3BaQsxelFFI5PY7nN71OEeiL47/zUWcYe1A==",
168 | "dependencies": {
169 | "@fastify/merge-json-schemas": "^0.1.1",
170 | "ajv": "^8.12.0",
171 | "ajv-formats": "^3.0.1",
172 | "fast-deep-equal": "^3.1.3",
173 | "fast-uri": "^2.3.0",
174 | "json-schema-ref-resolver": "^1.0.1",
175 | "rfdc": "^1.2.0"
176 | }
177 | },
178 | "node_modules/fast-json-stringify/node_modules/fast-uri": {
179 | "version": "2.4.0",
180 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz",
181 | "integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA=="
182 | },
183 | "node_modules/fast-querystring": {
184 | "version": "1.1.2",
185 | "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
186 | "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
187 | "dependencies": {
188 | "fast-decode-uri-component": "^1.0.1"
189 | }
190 | },
191 | "node_modules/fast-redact": {
192 | "version": "3.5.0",
193 | "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
194 | "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
195 | "engines": {
196 | "node": ">=6"
197 | }
198 | },
199 | "node_modules/fast-uri": {
200 | "version": "3.0.3",
201 | "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
202 | "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw=="
203 | },
204 | "node_modules/fastify": {
205 | "version": "5.0.0",
206 | "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.0.0.tgz",
207 | "integrity": "sha512-Qe4dU+zGOzg7vXjw4EvcuyIbNnMwTmcuOhlOrOJsgwzvjEZmsM/IeHulgJk+r46STjdJS/ZJbxO8N70ODXDMEQ==",
208 | "funding": [
209 | {
210 | "type": "github",
211 | "url": "https://github.com/sponsors/fastify"
212 | },
213 | {
214 | "type": "opencollective",
215 | "url": "https://opencollective.com/fastify"
216 | }
217 | ],
218 | "dependencies": {
219 | "@fastify/ajv-compiler": "^4.0.0",
220 | "@fastify/error": "^4.0.0",
221 | "@fastify/fast-json-stringify-compiler": "^5.0.0",
222 | "abstract-logging": "^2.0.1",
223 | "avvio": "^9.0.0",
224 | "fast-json-stringify": "^6.0.0",
225 | "find-my-way": "^9.0.0",
226 | "light-my-request": "^6.0.0",
227 | "pino": "^9.0.0",
228 | "process-warning": "^4.0.0",
229 | "proxy-addr": "^2.0.7",
230 | "rfdc": "^1.3.1",
231 | "secure-json-parse": "^2.7.0",
232 | "semver": "^7.6.0",
233 | "toad-cache": "^3.7.0"
234 | }
235 | },
236 | "node_modules/fastify-plugin": {
237 | "version": "5.0.1",
238 | "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz",
239 | "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==",
240 | "license": "MIT"
241 | },
242 | "node_modules/fastq": {
243 | "version": "1.17.1",
244 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
245 | "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
246 | "dependencies": {
247 | "reusify": "^1.0.4"
248 | }
249 | },
250 | "node_modules/find-my-way": {
251 | "version": "9.1.0",
252 | "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.1.0.tgz",
253 | "integrity": "sha512-Y5jIsuYR4BwWDYYQ2A/RWWE6gD8a0FMgtU+HOq1WKku+Cwdz8M1v8wcAmRXXM1/iqtoqg06v+LjAxMYbCjViMw==",
254 | "dependencies": {
255 | "fast-deep-equal": "^3.1.3",
256 | "fast-querystring": "^1.0.0",
257 | "safe-regex2": "^4.0.0"
258 | },
259 | "engines": {
260 | "node": ">=14"
261 | }
262 | },
263 | "node_modules/forwarded": {
264 | "version": "0.2.0",
265 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
266 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
267 | "engines": {
268 | "node": ">= 0.6"
269 | }
270 | },
271 | "node_modules/ipaddr.js": {
272 | "version": "1.9.1",
273 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
274 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
275 | "engines": {
276 | "node": ">= 0.10"
277 | }
278 | },
279 | "node_modules/json-schema-ref-resolver": {
280 | "version": "1.0.1",
281 | "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-1.0.1.tgz",
282 | "integrity": "sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==",
283 | "dependencies": {
284 | "fast-deep-equal": "^3.1.3"
285 | }
286 | },
287 | "node_modules/json-schema-traverse": {
288 | "version": "1.0.0",
289 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
290 | "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
291 | },
292 | "node_modules/jsonwebtoken": {
293 | "version": "9.0.2",
294 | "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
295 | "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
296 | "dependencies": {
297 | "jws": "^3.2.2",
298 | "lodash.includes": "^4.3.0",
299 | "lodash.isboolean": "^3.0.3",
300 | "lodash.isinteger": "^4.0.4",
301 | "lodash.isnumber": "^3.0.3",
302 | "lodash.isplainobject": "^4.0.6",
303 | "lodash.isstring": "^4.0.1",
304 | "lodash.once": "^4.0.0",
305 | "ms": "^2.1.1",
306 | "semver": "^7.5.4"
307 | },
308 | "engines": {
309 | "node": ">=12",
310 | "npm": ">=6"
311 | }
312 | },
313 | "node_modules/jsonwebtoken/node_modules/ms": {
314 | "version": "2.1.3",
315 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
316 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
317 | },
318 | "node_modules/jwa": {
319 | "version": "1.4.1",
320 | "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz",
321 | "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==",
322 | "dependencies": {
323 | "buffer-equal-constant-time": "1.0.1",
324 | "ecdsa-sig-formatter": "1.0.11",
325 | "safe-buffer": "^5.0.1"
326 | }
327 | },
328 | "node_modules/jws": {
329 | "version": "3.2.2",
330 | "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
331 | "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
332 | "dependencies": {
333 | "jwa": "^1.4.1",
334 | "safe-buffer": "^5.0.1"
335 | }
336 | },
337 | "node_modules/light-my-request": {
338 | "version": "6.1.0",
339 | "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.1.0.tgz",
340 | "integrity": "sha512-+NFuhlOGoEwxeQfJ/pobkVFxcnKyDtiX847hLjuB/IzBxIl3q4VJeFI8uRCgb3AlTWL1lgOr+u5+8QdUcr33ng==",
341 | "dependencies": {
342 | "cookie": "^0.7.0",
343 | "process-warning": "^4.0.0",
344 | "set-cookie-parser": "^2.6.0"
345 | }
346 | },
347 | "node_modules/lodash.includes": {
348 | "version": "4.3.0",
349 | "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
350 | "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="
351 | },
352 | "node_modules/lodash.isboolean": {
353 | "version": "3.0.3",
354 | "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
355 | "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="
356 | },
357 | "node_modules/lodash.isinteger": {
358 | "version": "4.0.4",
359 | "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
360 | "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="
361 | },
362 | "node_modules/lodash.isnumber": {
363 | "version": "3.0.3",
364 | "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
365 | "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="
366 | },
367 | "node_modules/lodash.isplainobject": {
368 | "version": "4.0.6",
369 | "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
370 | "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="
371 | },
372 | "node_modules/lodash.isstring": {
373 | "version": "4.0.1",
374 | "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
375 | "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="
376 | },
377 | "node_modules/lodash.once": {
378 | "version": "4.1.1",
379 | "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
380 | "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="
381 | },
382 | "node_modules/on-exit-leak-free": {
383 | "version": "2.1.2",
384 | "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
385 | "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
386 | "engines": {
387 | "node": ">=14.0.0"
388 | }
389 | },
390 | "node_modules/pino": {
391 | "version": "9.5.0",
392 | "resolved": "https://registry.npmjs.org/pino/-/pino-9.5.0.tgz",
393 | "integrity": "sha512-xSEmD4pLnV54t0NOUN16yCl7RIB1c5UUOse5HSyEXtBp+FgFQyPeDutc+Q2ZO7/22vImV7VfEjH/1zV2QuqvYw==",
394 | "dependencies": {
395 | "atomic-sleep": "^1.0.0",
396 | "fast-redact": "^3.1.1",
397 | "on-exit-leak-free": "^2.1.0",
398 | "pino-abstract-transport": "^2.0.0",
399 | "pino-std-serializers": "^7.0.0",
400 | "process-warning": "^4.0.0",
401 | "quick-format-unescaped": "^4.0.3",
402 | "real-require": "^0.2.0",
403 | "safe-stable-stringify": "^2.3.1",
404 | "sonic-boom": "^4.0.1",
405 | "thread-stream": "^3.0.0"
406 | },
407 | "bin": {
408 | "pino": "bin.js"
409 | }
410 | },
411 | "node_modules/pino-abstract-transport": {
412 | "version": "2.0.0",
413 | "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
414 | "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
415 | "dependencies": {
416 | "split2": "^4.0.0"
417 | }
418 | },
419 | "node_modules/pino-std-serializers": {
420 | "version": "7.0.0",
421 | "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
422 | "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
423 | },
424 | "node_modules/process-warning": {
425 | "version": "4.0.0",
426 | "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.0.tgz",
427 | "integrity": "sha512-/MyYDxttz7DfGMMHiysAsFE4qF+pQYAA8ziO/3NcRVrQ5fSk+Mns4QZA/oRPFzvcqNoVJXQNWNAsdwBXLUkQKw=="
428 | },
429 | "node_modules/proxy-addr": {
430 | "version": "2.0.7",
431 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
432 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
433 | "dependencies": {
434 | "forwarded": "0.2.0",
435 | "ipaddr.js": "1.9.1"
436 | },
437 | "engines": {
438 | "node": ">= 0.10"
439 | }
440 | },
441 | "node_modules/quick-format-unescaped": {
442 | "version": "4.0.4",
443 | "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
444 | "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
445 | },
446 | "node_modules/real-require": {
447 | "version": "0.2.0",
448 | "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
449 | "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
450 | "engines": {
451 | "node": ">= 12.13.0"
452 | }
453 | },
454 | "node_modules/require-from-string": {
455 | "version": "2.0.2",
456 | "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
457 | "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
458 | "engines": {
459 | "node": ">=0.10.0"
460 | }
461 | },
462 | "node_modules/ret": {
463 | "version": "0.5.0",
464 | "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
465 | "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
466 | "engines": {
467 | "node": ">=10"
468 | }
469 | },
470 | "node_modules/reusify": {
471 | "version": "1.0.4",
472 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz",
473 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
474 | "engines": {
475 | "iojs": ">=1.0.0",
476 | "node": ">=0.10.0"
477 | }
478 | },
479 | "node_modules/rfdc": {
480 | "version": "1.4.1",
481 | "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
482 | "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="
483 | },
484 | "node_modules/safe-buffer": {
485 | "version": "5.2.1",
486 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
487 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
488 | "funding": [
489 | {
490 | "type": "github",
491 | "url": "https://github.com/sponsors/feross"
492 | },
493 | {
494 | "type": "patreon",
495 | "url": "https://www.patreon.com/feross"
496 | },
497 | {
498 | "type": "consulting",
499 | "url": "https://feross.org/support"
500 | }
501 | ]
502 | },
503 | "node_modules/safe-regex2": {
504 | "version": "4.0.0",
505 | "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-4.0.0.tgz",
506 | "integrity": "sha512-Hvjfv25jPDVr3U+4LDzBuZPPOymELG3PYcSk5hcevooo1yxxamQL/bHs/GrEPGmMoMEwRrHVGiCA1pXi97B8Ew==",
507 | "dependencies": {
508 | "ret": "~0.5.0"
509 | }
510 | },
511 | "node_modules/safe-stable-stringify": {
512 | "version": "2.5.0",
513 | "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
514 | "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
515 | "engines": {
516 | "node": ">=10"
517 | }
518 | },
519 | "node_modules/secure-json-parse": {
520 | "version": "2.7.0",
521 | "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
522 | "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
523 | },
524 | "node_modules/semver": {
525 | "version": "7.6.3",
526 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
527 | "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
528 | "bin": {
529 | "semver": "bin/semver.js"
530 | },
531 | "engines": {
532 | "node": ">=10"
533 | }
534 | },
535 | "node_modules/set-cookie-parser": {
536 | "version": "2.7.1",
537 | "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
538 | "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="
539 | },
540 | "node_modules/sonic-boom": {
541 | "version": "4.2.0",
542 | "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
543 | "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
544 | "dependencies": {
545 | "atomic-sleep": "^1.0.0"
546 | }
547 | },
548 | "node_modules/split2": {
549 | "version": "4.2.0",
550 | "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
551 | "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
552 | "engines": {
553 | "node": ">= 10.x"
554 | }
555 | },
556 | "node_modules/thread-stream": {
557 | "version": "3.1.0",
558 | "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
559 | "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
560 | "dependencies": {
561 | "real-require": "^0.2.0"
562 | }
563 | },
564 | "node_modules/toad-cache": {
565 | "version": "3.7.0",
566 | "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
567 | "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
568 | "engines": {
569 | "node": ">=12"
570 | }
571 | }
572 | }
573 | }
574 |
--------------------------------------------------------------------------------
/services/ssa-auth-app/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ssa-auth-app",
3 | "description": "Simple web application providing authentication for accessing project and design data in Autodesk Construction Cloud using Secure Service Accounts.",
4 | "keywords": [
5 | "autodesk-platform-services",
6 | "service-account"
7 | ],
8 | "author": "Petr Broz ",
9 | "type": "module",
10 | "bin": {
11 | "create-service-account": "./tools/create-service-account.js"
12 | },
13 | "scripts": {
14 | "start": "node server.js"
15 | },
16 | "dependencies": {
17 | "@fastify/cors": "^11.0.1",
18 | "dotenv": "^16.4.5",
19 | "fastify": "^5.0.0",
20 | "jsonwebtoken": "^9.0.2"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/services/ssa-auth-app/server.js:
--------------------------------------------------------------------------------
1 | import dotenv from "dotenv";
2 | import fastify from "fastify";
3 | import cors from "@fastify/cors";
4 | import { getServiceAccountAccessToken } from "./lib/auth.js";
5 |
6 | const { APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_EMAIL, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY, PORT } = dotenv.config().parsed;
7 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !APS_SA_ID || !APS_SA_EMAIL || !APS_SA_KEY_ID || !APS_SA_PRIVATE_KEY) {
8 | 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");
9 | process.exit(1);
10 | }
11 | const SCOPES = ["viewables:read", "data:read"];
12 | const HTML = `
13 |
14 |
15 |
16 |
17 |
18 | SSA Auth App
19 |
20 |
21 | SSA Auth App
22 | This is a simple web application providing authentication for accessing project and design data in Autodesk Construction Cloud using Secure Service Accounts.
23 | Usage
24 |
25 | - Add the following APS Client ID as a custom integration to your ACC account:
${APS_CLIENT_ID}
26 | - Invite the following Service Account to your project, and configure its permissions as needed:
${APS_SA_EMAIL}
27 | - Use the /token endpoint to generate an access token.
28 | - Use the token to access project or design data in ACC, for example, from a Power BI report.
29 |
30 |
31 |
32 | `;
33 |
34 | const app = fastify({ logger: true });
35 | await app.register(cors, { origin: "*", methods: ["GET"] });
36 | app.get("/", (request, reply) => { reply.type("text/html").send(HTML); });
37 | app.get("/token", () => getServiceAccountAccessToken(APS_CLIENT_ID, APS_CLIENT_SECRET, APS_SA_ID, APS_SA_KEY_ID, APS_SA_PRIVATE_KEY, SCOPES));
38 | try {
39 | await app.listen({ port: PORT || 3000 });
40 | } catch (err) {
41 | app.log.error(err);
42 | process.exit(1);
43 | }
--------------------------------------------------------------------------------
/services/ssa-auth-app/tools/create-service-account.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import process from "node:process";
4 | import dotenv from "dotenv";
5 | import { getClientCredentialsAccessToken, createServiceAccount, createServiceAccountPrivateKey } from "../lib/auth.js";
6 |
7 | const { APS_CLIENT_ID, APS_CLIENT_SECRET } = dotenv.config().parsed;
8 | const [,, userName, firstName, lastName] = process.argv;
9 | if (!APS_CLIENT_ID || !APS_CLIENT_SECRET || !userName || !firstName || !lastName) {
10 | console.error("Usage:");
11 | console.error(" APS_CLIENT_ID= APS_CLIENT_SECRET= node create-service-account.js ");
12 | process.exit(1);
13 | }
14 |
15 | try {
16 | const credentials = await getClientCredentialsAccessToken(APS_CLIENT_ID, APS_CLIENT_SECRET, ["application:service_account:write", "application:service_account_key:write"]);
17 | const { serviceAccountId, email } = await createServiceAccount(userName, firstName, lastName, credentials.access_token);
18 | const { kid, privateKey } = await createServiceAccountPrivateKey(serviceAccountId, credentials.access_token);
19 | console.log("Service account created successfully!");
20 | console.log("Invite the following user to your project:", email);
21 | console.log("Include the following environment variables to your application:");
22 | console.log(`APS_SA_ID="${serviceAccountId}"`);
23 | console.log(`APS_SA_EMAIL="${email}"`);
24 | console.log(`APS_SA_KEY_ID="${kid}"`);
25 | console.log(`APS_SA_PRIVATE_KEY="${privateKey}"`);
26 | } catch (err) {
27 | console.error(err);
28 | process.exit(1);
29 | }
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/.eslintignore:
--------------------------------------------------------------------------------
1 | assets
2 | style
3 | dist
4 | node_modules
5 | .eslintrc.js
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | "browser": true,
4 | "es6": true,
5 | "es2017": true
6 | },
7 | root: true,
8 | parser: "@typescript-eslint/parser",
9 | parserOptions: {
10 | project: "tsconfig.json",
11 | tsconfigRootDir: ".",
12 | },
13 | plugins: [
14 | "powerbi-visuals"
15 | ],
16 | extends: [
17 | "plugin:powerbi-visuals/recommended"
18 | ],
19 | rules: {}
20 | };
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | .tmp
4 | .env
5 | *.log
6 | webpack.statistics.dev.html
7 | webpack.statistics.prod.html
8 | .DS_Store
9 | Thumbs.db
10 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "configurations": [
4 | {
5 | "name": "Debugger",
6 | "type": "chrome",
7 | "request": "attach",
8 | "port": 9222,
9 | "sourceMaps": true,
10 | "webRoot": "${cwd}/"
11 | }
12 | ]
13 | }
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.tabSize": 4,
3 | "editor.insertSpaces": true,
4 | "files.eol": "\n",
5 | "files.watcherExclude": {
6 | "**/.git/objects/**": true,
7 | "**/node_modules/**": true,
8 | ".tmp": true
9 | },
10 | "files.exclude": {
11 | ".tmp": true
12 | },
13 | "files.associations": {
14 | "*.resjson": "json"
15 | },
16 | "search.exclude": {
17 | ".tmp": true,
18 | "typings": true
19 | },
20 | "json.schemas": [
21 | {
22 | "fileMatch": [
23 | "/pbiviz.json"
24 | ],
25 | "url": "./node_modules/powerbi-visuals-api/schema.pbiviz.json"
26 | },
27 | {
28 | "fileMatch": [
29 | "/capabilities.json"
30 | ],
31 | "url": "./node_modules/powerbi-visuals-api/schema.capabilities.json"
32 | },
33 | {
34 | "fileMatch": [
35 | "/dependencies.json"
36 | ],
37 | "url": "./node_modules/powerbi-visuals-api/schema.dependencies.json"
38 | }
39 | ]
40 | }
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Autodesk
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/README.md:
--------------------------------------------------------------------------------
1 | # APS Viewer Visual
2 |
3 | [Custom visual](https://powerbi.microsoft.com/en-us/developers/custom-visualization/) for previewing 2D and 3D designs from [Autodesk Platform Services](https://aps.autodesk.com) in Power BI reports.
4 |
5 | ## Usage
6 |
7 | [](https://www.youtube.com/watch?v=8wsA5sd4_Xc)
8 |
9 | ## Development
10 |
11 | ### Prerequisites
12 |
13 | - [Set up your environment for developing Power BI visuals](https://learn.microsoft.com/en-us/power-bi/developer/visuals/environment-setup)
14 | - Note: this project has been developed and tested with `pbiviz` version 5.4.x
15 |
16 | The viewer relies on an external web service to generate access tokens for accessing design data in Autodesk Platform Services. The response from the web service should be a JSON with the following structure:
17 |
18 | ```json
19 | {
20 | "access_token": ,
21 | "token_type": "Bearer",
22 | "expires_in":
23 | }
24 | ```
25 |
26 | If you don't want to build your own web service, consider using the [APS Shares App](../../services/aps-shares-app/) application that's part of this repository.
27 |
28 | ### Running locally
29 |
30 | - Clone this repository
31 | - Install dependencies: `npm install`
32 | - Run the local development server: `npm start`
33 | - Open one of your Power BI reports on https://app.powerbi.com
34 | - Add a _Developer Visual_ from the _Visualizations_ tab to the report
35 |
36 | 
37 |
38 | > If you see an error saying "Can't contact visual server", open a new tab in your browser, navigate to https://localhost:8080/assets, and authorize your browser to use this address.
39 |
40 | - With the visual selected, switch to the _Format your visual_ tab, and add your authentication endpoint URL to the _Access Token Endpoint_ input
41 |
42 | 
43 |
44 | - Drag & drop the columns from your data that represent the design URNs and element IDs to the _Design URNs & Element IDs_ bucket
45 |
46 | 
47 |
48 | ### Publish
49 |
50 | - Update [pbiviz.json](./pbiviz.json) with your own visual name, description, etc.
51 | - If needed, update the [capabilities.json](./capabilities.json) file, restricting the websites that the visual will have access to (for example, replacing the `[ "*" ]` list under the `"privileges"` section with `[ "https://your-custom-app.com", "https://*.autodesk.com" ]`)
52 | - Build the *.pbiviz file using `npm run package`
53 | - Import the newly created *.pbiviz file from the _dist_ subfolder into your Power BI report
54 |
55 | ## FAQ
56 |
57 | ### Where do I find URN/GUID values?
58 |
59 | You can retrieve the design URN and viewable GUID after loading the design into any APS-based application. For example, after opening your design in [Autodesk Construction Cloud](https://construction.autodesk.com), open the browser console and type `NOP_VIEWER.model.getData().urn` to retrieve the URN, and `NOP_VIEWER.model.getDocumentNode().guid()` to retrieve the GUID.
60 |
61 | ### Why the visual card cannot load my models?
62 |
63 | Here are check points for your reference:
64 |
65 | 1. Ensure the changes you made has been saved into the PowerBI report after filling in `Access Token Endpoint`, `URN` and `GUID`. Commonly, we can verify this by closing PowerBI Desktop to see if it prompts warnings about unsaved changes.
66 |
67 | 2. Check if you have right access to the model by using the [simple viewer sample](https://aps.autodesk.com/en/docs/viewer/v7/developers_guide/viewer_basics/starting-html/) from APS viewer documentation with the access token and urn from the token endpoint.
68 |
69 | If your models are hosted on BIM360/ACC, please ensure your client id used in the token endpoint has provisioned in the BIM360/ACC account. If not, please follow the [Provision access in other products](https://tutorials.autodesk.io/#provision-access-in-other-products) section in APS tutorial to provision your client id.
70 |
71 | 3. Upload your PowerBI report containing the viewer visual card to your PowerBI workspace (https://app.powerbi.com/groups/me/list?experience=power-bi) and check if there is any error message appearing in the [Web browser dev console](https://developer.chrome.com/docs/devtools/console/).
72 |
73 | ## Troubleshooting
74 |
75 | Please contact us via https://aps.autodesk.com/get-help.
76 |
77 | ## License
78 |
79 | This sample is licensed under the terms of the [MIT License](http://opensource.org/licenses/MIT). Please see the [LICENSE](LICENSE) file for more details.
80 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/assets/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/assets/icon.png
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/capabilities.json:
--------------------------------------------------------------------------------
1 | {
2 | "dataRoles": [
3 | {
4 | "displayName": "Design URNs & Element IDs",
5 | "name": "elementIds",
6 | "kind": "Grouping"
7 | }
8 | ],
9 | "dataViewMappings": [
10 | {
11 | "conditions": [
12 | {
13 | "elementIds": { "min": 1, "max": 2 }
14 | }
15 | ],
16 | "table": {
17 | "rows": {
18 | "select": [
19 | {"for": { "in": "elementIds" }}
20 | ]
21 | }
22 | }
23 | }
24 | ],
25 | "objects": {
26 | "viewer": {
27 | "properties": {
28 | "accessTokenEndpoint": {
29 | "type": {
30 | "text": true
31 | }
32 | }
33 | }
34 | }
35 | },
36 | "privileges": [
37 | {
38 | "name": "WebAccess",
39 | "essential": true,
40 | "parameters": [
41 | "*"
42 | ]
43 | }
44 | ],
45 | "supportsMultiVisualSelection": true,
46 | "suppressDefaultTitle": true,
47 | "supportsLandingPage": true,
48 | "supportsEmptyDataView": true
49 | }
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/docs/add-developer-visual.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/docs/add-developer-visual.png
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/docs/add-element-ids.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/docs/add-element-ids.png
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/docs/add-token-endpoint.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/aps-viewer-visual/docs/add-token-endpoint.png
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aps-viewer-visual",
3 | "description": "Custom visual for embedding Autodesk Platform Services Viewer into PowerBI reports.",
4 | "license": "MIT",
5 | "scripts": {
6 | "pbiviz": "pbiviz",
7 | "start": "pbiviz start",
8 | "package": "pbiviz package",
9 | "lint": "npx eslint . --ext .js,.jsx,.ts,.tsx"
10 | },
11 | "dependencies": {
12 | "powerbi-visuals-api": "~5.4.0",
13 | "powerbi-visuals-utils-formattingmodel": "6.0.0"
14 | },
15 | "devDependencies": {
16 | "@types/forge-viewer": "^7.89.1",
17 | "@typescript-eslint/eslint-plugin": "^5.59.11",
18 | "eslint": "^8.42.0",
19 | "eslint-plugin-powerbi-visuals": "^0.8.1",
20 | "typescript": "4.9.3"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/pbiviz.json:
--------------------------------------------------------------------------------
1 | {
2 | "visual": {
3 | "name": "aps-viewer-visual",
4 | "displayName": "APS Viewer Visual (0.0.9.0)",
5 | "guid": "aps_viewer_visual_a4f2990a03324cf79eb44f982719df44",
6 | "visualClassName": "Visual",
7 | "version": "0.0.9.0",
8 | "description": "Visual for previewing shared 2D/3D designs from Autodesk Platform Services.",
9 | "supportUrl": "https://github.com/autodesk-platform-services/aps-powerbi-tools",
10 | "gitHubUrl": "https://github.com/autodesk-platform-services/aps-powerbi-tools"
11 | },
12 | "apiVersion": "5.4.0",
13 | "author": {
14 | "name": "Autodesk Platform Services - Developer Advocacy Support",
15 | "email": "aps.help@autodesk.com"
16 | },
17 | "assets": {
18 | "icon": "assets/icon.png"
19 | },
20 | "style": "style/visual.less",
21 | "capabilities": "capabilities.json",
22 | "dependencies": null,
23 | "stringResources": []
24 | }
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/src/settings.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { formattingSettings } from 'powerbi-visuals-utils-formattingmodel';
4 |
5 | import Card = formattingSettings.SimpleCard;
6 | import Slice = formattingSettings.Slice;
7 | import Model = formattingSettings.Model;
8 |
9 | class ViewerCard extends Card {
10 | accessTokenEndpoint = new formattingSettings.TextInput({
11 | name: 'accessTokenEndpoint',
12 | displayName: 'Access Token Endpoint',
13 | description: 'URL that the viewer can call to generate access tokens.',
14 | placeholder: '',
15 | value: ''
16 | });
17 | name: string = 'viewer';
18 | displayName: string = 'Viewer Runtime';
19 | slices: Array = [this.accessTokenEndpoint];
20 | }
21 |
22 | export class VisualSettingsModel extends Model {
23 | viewerCard = new ViewerCard();
24 | cards = [this.viewerCard];
25 | }
26 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/src/viewer.utils.ts:
--------------------------------------------------------------------------------
1 | /// import * as Autodesk from "@types/forge-viewer";
2 |
3 | 'use strict';
4 |
5 | const runtime: { options: Autodesk.Viewing.InitializerOptions; ready: Promise | null } = {
6 | options: {},
7 | ready: null
8 | };
9 |
10 | declare global {
11 | interface Window { DISABLE_INDEXED_DB: boolean; }
12 | }
13 |
14 | export function initializeViewerRuntime(options: Autodesk.Viewing.InitializerOptions): Promise {
15 | if (!runtime.ready) {
16 | runtime.options = { ...options };
17 | runtime.ready = (async function () {
18 | window.DISABLE_INDEXED_DB = true;
19 |
20 | await loadScript('https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js');
21 | await loadStylesheet('https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.css');
22 | return new Promise((resolve) => Autodesk.Viewing.Initializer(runtime.options, resolve));
23 | })() as Promise;
24 | } else {
25 | if (['accessToken', 'getAccessToken', 'env', 'api', 'language'].some(prop => options[prop] !== runtime.options[prop])) {
26 | return Promise.reject('Cannot initialize another viewer runtime with different settings.');
27 | }
28 | }
29 | return runtime.ready;
30 | }
31 |
32 | export function loadModel(viewer: Autodesk.Viewing.Viewer3D, urn: string, guid?: string): Promise {
33 | return new Promise(function (resolve, reject) {
34 | Autodesk.Viewing.Document.load(
35 | 'urn:' + urn,
36 | (doc) => {
37 | const view = guid ? doc.getRoot().findByGuid(guid) : doc.getRoot().getDefaultGeometry();
38 | viewer.loadDocumentNode(doc, view).then(m => resolve(m));
39 | },
40 | (code, message, args) => reject({ code, message, args })
41 | );
42 | });
43 | }
44 |
45 | export function getVisibleNodes(model: Autodesk.Viewing.Model): number[] {
46 | const tree = model.getInstanceTree();
47 | const dbids: number[] = [];
48 | tree.enumNodeChildren(tree.getRootId(), dbid => {
49 | if (tree.getChildCount(dbid) === 0 && !tree.isNodeHidden(dbid) && !tree.isNodeOff(dbid)) {
50 | dbids.push(dbid);
51 | }
52 | }, true);
53 | return dbids;
54 | }
55 |
56 | /**
57 | * Helper class for mapping between "dbIDs" (sequential numbers assigned to each design element;
58 | * typically used by the Viewer APIs) and "external IDs" (typically based on persistent IDs
59 | * from the authoring application, for example, Revit GUIDs).
60 | */
61 | export class IdMapping {
62 | private readonly externalIdMappingPromise: Promise<{ [externalId: string]: number; }>;
63 |
64 | constructor(private model: Autodesk.Viewing.Model) {
65 | this.externalIdMappingPromise = new Promise((resolve, reject) => {
66 | model.getExternalIdMapping(resolve, reject);
67 | });
68 | }
69 |
70 | /**
71 | * Converts external IDs into dbIDs.
72 | * @param externalIds List of external IDs.
73 | * @returns List of corresponding dbIDs.
74 | */
75 | getDbids(externalIds: string[]): Promise {
76 | return this.externalIdMappingPromise
77 | .then(externalIdMapping => externalIds.map(externalId => externalIdMapping[externalId]));
78 | }
79 |
80 | /**
81 | * Converts dbIDs into external IDs.
82 | * @param dbids List of dbIDs.
83 | * @returns List of corresponding external IDs.
84 | */
85 | getExternalIds(dbids: number[]): Promise {
86 | return new Promise((resolve, reject) => {
87 | this.model.getBulkProperties(dbids, { propFilter: ['externalId'] }, results => {
88 | resolve(results.map(result => result.externalId))
89 | }, reject);
90 | });
91 | }
92 | }
93 |
94 | function loadScript(src: string): Promise {
95 | return new Promise((resolve, reject) => {
96 | const el = document.createElement("script");
97 | el.onload = () => resolve();
98 | el.onerror = (err) => reject(err);
99 | el.type = 'application/javascript';
100 | el.src = src;
101 | document.head.appendChild(el);
102 | });
103 | }
104 |
105 | function loadStylesheet(href: string): Promise {
106 | return new Promise((resolve, reject) => {
107 | const el = document.createElement('link');
108 | el.onload = () => resolve();
109 | el.onerror = (err) => reject(err);
110 | el.rel = 'stylesheet';
111 | el.href = href;
112 | document.head.appendChild(el);
113 | });
114 | }
115 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/src/visual.ts:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import powerbi from 'powerbi-visuals-api';
4 | import { FormattingSettingsService } from 'powerbi-visuals-utils-formattingmodel';
5 | import '../style/visual.less';
6 |
7 | import VisualConstructorOptions = powerbi.extensibility.visual.VisualConstructorOptions;
8 | import VisualUpdateOptions = powerbi.extensibility.visual.VisualUpdateOptions;
9 | import IVisual = powerbi.extensibility.visual.IVisual;
10 | import IVisualHost = powerbi.extensibility.visual.IVisualHost;
11 | import ISelectionManager = powerbi.extensibility.ISelectionManager;
12 | import DataView = powerbi.DataView;
13 |
14 | import { VisualSettingsModel } from './settings';
15 | import { initializeViewerRuntime, loadModel, IdMapping } from './viewer.utils';
16 |
17 | /**
18 | * Custom visual wrapper for the Autodesk Platform Services Viewer.
19 | */
20 | export class Visual implements IVisual {
21 | // Visual state
22 | private host: IVisualHost;
23 | private container: HTMLElement;
24 | private formattingSettings: VisualSettingsModel;
25 | private formattingSettingsService: FormattingSettingsService;
26 | private currentDataView: DataView = null;
27 | private selectionManager: ISelectionManager = null;
28 |
29 | // Visual inputs
30 | private accessTokenEndpoint: string = '';
31 |
32 | // Viewer runtime
33 | private viewer: Autodesk.Viewing.GuiViewer3D = null;
34 | private urn: string = '';
35 | private guid: string = '';
36 | private externalIds: string[] = [];
37 | private model: Autodesk.Viewing.Model = null;
38 | private idMapping: IdMapping = null;
39 |
40 | /**
41 | * Initializes the viewer visual.
42 | * @param options Additional visual initialization options.
43 | */
44 | constructor(options: VisualConstructorOptions) {
45 | this.host = options.host;
46 | this.selectionManager = options.host.createSelectionManager();
47 | this.formattingSettingsService = new FormattingSettingsService();
48 | this.container = options.element;
49 | this.getAccessToken = this.getAccessToken.bind(this);
50 | this.onPropertiesLoaded = this.onPropertiesLoaded.bind(this);
51 | this.onSelectionChanged = this.onSelectionChanged.bind(this);
52 | }
53 |
54 | /**
55 | * Notifies the viewer visual of an update (data, viewmode, size change).
56 | * @param options Additional visual update options.
57 | */
58 | public async update(options: VisualUpdateOptions): Promise {
59 | // this.logVisualUpdateOptions(options);
60 |
61 | this.formattingSettings = this.formattingSettingsService.populateFormattingSettingsModel(VisualSettingsModel, options.dataViews[0]);
62 | const { accessTokenEndpoint } = this.formattingSettings.viewerCard;
63 | if (accessTokenEndpoint.value !== this.accessTokenEndpoint) {
64 | this.accessTokenEndpoint = accessTokenEndpoint.value;
65 | if (!this.viewer) {
66 | this.initializeViewer();
67 | }
68 | }
69 |
70 | this.currentDataView = options.dataViews[0];
71 | if (this.currentDataView.table?.rows?.length > 0) {
72 | const rows = this.currentDataView.table.rows;
73 | const urns = this.collectDesignUrns(this.currentDataView);
74 | if (urns.length > 1) {
75 | this.showNotification('Multiple design URNs detected. Only the first one will be displayed.');
76 | }
77 | if (urns[0] !== this.urn) {
78 | this.urn = urns[0];
79 | this.updateModel();
80 | }
81 | this.externalIds = rows.map(r => r[1].valueOf() as string);
82 | } else {
83 | this.urn = '';
84 | this.externalIds = [];
85 | this.updateModel();
86 | }
87 |
88 | if (this.idMapping) {
89 | const isDataFilterApplied = this.currentDataView.metadata?.isDataFilterApplied;
90 | if (this.externalIds.length > 0 && isDataFilterApplied) {
91 | const dbids = await this.idMapping.getDbids(this.externalIds);
92 | this.viewer.isolate(dbids);
93 | this.viewer.fitToView(dbids);
94 | } else {
95 | this.viewer.isolate();
96 | this.viewer.fitToView();
97 | }
98 | }
99 | }
100 |
101 | /**
102 | * Returns properties pane formatting model content hierarchies, properties and latest formatting values, Then populate properties pane.
103 | * This method is called once every time we open properties pane or when the user edit any format property.
104 | */
105 | public getFormattingModel(): powerbi.visuals.FormattingModel {
106 | return this.formattingSettingsService.buildFormattingModel(this.formattingSettings);
107 | }
108 |
109 | /**
110 | * Displays a notification that will automatically disappear after some time.
111 | * @param content HTML content to display inside the notification.
112 | */
113 | private showNotification(content: string): void {
114 | let notifications = this.container.querySelector('#notifications');
115 | if (!notifications) {
116 | notifications = document.createElement('div');
117 | notifications.id = 'notifications';
118 | this.container.appendChild(notifications);
119 | }
120 | const notification = document.createElement('div');
121 | notification.className = 'notification';
122 | notification.innerText = content;
123 | notifications.appendChild(notification);
124 | setTimeout(() => notifications.removeChild(notification), 5000);
125 | }
126 |
127 | /**
128 | * Initializes the viewer runtime.
129 | */
130 | private async initializeViewer(): Promise {
131 | try {
132 | await initializeViewerRuntime({ env: 'AutodeskProduction2', api: 'streamingV2', getAccessToken: this.getAccessToken });
133 | this.container.innerText = '';
134 | this.viewer = new Autodesk.Viewing.GuiViewer3D(this.container);
135 | this.viewer.start();
136 | this.viewer.addEventListener(Autodesk.Viewing.OBJECT_TREE_CREATED_EVENT, this.onPropertiesLoaded);
137 | this.viewer.addEventListener(Autodesk.Viewing.SELECTION_CHANGED_EVENT, this.onSelectionChanged);
138 | if (this.urn) {
139 | this.updateModel();
140 | }
141 | } catch (err) {
142 | this.showNotification('Could not initialize viewer runtime. Please see console for more details.');
143 | console.error(err);
144 | }
145 | }
146 |
147 | /**
148 | * Retrieves a new access token for the viewer.
149 | * @param callback Callback function to call with new access token.
150 | */
151 | private async getAccessToken(callback: (accessToken: string, expiresIn: number) => void): Promise {
152 | try {
153 | const response = await fetch(this.accessTokenEndpoint);
154 | if (!response.ok) {
155 | throw new Error(await response.text());
156 | }
157 | const share = await response.json();
158 | callback(share.access_token, share.expires_in);
159 | } catch (err) {
160 | this.showNotification('Could not retrieve access token. Please see console for more details.');
161 | console.error(err);
162 | }
163 | }
164 |
165 | /**
166 | * Ensures that the correct model is loaded into the viewer.
167 | */
168 | private async updateModel(): Promise {
169 | if (!this.viewer) {
170 | return;
171 | }
172 |
173 | if (this.model && this.model.getData().urn !== this.urn) {
174 | this.viewer.unloadModel(this.model);
175 | this.model = null;
176 | this.idMapping = null;
177 | }
178 |
179 | try {
180 | if (this.urn) {
181 | this.model = await loadModel(this.viewer, this.urn, this.guid);
182 | }
183 | } catch (err) {
184 | this.showNotification('Could not load model in the viewer. See console for more details.');
185 | console.error(err);
186 | }
187 | }
188 |
189 | private async onPropertiesLoaded() {
190 | this.idMapping = new IdMapping(this.model);
191 | }
192 |
193 | private async onSelectionChanged() {
194 | const allExternalIds = this.currentDataView.table.rows;
195 | if (!allExternalIds) {
196 | return;
197 | }
198 | const selectedDbids = this.viewer.getSelection();
199 | const selectedExternalIds = await this.idMapping.getExternalIds(selectedDbids);
200 | const selectionIds: powerbi.extensibility.ISelectionId[] = [];
201 | for (const selectedExternalId of selectedExternalIds) {
202 | const rowIndex = allExternalIds.findIndex(row => row[1] === selectedExternalId);
203 | if (rowIndex !== -1) {
204 | const selectionId = this.host.createSelectionIdBuilder()
205 | .withTable(this.currentDataView.table, rowIndex)
206 | .createSelectionId();
207 | selectionIds.push(selectionId);
208 | }
209 | }
210 | this.selectionManager.select(selectionIds);
211 | }
212 |
213 | private collectDesignUrns(dataView: DataView): string[] {
214 | let urns = new Set(dataView.table.rows.map(row => row[0].valueOf() as string));
215 | return [...urns.values()];
216 | }
217 |
218 | private logVisualUpdateOptions(options: VisualUpdateOptions) {
219 | const EditMode = {
220 | [powerbi.EditMode.Advanced]: 'Advanced',
221 | [powerbi.EditMode.Default]: 'Default',
222 | };
223 | const VisualDataChangeOperationKind = {
224 | [powerbi.VisualDataChangeOperationKind.Append]: 'Append',
225 | [powerbi.VisualDataChangeOperationKind.Create]: 'Create',
226 | [powerbi.VisualDataChangeOperationKind.Segment]: 'Segment',
227 | };
228 | const VisualUpdateType = {
229 | [powerbi.VisualUpdateType.All]: 'All',
230 | [powerbi.VisualUpdateType.Data]: 'Data',
231 | [powerbi.VisualUpdateType.Resize]: 'Resize',
232 | [powerbi.VisualUpdateType.ResizeEnd]: 'ResizeEnd',
233 | [powerbi.VisualUpdateType.Style]: 'Style',
234 | [powerbi.VisualUpdateType.ViewMode]: 'ViewMode',
235 | };
236 | const ViewMode = {
237 | [powerbi.ViewMode.Edit]: 'Edit',
238 | [powerbi.ViewMode.InFocusEdit]: 'InFocusEdit',
239 | [powerbi.ViewMode.View]: 'View',
240 | };
241 | console.debug('editMode', EditMode[options.editMode]);
242 | console.debug('isInFocus', options.isInFocus);
243 | console.debug('jsonFilters', options.jsonFilters);
244 | console.debug('operationKind', VisualDataChangeOperationKind[options.operationKind]);
245 | console.debug('type', VisualUpdateType[options.type]);
246 | console.debug('viewMode', ViewMode[options.viewMode]);
247 | console.debug('viewport', options.viewport);
248 | console.debug('Data views:');
249 | for (const dataView of options.dataViews) {
250 | console.debug(dataView);
251 | }
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/style/visual.less:
--------------------------------------------------------------------------------
1 | #overlay {
2 | display: flex;
3 | flex-flow: column;
4 | align-items: center;
5 | justify-content: center;
6 | width: 100%;
7 | height: 100%;
8 | padding: 1em;
9 | background: #fafafa;
10 | }
11 |
12 | #overlay, button {
13 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
14 | }
15 |
16 | #notifications {
17 | z-index: 999;
18 | position: absolute;
19 | inset: 0;
20 | display: flex;
21 | flex-flow: column;
22 | overflow-y: scroll;
23 | }
24 |
25 | .notification {
26 | margin: 1em 1em 0 1em;
27 | padding: 1em;
28 | background: rgba(255, 255, 255, 0.9);
29 | box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
30 | }
31 |
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": false,
4 | "emitDecoratorMetadata": true,
5 | "experimentalDecorators": true,
6 | "target": "es2022",
7 | "sourceMap": true,
8 | "outDir": "./.tmp/build/",
9 | "moduleResolution": "node",
10 | "declaration": true,
11 | "lib": [
12 | "es2022",
13 | "dom"
14 | ]
15 | },
16 | "files": [
17 | "./src/visual.ts"
18 | ]
19 | }
--------------------------------------------------------------------------------
/visuals/aps-viewer-visual/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "class-name": true,
4 | "comment-format": [
5 | true,
6 | "check-space"
7 | ],
8 | "indent": [
9 | true,
10 | "spaces"
11 | ],
12 | "no-duplicate-variable": true,
13 | "no-eval": true,
14 | "no-internal-module": false,
15 | "no-trailing-whitespace": true,
16 | "no-unsafe-finally": true,
17 | "no-var-keyword": true,
18 | "one-line": [
19 | true,
20 | "check-open-brace",
21 | "check-whitespace"
22 | ],
23 | "quotemark": [
24 | false,
25 | "double"
26 | ],
27 | "semicolon": [
28 | true,
29 | "always"
30 | ],
31 | "triple-equals": [
32 | true,
33 | "allow-null-check"
34 | ],
35 | "typedef-whitespace": [
36 | true,
37 | {
38 | "call-signature": "nospace",
39 | "index-signature": "nospace",
40 | "parameter": "nospace",
41 | "property-declaration": "nospace",
42 | "variable-declaration": "nospace"
43 | }
44 | ],
45 | "variable-name": [
46 | true,
47 | "ban-keywords"
48 | ],
49 | "whitespace": [
50 | true,
51 | "check-branch",
52 | "check-decl",
53 | "check-operator",
54 | "check-separator",
55 | "check-type"
56 | ],
57 | "insecure-random": true,
58 | "no-banned-terms": true,
59 | "no-cookies": true,
60 | "no-delete-expression": true,
61 | "no-disable-auto-sanitization": true,
62 | "no-document-domain": true,
63 | "no-document-write": true,
64 | "no-exec-script": true,
65 | "no-function-constructor-with-string-args": true,
66 | "no-http-string": [true, "http://www.example.com/?.*", "http://www.examples.com/?.*"],
67 | "no-inner-html": true,
68 | "no-octal-literal": true,
69 | "no-reserved-keywords": true,
70 | "no-string-based-set-immediate": true,
71 | "no-string-based-set-interval": true,
72 | "no-string-based-set-timeout": true,
73 | "non-literal-require": true,
74 | "possible-timing-attack": true,
75 | "react-anchor-blank-noopener": true,
76 | "react-iframe-missing-sandbox": true,
77 | "react-no-dangerous-html": true
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/visuals/tandem-viewer-visual/release/tandem-visual-visual.1.0.0.1.pbiviz:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/autodesk-platform-services/aps-powerbi-tools/3bc6aeb22e20344ba0c865829bd5af8bd39aba8c/visuals/tandem-viewer-visual/release/tandem-visual-visual.1.0.0.1.pbiviz
--------------------------------------------------------------------------------