@cognite/dune 0.3.1 → 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.
Files changed (52) hide show
  1. package/_templates/app/new/config/eslint.config.mjs.ejs.t +96 -0
  2. package/_templates/app/new/config/tailwind.config.js.ejs.t +1 -5
  3. package/_templates/app/new/config/vite.config.ts.ejs.t +9 -10
  4. package/_templates/app/new/config/vitest.config.ts.ejs.t +4 -5
  5. package/_templates/app/new/config/vitest.setup.ts.ejs.t +1 -2
  6. package/_templates/app/new/cursor/mcp.json.ejs.t +0 -5
  7. package/_templates/app/new/cursor/rules.mdc.ejs.t +1 -2
  8. package/_templates/app/new/prompt.js +29 -29
  9. package/_templates/app/new/root/index.html.ejs.t +3 -3
  10. package/_templates/app/new/root/package.json.ejs.t +11 -5
  11. package/_templates/app/new/src/App.test.tsx.ejs.t +32 -20
  12. package/_templates/app/new/src/App.tsx.ejs.t +118 -7
  13. package/_templates/app/new/src/lib/utils.ts.ejs.t +2 -3
  14. package/_templates/app/new/src/main.tsx.ejs.t +8 -8
  15. package/_templates/app/new/src/styles.css.ejs.t +5 -19
  16. package/bin/auth/authentication-flow.js +16 -14
  17. package/bin/auth/callback-server.js +23 -23
  18. package/bin/auth/certificate-manager.js +13 -13
  19. package/bin/auth/client-credentials.js +31 -32
  20. package/bin/auth/oauth-client.js +7 -7
  21. package/bin/cli.js +31 -30
  22. package/bin/deploy-command.js +32 -32
  23. package/bin/deploy-interactive-command.js +73 -73
  24. package/bin/skills-command.js +28 -28
  25. package/bin/utils/crypto.js +7 -8
  26. package/dist/auth/index.d.ts +10 -13
  27. package/dist/auth/index.js +1 -1
  28. package/dist/{chunk-VIBN7U5H.js → chunk-53VTKDSC.js} +1 -2
  29. package/dist/deploy/index.d.ts +9 -1
  30. package/dist/deploy/index.js +89 -7
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +1 -1
  33. package/dist/vite/index.d.ts +1 -0
  34. package/dist/vite/index.js +5 -2
  35. package/package.json +1 -1
  36. package/src/auth/dune-auth-provider.tsx +17 -16
  37. package/src/auth/index.ts +5 -5
  38. package/src/auth/use-dune.ts +5 -4
  39. package/src/auth/utils.ts +18 -18
  40. package/src/deploy/application-deployer.ts +12 -11
  41. package/src/deploy/application-packager.ts +11 -10
  42. package/src/deploy/deploy.ts +7 -6
  43. package/src/deploy/get-sdk.ts +5 -4
  44. package/src/deploy/index.ts +6 -6
  45. package/src/deploy/login.test.ts +49 -0
  46. package/src/deploy/login.ts +134 -11
  47. package/src/deploy/types.ts +4 -0
  48. package/src/index.ts +1 -1
  49. package/src/vite/fusion-open-plugin.ts +17 -15
  50. package/src/vite/index.ts +1 -1
  51. package/_templates/app/new/config/biome.json.ejs.t +0 -54
  52. package/_templates/app/new/config/components.json.ejs.t +0 -28
@@ -1,8 +1,9 @@
1
- import fs from "node:fs";
2
- import { CdfApplicationDeployer } from "./application-deployer";
3
- import { ApplicationPackager } from "./application-packager";
4
- import { getSdk } from "./get-sdk";
5
- import type { App, Deployment } from "./types";
1
+ import fs from 'node:fs';
2
+
3
+ import { CdfApplicationDeployer } from './application-deployer';
4
+ import { ApplicationPackager } from './application-packager';
5
+ import { getSdk } from './get-sdk';
6
+ import type { App, Deployment } from './types';
6
7
 
