@cognite/dune 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,6 +7,10 @@ type Deployment = {
7
7
  deployClientId: string;
8
8
  deploySecretName: string;
9
9
  published: boolean;
10
+ /** Identity provider type. Defaults to "cdf" if not specified */
11
+ idpType?: "cdf" | "entra_id";
12
+ /** Tenant ID for Entra ID authentication. Required when idpType is "entra_id" */
13
+ tenantId?: string;
10
14
  };
11
15
  type App = {
12
16
  externalId: string;
@@ -85,6 +89,10 @@ declare class ApplicationPackager {
85
89
 
86
90
  declare const getSdk: (deployment: Deployment, folder: string) => Promise<CogniteClient>;
87
91
 
88
- declare const getToken: (deployClientId: string, deploySecretName: string) => Promise<any>;
92
+ /**
93
+ * Get access token for deployment using the appropriate identity provider.
94
+ * Supports both CDF OAuth and Entra ID (Azure AD) authentication.
95
+ */
96
+ declare const getToken: (deployment: Deployment) => Promise<string>;
89
97
 
90
98
  export { type App, ApplicationPackager, CdfApplicationDeployer, type Deployment, deploy, getSdk, getToken };
@@ -187,22 +187,39 @@ var loadSecretsFromEnv = () => {
187
187
  return {};
188
188
  }
189
189
  };
190
- var getToken = async (deployClientId, deploySecretName) => {
190
+ var getSecretFromEnv = (secretEnvVarName) => {
191
191
  let deploySecret;
192
192
  if (process.env.DEPLOYMENT_SECRET) {
193
193
  deploySecret = process.env.DEPLOYMENT_SECRET;
194
194
  }
195
195
  if (!deploySecret) {
196
196
  const secrets = loadSecretsFromEnv();
197
- deploySecret = secrets[deploySecretName];
197
+ deploySecret = secrets[secretEnvVarName];
198
198
  }
199
199
  if (!deploySecret) {
200
- deploySecret = process.env[deploySecretName];
200
+ deploySecret = process.env[secretEnvVarName];
201
201
  }
202
202
  if (!deploySecret) {
203
- throw new Error(`Deployment secret not found in environment: ${deploySecretName}`);
203
+ throw new Error(`Secret not found in environment: ${secretEnvVarName}`);
204
204
  }
205
- const header = `Basic ${btoa(`${deployClientId}:${deploySecret}`)}`;
205
+ return deploySecret;
206
+ };
207
+ var extractClusterFromUrl = (url) => {
208
+ if (!url) return "";
209
+ try {
210
+ const urlObj = new URL(url);
211
+ const hostname = urlObj.hostname;
212
+ return hostname.replace(/\.cognitedata\.com$/, "");
213
+ } catch {
214
+ let cluster = url.replace(/^https?:\/\//, "");
215
+ cluster = cluster.split("/")[0];
216
+ cluster = cluster.split(":")[0];
217
+ cluster = cluster.replace(/\.cognitedata\.com$/, "");
218
+ return cluster;
219
+ }
220
+ };
221
+ var getTokenCdf = async (clientId, clientSecret) => {
222
+ const header = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
206
223
  const response = await fetch("https://auth.cognite.com/oauth2/token", {
207
224
  method: "POST",
208
225
  headers: {
@@ -212,15 +229,80 @@ var getToken = async (deployClientId, deploySecretName) => {
212
229
  body: new URLSearchParams({ grant_type: "client_credentials" })
213
230
  });
214
231
  if (!response.ok) {
215
- throw new Error(`Failed to get token: ${response.status} ${response.statusText}`);
232
+ const errorText = await response.text();
233
+ throw new Error(
234
+ `Failed to get token from CDF: ${response.status} ${response.statusText}
235
+ ${errorText}`
236
+ );
216
237
  }
217
238
  const data = await response.json();
239
+ if (!data.access_token) {
240
+ throw new Error("No access token returned from CDF authentication");
241
+ }
218
242
  return data.access_token;
219
243
  };
244
+ var getTokenEntra = async (clientId, clientSecret, tenantId, baseUrl) => {
245
+ if (!baseUrl) {
246
+ throw new Error(
247
+ "Entra ID authentication requires 'baseUrl' to be set in deployment configuration"
248
+ );
249
+ }
250
+ const cluster = extractClusterFromUrl(baseUrl);
251
+ if (!cluster) {
252
+ throw new Error(
253
+ `Entra ID authentication requires 'baseUrl' to be a valid CDF URL (e.g., https://cluster.cognitedata.com), got: ${baseUrl}`
254
+ );
255
+ }
256
+ const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
257
+ const scope = `https://${cluster}.cognitedata.com/.default`;
258
+ const response = await fetch(tokenUrl, {
259
+ method: "POST",
260
+ headers: {
261
+ "Content-Type": "application/x-www-form-urlencoded"
262
+ },
263
+ body: new URLSearchParams({
264
+ client_id: clientId,
265
+ client_secret: clientSecret,
266
+ scope,
267
+ grant_type: "client_credentials"
268
+ })
269
+ });
270
+ if (!response.ok) {
271
+ const errorText = await response.text();
272
+ throw new Error(
273
+ `Failed to get token from Entra ID: ${response.status} ${response.statusText}
274
+ ${errorText}`
275
+ );
276
+ }
277
+ const data = await response.json();
278
+ if (!data.access_token) {
279
+ throw new Error("No access token returned from Entra ID authentication");
280
+ }
281
+ return data.access_token;
282
+ };
283
+ var getToken = async (deployment) => {
284
+ const {
285
+ deployClientId,
286
+ deploySecretName,
287
+ idpType = "cdf",
288
+ tenantId,
289
+ baseUrl
290
+ } = deployment;
291
+ const deploySecret = getSecretFromEnv(deploySecretName);
292
+ if (idpType === "entra_id") {
293
+ if (!tenantId) {
294
+ throw new Error(
295
+ "Entra ID authentication requires 'tenantId' in deployment configuration"
296
+ );
297
+ }
298
+ return getTokenEntra(deployClientId, deploySecret, tenantId, baseUrl);
299
+ }
300
+ return getTokenCdf(deployClientId, deploySecret);
301
+ };
220
302
 
221
303
  // src/deploy/get-sdk.ts
222
304
  var getSdk = async (deployment, folder) => {
223
- const token = await getToken(deployment.deployClientId, deployment.deploySecretName);
305
+ const token = await getToken(deployment);
224
306
  const sdk = new CogniteClient({
225
307
  appId: folder,
226
308
  project: deployment.project,
@@ -5,6 +5,7 @@ interface ViteDevServer {
5
5
  } | string | null;
6
6
  on: (event: string, callback: () => void) => void;
7
7
  } | null;
8
+ printUrls: () => void;
8
9
  }
9
10
  declare const fusionOpenPlugin: () => {
10
11
  name: string;
@@ -14,7 +14,7 @@ var fusionOpenPlugin = () => {
14
14
  return {
15
15
  name: "fusion-open",
16
16
  configureServer(server) {
17
- server.httpServer?.on("listening", () => {
17
+ server.printUrls = () => {
18
18
  const address = server.httpServer?.address();
19
19
  const port = address && typeof address === "object" ? address.port : 3001;
20
20
  const appJsonPath = path.join(process.cwd(), "app.json");
@@ -26,13 +26,16 @@ var fusionOpenPlugin = () => {
26
26
  const parsedBaseUrl = baseUrl?.split("//")[1];
27
27
  if (org && project && baseUrl) {
28
28
  const fusionUrl = `https://${org}.fusion.cognite.com/${project}/streamlit-apps/dune/development/${port}?cluster=${parsedBaseUrl}&workspace=industrial-tools`;
29
+ console.log(` \u279C Fusion: ${fusionUrl}`);
29
30
  openUrl(fusionUrl);
31
+ return;
30
32
  }
31
33
  } catch (error) {
32
34
  console.warn("Failed to read app.json for Fusion URL", error);
33
35
  }
34
36
  }
35
- });
37
+ console.warn(" \u279C No valid app.json found \u2014 cannot determine Fusion URL");
38
+ };
36
39
  }
37
40
  };
38
41
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cognite/dune",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Build and deploy React apps to Cognite Data Fusion",
5
5
  "keywords": ["cognite", "dune", "cdf", "fusion", "react", "scaffold", "deploy"],
6
6
  "license": "Apache-2.0",
@@ -4,7 +4,7 @@ import { getToken } from './login';
4
4
  import type { Deployment } from './types';
5
5
 
6
6
  export const getSdk = async (deployment: Deployment, folder: string) => {
7
- const token = await getToken(deployment.deployClientId, deployment.deploySecretName);
7
+ const token = await getToken(deployment);
8
8
  const sdk = new CogniteClient({
9
9
  appId: folder,
10
10
  project: deployment.project,
@@ -0,0 +1,49 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { extractClusterFromUrl } from "./login";
3
+
4
+ describe("extractClusterFromUrl", () => {
5
+ it("extracts cluster name from standard CDF URL", () => {
6
+ expect(extractClusterFromUrl("https://az-ams-sp-002.cognitedata.com")).toBe(
7
+ "az-ams-sp-002"
8
+ );
9
+ });
10
+
11
+ it("extracts cluster name from URL with path", () => {
12
+ expect(
13
+ extractClusterFromUrl("https://westeurope-1.cognitedata.com/api/v1")
14
+ ).toBe("westeurope-1");
15
+ });
16
+
17
+ it("extracts cluster name from greenfield URL", () => {
18
+ expect(
19
+ extractClusterFromUrl("https://greenfield.cognitedata.com")
20
+ ).toBe("greenfield");
21
+ });
22
+
23
+ it("handles URL with port", () => {
24
+ expect(
25
+ extractClusterFromUrl("https://az-ams-sp-002.cognitedata.com:443")
26
+ ).toBe("az-ams-sp-002");
27
+ });
28
+
29
+ it("handles URL without protocol", () => {
30
+ expect(extractClusterFromUrl("az-ams-sp-002.cognitedata.com")).toBe(
31
+ "az-ams-sp-002"
32
+ );
33
+ });
34
+
35
+ it("returns empty string for empty input", () => {
36
+ expect(extractClusterFromUrl("")).toBe("");
37
+ });
38
+
39
+ it("handles malformed URL gracefully", () => {
40
+ // Should use fallback string manipulation
41
+ expect(extractClusterFromUrl("not-a-valid-url")).toBe("not-a-valid-url");
42
+ });
43
+
44
+ it("extracts cluster from http URL", () => {
45
+ expect(extractClusterFromUrl("http://az-ams-sp-002.cognitedata.com")).toBe(
46
+ "az-ams-sp-002"
47
+ );
48
+ });
49
+ });
@@ -1,3 +1,5 @@
1
+ import type { Deployment } from "./types";
2
+
1
3
  /**
2
4
  * Load secrets from DEPLOYMENT_SECRETS environment variable (JSON)
3
5
  */
@@ -27,7 +29,10 @@ const loadSecretsFromEnv = (): Record<string, string> => {
27
29
  }
28
30
  };
29
31
 
30
- export const getToken = async (deployClientId: string, deploySecretName: string) => {
32
+ /**
33
+ * Get the deployment secret from environment variables
34
+ */
35
+ const getSecretFromEnv = (secretEnvVarName: string): string => {
31
36
  let deploySecret: string | undefined;
32
37
 
33
38
  // First try DEPLOYMENT_SECRET (for matrix-based deployments)
@@ -38,21 +43,54 @@ export const getToken = async (deployClientId: string, deploySecretName: string)
38
43
  // Then try to get from DEPLOYMENT_SECRETS JSON
39
44
  if (!deploySecret) {
40
45
  const secrets = loadSecretsFromEnv();
41
- deploySecret = secrets[deploySecretName];
46
+ deploySecret = secrets[secretEnvVarName];
42
47
  }
43
48
 
44
49
  // Fall back to direct environment variable for local development
45
50
  if (!deploySecret) {
46
- deploySecret = process.env[deploySecretName];
51
+ deploySecret = process.env[secretEnvVarName];
47
52
  }
48
53
 
49
54
  if (!deploySecret) {
50
- throw new Error(`Deployment secret not found in environment: ${deploySecretName}`);
55
+ throw new Error(`Secret not found in environment: ${secretEnvVarName}`);
51
56
  }
52
57
 
53
- const header = `Basic ${btoa(`${deployClientId}:${deploySecret}`)}`;
54
- const response = await fetch('https://auth.cognite.com/oauth2/token', {
55
- method: 'POST',
58
+ return deploySecret;
59
+ };
60
+
61
+ /**
62
+ * Extract cluster name from base URL
63
+ * @param url - Full URL like "https://az-ams-sp-002.cognitedata.com"
64
+ * @returns Cluster name like "az-ams-sp-002"
65
+ */
66
+ export const extractClusterFromUrl = (url: string): string => {
67
+ if (!url) return "";
68
+
69
+ try {
70
+ const urlObj = new URL(url);
71
+ const hostname = urlObj.hostname;
72
+ // Remove .cognitedata.com suffix
73
+ return hostname.replace(/\.cognitedata\.com$/, "");
74
+ } catch {
75
+ // Fallback to simple string manipulation
76
+ let cluster = url.replace(/^https?:\/\//, "");
77
+ cluster = cluster.split("/")[0]; // Remove path
78
+ cluster = cluster.split(":")[0]; // Remove port
79
+ cluster = cluster.replace(/\.cognitedata\.com$/, "");
80
+ return cluster;
81
+ }
82
+ };
83
+
84
+ /**
85
+ * Get access token using CDF OAuth provider
86
+ */
87
+ const getTokenCdf = async (
88
+ clientId: string,
89
+ clientSecret: string
90
+ ): Promise<string> => {
91
+ const header = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
92
+ const response = await fetch("https://auth.cognite.com/oauth2/token", {
93
+ method: "POST",
56
94
  headers: {
57
95
  Authorization: header,
58
96
  'Content-Type': 'application/x-www-form-urlencoded',
@@ -61,9 +99,94 @@ export const getToken = async (deployClientId: string, deploySecretName: string)
61
99
  });
62
100
 
63
101
  if (!response.ok) {
64
- throw new Error(`Failed to get token: ${response.status} ${response.statusText}`);
102
+ const errorText = await response.text();
103
+ throw new Error(
104
+ `Failed to get token from CDF: ${response.status} ${response.statusText}\n${errorText}`
105
+ );
65
106
  }
66
107
 
67
- const data = await response.json();
108
+ const data = (await response.json()) satisfies { access_token?: string };
109
+ if (!data.access_token) {
110
+ throw new Error("No access token returned from CDF authentication");
111
+ }
68
112
  return data.access_token;
69
113
  };
114
+
115
+ /**
116
+ * Get access token using Entra ID (Azure AD) OAuth provider
117
+ */
118
+ const getTokenEntra = async (
119
+ clientId: string,
120
+ clientSecret: string,
121
+ tenantId: string,
122
+ baseUrl: string
123
+ ): Promise<string> => {
124
+ if (!baseUrl) {
125
+ throw new Error(
126
+ "Entra ID authentication requires 'baseUrl' to be set in deployment configuration"
127
+ );
128
+ }
129
+ const cluster = extractClusterFromUrl(baseUrl);
130
+ if (!cluster) {
131
+ throw new Error(
132
+ `Entra ID authentication requires 'baseUrl' to be a valid CDF URL (e.g., https://cluster.cognitedata.com), got: ${baseUrl}`
133
+ );
134
+ }
135
+
136
+ const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
137
+ const scope = `https://${cluster}.cognitedata.com/.default`;
138
+
139
+ const response = await fetch(tokenUrl, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/x-www-form-urlencoded",
143
+ },
144
+ body: new URLSearchParams({
145
+ client_id: clientId,
146
+ client_secret: clientSecret,
147
+ scope: scope,
148
+ grant_type: "client_credentials",
149
+ }),
150
+ });
151
+
152
+ if (!response.ok) {
153
+ const errorText = await response.text();
154
+ throw new Error(
155
+ `Failed to get token from Entra ID: ${response.status} ${response.statusText}\n${errorText}`
156
+ );
157
+ }
158
+
159
+ const data = (await response.json()) satisfies { access_token?: string };
160
+ if (!data.access_token) {
161
+ throw new Error("No access token returned from Entra ID authentication");
162
+ }
163
+ return data.access_token;
164
+ };
165
+
166
+ /**
167
+ * Get access token for deployment using the appropriate identity provider.
168
+ * Supports both CDF OAuth and Entra ID (Azure AD) authentication.
169
+ */
170
+ export const getToken = async (deployment: Deployment): Promise<string> => {
171
+ const {
172
+ deployClientId,
173
+ deploySecretName,
174
+ idpType = "cdf",
175
+ tenantId,
176
+ baseUrl,
177
+ } = deployment;
178
+
179
+ const deploySecret = getSecretFromEnv(deploySecretName);
180
+
181
+ if (idpType === "entra_id") {
182
+ if (!tenantId) {
183
+ throw new Error(
184
+ "Entra ID authentication requires 'tenantId' in deployment configuration"
185
+ );
186
+ }
187
+ return getTokenEntra(deployClientId, deploySecret, tenantId, baseUrl);
188
+ }
189
+
190
+ // Default: CDF provider
191
+ return getTokenCdf(deployClientId, deploySecret);
192
+ };
@@ -5,6 +5,10 @@ export type Deployment = {
5
5
  deployClientId: string;
6
6
  deploySecretName: string;
7
7
  published: boolean;
8
+ /** Identity provider type. Defaults to "cdf" if not specified */
9
+ idpType?: "cdf" | "entra_id";
10
+ /** Tenant ID for Entra ID authentication. Required when idpType is "entra_id" */
11
+ tenantId?: string;
8
12
  };
9
13
 
10
14
  export type App = {
@@ -17,13 +17,14 @@ interface ViteDevServer {
17
17
  address: () => { port: number } | string | null;
18
18
  on: (event: string, callback: () => void) => void;
19
19
  } | null;
20
+ printUrls: () => void;
20
21
  }
21
22
 
22
23
  export const fusionOpenPlugin = () => {
23
24
  return {
24
25
  name: 'fusion-open',
25
26
  configureServer(server: ViteDevServer) {
26
- server.httpServer?.on('listening', () => {
27
+ server.printUrls = () => {
27
28
  const address = server.httpServer?.address();
28
29
  const port = address && typeof address === 'object' ? address.port : 3001;
29
30
 
@@ -33,19 +34,20 @@ export const fusionOpenPlugin = () => {
33
34
  const appJson = JSON.parse(fs.readFileSync(appJsonPath, 'utf-8'));
34
35
  const firstDeployment = appJson.deployments?.[0];
35
36
  const { org, project, baseUrl } = firstDeployment || {};
36
-
37
37
  const parsedBaseUrl = baseUrl?.split('//')[1];
38
38
 
39
39
  if (org && project && baseUrl) {
40
40
  const fusionUrl = `https://${org}.fusion.cognite.com/${project}/streamlit-apps/dune/development/${port}?cluster=${parsedBaseUrl}&workspace=industrial-tools`;
41
-
41
+ console.log(` ➜ Fusion: ${fusionUrl}`);
42
42
  openUrl(fusionUrl);
43
+ return;
43
44
  }
44
45
  } catch (error) {
45
46
  console.warn('Failed to read app.json for Fusion URL', error);
46
47
  }
47
48
  }
48
- });
49
+ console.warn(' ➜ No valid app.json found — cannot determine Fusion URL');
50
+ };
49
51
  },
50
52
  };
51
53
  };