7
8
  export const deploy = async (deployment: Deployment, app: App, folder: string) => {
8
9
  // Step 1: Get an SDK instance
@@ -11,7 +12,7 @@ export const deploy = async (deployment: Deployment, app: App, folder: string) =
11
12
  // Step 2: Package application (from the dist subdirectory)
12
13
  const distPath = `${folder}/dist`;
13
14
  const packager = new ApplicationPackager(distPath);
14
- const zipFilename = await packager.createZip("app.zip", true);
15
+ const zipFilename = await packager.createZip('app.zip', true);
15
16
 
16
17
  // Step 3: Deploy to CDF
17
18
  const deployer = new CdfApplicationDeployer(sdk);
@@ -1,9 +1,10 @@
1
- import { CogniteClient } from "@cognite/sdk";
2
- import { getToken } from "./login";
3
- import type { Deployment } from "./types";
1
+ import { CogniteClient } from '@cognite/sdk';
2
+
3
+ import { getToken } from './login';
4
+ import type { Deployment } from './types';
4
5
 
5
6
  export const getSdk = async (deployment: Deployment, folder: string) => {
6
- const token = await getToken(deployment.deployClientId, deployment.deploySecretName);
7
+ const token = await getToken(deployment);
7
8
  const sdk = new CogniteClient({
8
9
  appId: folder,
9
10
  project: deployment.project,
@@ -1,9 +1,9 @@
1
1
  // Deploy exports for CI/CD and programmatic deployment
2
- export { deploy } from "./deploy";
3
- export { CdfApplicationDeployer } from "./application-deployer";
4
- export { ApplicationPackager } from "./application-packager";
5
- export { getSdk } from "./get-sdk";
6
- export { getToken } from "./login";
2
+ export { deploy } from './deploy';
3
+ export { CdfApplicationDeployer } from './application-deployer';
4
+ export { ApplicationPackager } from './application-packager';
5
+ export { getSdk } from './get-sdk';
6
+ export { getToken } from './login';
7
7
 
8
8
  // Type exports
9
- export type { Deployment, App } from "./types";
9
+ export type { Deployment, App } from './types';
@@ -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
  */
@@ -13,21 +15,24 @@ const loadSecretsFromEnv = (): Record<string, string> => {
13
15
  const normalizedSecrets: Record<string, string> = {};
14
16
 
15
17
  for (const [key, value] of Object.entries(secrets)) {
16
- if (typeof value === "string") {
18
+ if (typeof value === 'string') {
17
19
  // Convert UPPER_CASE to lower-case-with-dashes
18
- const normalizedKey = key.toLowerCase().replace(/_/g, "-");
20
+ const normalizedKey = key.toLowerCase().replace(/_/g, '-');
19
21
  normalizedSecrets[normalizedKey] = value;
20
22
  }
21
23
  }
22
24
 
23
25
  return normalizedSecrets;
24
26
  } catch (error) {
25
- console.error("Error parsing DEPLOYMENT_SECRETS:", error);
27
+ console.error('Error parsing DEPLOYMENT_SECRETS:', error);
26
28
  return {};
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,32 +43,150 @@ 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}`)}`;
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}`)}`;
54
92
  const response = await fetch("https://auth.cognite.com/oauth2/token", {
55
93
  method: "POST",
56
94
  headers: {
57
95
  Authorization: header,
96
+ 'Content-Type': 'application/x-www-form-urlencoded',
97
+ },
98
+ body: new URLSearchParams({ grant_type: 'client_credentials' }),
99
+ });
100
+
101
+ if (!response.ok) {
102
+ const errorText = await response.text();
103
+ throw new Error(
104
+ `Failed to get token from CDF: ${response.status} ${response.statusText}\n${errorText}`
105
+ );
106
+ }
107
+
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
+ }
112
+ return data.access_token;
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: {
58
142
  "Content-Type": "application/x-www-form-urlencoded",
59
143
  },
60
- body: new URLSearchParams({ grant_type: "client_credentials" }),
144
+ body: new URLSearchParams({
145
+ client_id: clientId,
146
+ client_secret: clientSecret,
147
+ scope: scope,
148
+ grant_type: "client_credentials",
149
+ }),
61
150
  });
62
151
 
63
152
  if (!response.ok) {
64
- throw new Error(`Failed to get token: ${response.status} ${response.statusText}`);
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
+ );
65
157
  }
66
158
 
67
- const data = await response.json();
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
+ }
68
163
  return data.access_token;
69
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 = {
package/src/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Main entry point - re-exports auth for convenience
2
- export * from "./auth";
2
+ export * from './auth';
@@ -1,11 +1,11 @@
1
- import { exec } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
1
+ import { exec } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
4
 
5
5
  const openUrl = (url: string) => {
6
6
  const start =
7
- process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
8
- if (process.platform === "win32") {
7
+ process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
8
+ if (process.platform === 'win32') {
9
9
  exec(`start "" "${url}"`);
10
10
  } else {
11
11
  exec(`${start} "${url}"`);
@@ -17,35 +17,37 @@ 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
- name: "fusion-open",
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
- const port = address && typeof address === "object" ? address.port : 3001;
29
+ const port = address && typeof address === 'object' ? address.port : 3001;
29
30
 
30
- const appJsonPath = path.join(process.cwd(), "app.json");
31
+ const appJsonPath = path.join(process.cwd(), 'app.json');
31
32
  if (fs.existsSync(appJsonPath)) {
32
33
  try {
33
- const appJson = JSON.parse(fs.readFileSync(appJsonPath, "utf-8"));
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
- const parsedBaseUrl = baseUrl?.split("//")[1];
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
- console.warn("Failed to read app.json for Fusion URL", error);
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
  };
package/src/vite/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  // Vite plugin exports
2
- export { fusionOpenPlugin } from "./fusion-open-plugin";
2
+ export { fusionOpenPlugin } from './fusion-open-plugin';
@@ -1,54 +0,0 @@
1
- ---
2
- to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>biome.json'
3
- ---
4
- {
5
- "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
6
- "organizeImports": {
7
- "enabled": true
8
- },
9
- "linter": {
10
- "enabled": true,
11
- "rules": {
12
- "recommended": true,
13
- "suspicious": {
14
- "noExplicitAny": "off"
15
- },
16
- "style": {
17
- "noNonNullAssertion": "off"
18
- },
19
- "complexity": {
20
- "noForEach": "off"
21
- }
22
- }
23
- },
24
- "formatter": {
25
- "enabled": true,
26
- "indentStyle": "space",
27
- "indentWidth": 2,
28
- "lineWidth": 100
29
- },
30
- "javascript": {
31
- "formatter": {
32
- "quoteStyle": "double",
33
- "trailingCommas": "es5",
34
- "semicolons": "always"
35
- }
36
- },
37
- "json": {
38
- "formatter": {
39
- "enabled": true
40
- }
41
- },
42
- "files": {
43
- "ignore": [
44
- "node_modules",
45
- "dist",
46
- "build",
47
- ".next",
48
- "coverage",
49
- "*.min.js",
50
- "pnpm-lock.yaml"
51
- ]
52
- }
53
- }
54
-
@@ -1,28 +0,0 @@
1
- ---
2
- to: '<%= useCurrentDir ? "" : ((directoryName || name) + "/") %>components.json'
3
- ---
4
- {
5
- "$schema": "https://ui.shadcn.com/schema.json",
6
- "style": "new-york",
7
- "rsc": false,
8
- "tsx": true,
9
- "tailwind": {
10
- "config": "tailwind.config.js",
11
- "css": "src/styles.css",
12
- "baseColor": "zinc",
13
- "cssVariables": true,
14
- "prefix": ""
15
- },
16
- "iconLibrary": "lucide",
17
- "aliases": {
18
- "components": "@/components",
19
- "utils": "@/lib/utils",
20
- "ui": "@/components/ui",
21
- "lib": "@/lib",
22
- "hooks": "@/hooks"
23
- },
24
- "registries": {
25
- "@aura": "https://cognitedata.github.io/aura/r/{name}.json"
26
- }
27
- }
28
-