@cognite/dune 0.1.1 → 0.1.2

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.
@@ -19,7 +19,7 @@ to: <%= name %>/package.json
19
19
  },
20
20
  "dependencies": {
21
21
  "@cognite/sdk": "^10.3.0",
22
- "@cognite/dune": "^0.1.0",
22
+ "@cognite/dune": "^0.1.2",
23
23
  "@tanstack/react-query": "^5.90.10",
24
24
  "react": "^19.2.0",
25
25
  "react-dom": "^19.2.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cognite/dune",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Build and deploy React apps to Cognite Data Fusion",
5
5
  "keywords": [
6
6
  "cognite",
@@ -23,23 +23,26 @@
23
23
  "types": "./dist/index.d.ts",
24
24
  "exports": {
25
25
  ".": {
26
- "types": "./src/index.ts",
26
+ "types": "./dist/index.d.ts",
27
27
  "development": "./src/index.ts",
28
+ "import": "./dist/index.js",
28
29
  "default": "./dist/index.js"
29
30
  },
30
31
  "./auth": {
31
- "types": "./src/auth/index.ts",
32
+ "types": "./dist/auth/index.d.ts",
32
33
  "development": "./src/auth/index.ts",
34
+ "import": "./dist/auth/index.js",
33
35
  "default": "./dist/auth/index.js"
34
36
  },
35
37
  "./deploy": {
36
- "types": "./src/deploy/index.ts",
38
+ "types": "./dist/deploy/index.d.ts",
37
39
  "development": "./src/deploy/index.ts",
40
+ "import": "./dist/deploy/index.js",
38
41
  "default": "./dist/deploy/index.js"
39
42
  },
40
43
  "./vite": {
41
- "types": "./src/vite/index.ts",
42
- "development": "./src/vite/index.ts",
44
+ "types": "./dist/vite/index.d.ts",
45
+ "import": "./dist/vite/index.js",
43
46
  "default": "./dist/vite/index.js"
44
47
  }
45
48
  },
@@ -49,6 +52,7 @@
49
52
  "files": [
50
53
  "bin",
51
54
  "dist",
55
+ "src",
52
56
  "_templates"
53
57
  ],
54
58
  "dependencies": {
@@ -0,0 +1,97 @@
1
+ import { CogniteClient } from "@cognite/sdk";
2
+ import type React from "react";
3
+ import { createContext, useEffect } from "react";
4
+ import { useState } from "react";
5
+ import { EMPTY_SDK, handleCredentialsResponse, requestCredentials } from "./utils";
6
+
7
+ export const DuneAuthProviderContext = createContext<{
8
+ sdk: CogniteClient;
9
+ isLoading: boolean;
10
+ error?: string;
11
+ }>({
12
+ sdk: EMPTY_SDK,
13
+ isLoading: false,
14
+ });
15
+
16
+ export interface DuneAuthProviderProps {
17
+ children: React.ReactNode;
18
+ useIFrameAuthentication?: boolean;
19
+ useLocalConfiguration?: { org: string; project: string; baseUrl: string };
20
+ loadingComponent?: React.ReactNode;
21
+ errorComponent?: (error: string) => React.ReactNode;
22
+ }
23
+
24
+ export const DuneAuthProvider = ({
25
+ children,
26
+ loadingComponent = <div>Loading CDF authentication...</div>,
27
+ errorComponent = (error: string) => <div>Authentication error: {error}</div>,
28
+ }: DuneAuthProviderProps) => {
29
+ return (
30
+ <FusionIframeAuthenticationInnerProvider
31
+ loadingComponent={loadingComponent}
32
+ errorComponent={errorComponent}
33
+ >
34
+ {children}
35
+ </FusionIframeAuthenticationInnerProvider>
36
+ );
37
+ };
38
+
39
+ export const FusionIframeAuthenticationInnerProvider = ({
40
+ children,
41
+ loadingComponent = <div>Loading CDF authentication...</div>,
42
+ errorComponent = (error: string) => <div>Authentication error: {error}</div>,
43
+ }: DuneAuthProviderProps) => {
44
+ const [isLoading, setIsLoading] = useState(true);
45
+ const [sdk, setSdk] = useState<CogniteClient>(EMPTY_SDK);
46
+ const [error] = useState<string>();
47
+
48
+ // Within fusion: listen for messages from parent application (Fusion)
49
+ useEffect(() => {
50
+ requestCredentials();
51
+
52
+ const handleMessage = async (event: MessageEvent) => {
53
+ console.log("🔍 Handling message from Fusion");
54
+
55
+ const credentials = handleCredentialsResponse(event);
56
+
57
+ if (!credentials) {
58
+ return; // Ignore non-credential messages
59
+ }
60
+
61
+ // Process credentials (initial or refresh)
62
+ const sdk = new CogniteClient({
63
+ appId: "dune-app",
64
+ project: credentials.project,
65
+ baseUrl: credentials.baseUrl,
66
+ oidcTokenProvider: async () => {
67
+ return credentials.token;
68
+ },
69
+ });
70
+
71
+ await sdk.authenticate();
72
+
73
+ setSdk(sdk);
74
+ setIsLoading(false);
75
+ };
76
+
77
+ console.log("🔍 Adding message listener");
78
+ window.addEventListener("message", handleMessage);
79
+ return () => window.removeEventListener("message", handleMessage);
80
+ }, []);
81
+
82
+ console.log("🔍 CDFIframeAuthenticationInnerProvider", sdk, isLoading, error);
83
+
84
+ if (error && errorComponent) {
85
+ return <>{errorComponent(error)}</>;
86
+ }
87
+
88
+ if (isLoading && loadingComponent) {
89
+ return <>{loadingComponent}</>;
90
+ }
91
+
92
+ return (
93
+ <DuneAuthProviderContext.Provider value={{ sdk, isLoading, error }}>
94
+ {children}
95
+ </DuneAuthProviderContext.Provider>
96
+ );
97
+ };
@@ -0,0 +1,11 @@
1
+ // Auth exports for React applications
2
+ export {
3
+ DuneAuthProvider,
4
+ DuneAuthProviderContext,
5
+ } from "./dune-auth-provider";
6
+ export { useDune } from "./use-dune";
7
+ export { getToken, createCDFSDK, EMPTY_SDK } from "./utils";
8
+
9
+ // Type exports
10
+ export type { CDFConfig } from "./utils";
11
+ export type { DuneAuthProviderProps } from "./dune-auth-provider";
@@ -0,0 +1,12 @@
1
+ import { useContext } from "react";
2
+ import { DuneAuthProviderContext } from "./dune-auth-provider";
3
+
4
+ export const useDune = () => {
5
+ const context = useContext(DuneAuthProviderContext);
6
+
7
+ if (!context) {
8
+ throw new Error("useDune must be used within a DuneAuthProvider");
9
+ }
10
+
11
+ return context;
12
+ };
@@ -0,0 +1,91 @@
1
+ import { CogniteClient } from "@cognite/sdk";
2
+
3
+ export const MESSAGE_TYPES = {
4
+ APP_HOST_READY: "APP_HOST_READY",
5
+ APP_READY: "APP_READY",
6
+ CREDENTIALS: "CREDENTIALS",
7
+ REQUEST_CREDENTIALS: "REQUEST_CREDENTIALS",
8
+ } as const;
9
+
10
+ export interface CDFConfig {
11
+ project: string;
12
+ baseUrl: string;
13
+ clientId: string;
14
+ clientSecret: string;
15
+ appId?: string;
16
+ }
17
+
18
+ export const getToken = async (clientId: string, clientSecret: string) => {
19
+ const header = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
20
+
21
+ const response = await fetch("https://auth.cognite.com/oauth2/token", {
22
+ method: "POST",
23
+ headers: {
24
+ Authorization: header,
25
+ "Content-Type": "application/x-www-form-urlencoded",
26
+ },
27
+ body: new URLSearchParams({
28
+ grant_type: "client_credentials",
29
+ }),
30
+ });
31
+
32
+ const data = await response.json();
33
+ return data.access_token;
34
+ };
35
+
36
+ export const createCDFSDK = async (config: CDFConfig) => {
37
+ const { project, baseUrl, clientId, clientSecret, appId = "cdf-authentication-package" } = config;
38
+
39
+ if (!project || !baseUrl || !clientId || !clientSecret) {
40
+ throw new Error(
41
+ "Missing required configuration. Please provide: project, baseUrl, clientId, clientSecret"
42
+ );
43
+ }
44
+
45
+ const sdk = new CogniteClient({
46
+ appId,
47
+ project,
48
+ baseUrl,
49
+ oidcTokenProvider: async () => {
50
+ return await getToken(clientId, clientSecret);
51
+ },
52
+ });
53
+
54
+ await sdk.authenticate();
55
+ return sdk;
56
+ };
57
+
58
+ export const EMPTY_SDK = new CogniteClient({
59
+ appId: "cdf-authentication-package",
60
+ project: "",
61
+ oidcTokenProvider: async () => "",
62
+ });
63
+
64
+ interface Credentials {
65
+ project: string;
66
+ baseUrl: string;
67
+ token: string;
68
+ }
69
+
70
+ export const requestCredentials = () => {
71
+ console.log("🔑 Requesting credentials from parent...");
72
+ if (window.parent && window.parent !== window) {
73
+ window.parent.postMessage({ type: "REQUEST_CREDENTIALS" }, "*");
74
+ }
75
+ };
76
+
77
+ export const handleCredentialsResponse = (event: MessageEvent) => {
78
+ // Check if this is a credentials message (wrapped in type/credentials format)
79
+ if (event.data?.type === "PROVIDE_CREDENTIALS" && event.data?.credentials) {
80
+ const creds = event.data.credentials;
81
+ if (creds.token && creds.baseUrl && creds.project) {
82
+ console.log("🎉 useCredentials received credentials:", {
83
+ hasToken: !!creds.token,
84
+ tokenLength: creds.token?.length || 0,
85
+ project: creds.project,
86
+ baseUrl: creds.baseUrl,
87
+ });
88
+ return creds as Credentials;
89
+ }
90
+ }
91
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * CDF Application Deployment
3
+ *
4
+ * Handles deployment of packaged applications to CDF.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import type { CogniteClient } from "@cognite/sdk";
9
+
10
+ export class CdfApplicationDeployer {
11
+ private client: CogniteClient;
12
+
13
+ /**
14
+ * @param {CogniteClient} client - Cognite SDK client
15
+ */
16
+ constructor(client: CogniteClient) {
17
+ this.client = client;
18
+ }
19
+
20
+ /**
21
+ * Upload application package to CDF Files API
22
+ * @param {string} appExternalId - Application external ID
23
+ * @param {string} name - Application name
24
+ * @param {string} description - Application description
25
+ * @param {string} versionTag - Version tag
26
+ * @param {string} zipFilename - Path to zip file
27
+ * @param {boolean} published - Whether the application should be published
28
+ */
29
+ async uploadToFilesApi(
30
+ appExternalId: string,
31
+ name: string,
32
+ description: string,
33
+ versionTag: string,
34
+ zipFilename: string,
35
+ published = false
36
+ ): Promise<void> {
37
+ console.log("📁 Creating file record...");
38
+
39
+ const fileContent = fs.readFileSync(zipFilename);
40
+ const metadata = {
41
+ published: String(published),
42
+ name: name,
43
+ description: description,
44
+ externalId: `${appExternalId}-${versionTag}`,
45
+ version: versionTag,
46
+ };
47
+
48
+ await this.client.files.upload(
49
+ {
50
+ name: `${appExternalId}-${versionTag}.zip`,
51
+ externalId: `${appExternalId}-${versionTag}`,
52
+ directory: "/dune-apps",
53
+ metadata: metadata,
54
+ },
55
+ fileContent,
56
+ true, // overwrite
57
+ true // waitUntilAcknowledged
58
+ );
59
+
60
+ console.log("✅ File record created");
61
+ }
62
+
63
+ /**
64
+ * Execute complete deployment to CDF
65
+ * @param {string} appExternalId - Application external ID
66
+ * @param {string} name - Application name
67
+ * @param {string} description - Application description
68
+ * @param {string} versionTag - Version tag
69
+ * @param {string} zipFilename - Path to zip file
70
+ * @param {boolean} published - Whether the application should be published
71
+ */
72
+ async deploy(
73
+ appExternalId: string,
74
+ name: string,
75
+ description: string,
76
+ versionTag: string,
77
+ zipFilename: string,
78
+ published = false
79
+ ): Promise<void> {
80
+ console.log("\n🚀 Deploying application to CDF...\n");
81
+
82
+ try {
83
+ // Upload to Files API
84
+ await this.uploadToFilesApi(
85
+ appExternalId,
86
+ name,
87
+ description,
88
+ versionTag,
89
+ zipFilename,
90
+ published
91
+ );
92
+
93
+ console.log("\n✅ Deployment successful!");
94
+ } catch (error: unknown) {
95
+ const message = error instanceof Error ? error.message : String(error);
96
+ throw new Error(`Deployment failed: ${message}`);
97
+ }
98
+ }
99
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Application Packaging
3
+ *
4
+ * Handles packaging of build directories into deployment-ready zip files.
5
+ */
6
+
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import archiver from "archiver";
10
+
11
+ export class ApplicationPackager {
12
+ private distPath: string;
13
+
14
+ /**
15
+ * @param {string} distDirectory - Build directory to package (can be relative or absolute)
16
+ */
17
+ constructor(distDirectory = "dist") {
18
+ // If distDirectory is already an absolute path, use it as-is
19
+ // Otherwise, join it with the current working directory
20
+ this.distPath = path.isAbsolute(distDirectory)
21
+ ? distDirectory
22
+ : path.join(process.cwd(), distDirectory);
23
+ }
24
+
25
+ /**
26
+ * Validate that build directory exists
27
+ * @throws {Error} If build directory not found
28
+ */
29
+ validateBuildDirectory(): void {
30
+ if (!fs.existsSync(this.distPath)) {
31
+ throw new Error(`Build directory "${this.distPath}" not found. Run build first.`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Create zip file from build directory
37
+ * @param {string} outputFilename - Output zip filename
38
+ * @param {boolean} verbose - Enable verbose logging
39
+ * @returns {Promise<string>} Path to created zip file
40
+ */
41
+ async createZip(outputFilename = "app.zip", verbose = false): Promise<string> {
42
+ this.validateBuildDirectory();
43
+
44
+ console.log("📦 Packaging application...");
45
+
46
+ return new Promise((resolve, reject) => {
47
+ const output = fs.createWriteStream(outputFilename);
48
+ const archive = archiver("zip", {
49
+ zlib: { level: 9 }, // Maximum compression
50
+ });
51
+
52
+ output.on("close", () => {
53
+ const sizeMB = (archive.pointer() / 1024 / 1024).toFixed(2);
54
+ console.log(`✅ App packaged: ${outputFilename} (${sizeMB} MB)`);
55
+ resolve(outputFilename);
56
+ });
57
+
58
+ archive.on("error", (err: Error) => {
59
+ reject(new Error(`Failed to create zip: ${err.message}`));
60
+ });
61
+
62
+ if (verbose) {
63
+ archive.on("entry", (entry: archiver.EntryData) => {
64
+ console.log(` 📄 ${entry.name}`);
65
+ });
66
+ }
67
+
68
+ archive.pipe(output);
69
+ archive.directory(this.distPath, false);
70
+ archive.finalize();
71
+ });
72
+ }
73
+ }
@@ -0,0 +1,25 @@
1
+ import { CdfApplicationDeployer } from "./application-deployer";
2
+ import { ApplicationPackager } from "./application-packager";
3
+ import { getSdk } from "./get-sdk";
4
+ import type { App, Deployment } from "./types";
5
+
6
+ export const deploy = async (deployment: Deployment, app: App, folder: string) => {
7
+ // Step 1: Get an SDK instance
8
+ const sdk = await getSdk(deployment, folder);
9
+
10
+ // Step 2: Package application (from the dist subdirectory)
11
+ const distPath = `${folder}/dist`;
12
+ const packager = new ApplicationPackager(distPath);
13
+ const zipFilename = await packager.createZip("app.zip", true);
14
+
15
+ // Step 3: Deploy to CDF
16
+ const deployer = new CdfApplicationDeployer(sdk);
17
+ await deployer.deploy(
18
+ app.externalId,
19
+ app.name,
20
+ app.description,
21
+ app.versionTag,
22
+ zipFilename,
23
+ deployment.published
24
+ );
25
+ };
@@ -0,0 +1,17 @@
1
+ import { CogniteClient } from "@cognite/sdk";
2
+ import { getToken } from "./login";
3
+ import type { Deployment } from "./types";
4
+
5
+ export const getSdk = async (deployment: Deployment, folder: string) => {
6
+ const token = await getToken(deployment.deployClientId, deployment.deploySecretName);
7
+ const sdk = new CogniteClient({
8
+ appId: folder,
9
+ project: deployment.project,
10
+ baseUrl: deployment.baseUrl,
11
+ oidcTokenProvider: async () => {
12
+ return token;
13
+ },
14
+ });
15
+ await sdk.authenticate();
16
+ return sdk;
17
+ };
@@ -0,0 +1,9 @@
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";
7
+
8
+ // Type exports
9
+ export type { Deployment, App } from "./types";
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Load secrets from DEPLOYMENT_SECRETS environment variable (JSON)
3
+ */
4
+ const loadSecretsFromEnv = (): Record<string, string> => {
5
+ const secretsJson = process.env.DEPLOYMENT_SECRETS;
6
+ if (!secretsJson) {
7
+ return {};
8
+ }
9
+
10
+ try {
11
+ const secrets = JSON.parse(secretsJson);
12
+ // Convert GitHub secret names (UPPER_CASE) to deployment secret names (lower-case)
13
+ const normalizedSecrets: Record<string, string> = {};
14
+
15
+ for (const [key, value] of Object.entries(secrets)) {
16
+ if (typeof value === "string") {
17
+ // Convert UPPER_CASE to lower-case-with-dashes
18
+ const normalizedKey = key.toLowerCase().replace(/_/g, "-");
19
+ normalizedSecrets[normalizedKey] = value;
20
+ }
21
+ }
22
+
23
+ return normalizedSecrets;
24
+ } catch (error) {
25
+ console.error("Error parsing DEPLOYMENT_SECRETS:", error);
26
+ return {};
27
+ }
28
+ };
29
+
30
+ export const getToken = async (deployClientId: string, deploySecretName: string) => {
31
+ let deploySecret: string | undefined;
32
+
33
+ // First try DEPLOYMENT_SECRET (for matrix-based deployments)
34
+ if (process.env.DEPLOYMENT_SECRET) {
35
+ deploySecret = process.env.DEPLOYMENT_SECRET;
36
+ }
37
+
38
+ // Then try to get from DEPLOYMENT_SECRETS JSON
39
+ if (!deploySecret) {
40
+ const secrets = loadSecretsFromEnv();
41
+ deploySecret = secrets[deploySecretName];
42
+ }
43
+
44
+ // Fall back to direct environment variable for local development
45
+ if (!deploySecret) {
46
+ deploySecret = process.env[deploySecretName];
47
+ }
48
+
49
+ if (!deploySecret) {
50
+ throw new Error(`Deployment secret not found in environment: ${deploySecretName}`);
51
+ }
52
+
53
+ const header = `Basic ${btoa(`${deployClientId}:${deploySecret}`)}`;
54
+ const response = await fetch("https://auth.cognite.com/oauth2/token", {
55
+ method: "POST",
56
+ headers: {
57
+ Authorization: header,
58
+ "Content-Type": "application/x-www-form-urlencoded",
59
+ },
60
+ body: new URLSearchParams({ grant_type: "client_credentials" }),
61
+ });
62
+
63
+ if (!response.ok) {
64
+ throw new Error(`Failed to get token: ${response.status} ${response.statusText}`);
65
+ }
66
+
67
+ const data = await response.json();
68
+ return data.access_token;
69
+ };
@@ -0,0 +1,15 @@
1
+ export type Deployment = {
2
+ org: string;
3
+ project: string;
4
+ baseUrl: string;
5
+ deployClientId: string;
6
+ deploySecretName: string;
7
+ published: boolean;
8
+ };
9
+
10
+ export type App = {
11
+ externalId: string;
12
+ name: string;
13
+ description: string;
14
+ versionTag: string;
15
+ };
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ // Main entry point - re-exports auth for convenience
2
+ export * from "./auth";
@@ -0,0 +1,51 @@
1
+ import { exec } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ const openUrl = (url: string) => {
6
+ const start =
7
+ process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
8
+ if (process.platform === "win32") {
9
+ exec(`start "" "${url}"`);
10
+ } else {
11
+ exec(`${start} "${url}"`);
12
+ }
13
+ };
14
+
15
+ interface ViteDevServer {
16
+ httpServer?: {
17
+ address: () => { port: number } | string | null;
18
+ on: (event: string, callback: () => void) => void;
19
+ } | null;
20
+ }
21
+
22
+ export const fusionOpenPlugin = () => {
23
+ return {
24
+ name: "fusion-open",
25
+ configureServer(server: ViteDevServer) {
26
+ server.httpServer?.on("listening", () => {
27
+ const address = server.httpServer?.address();
28
+ const port = address && typeof address === "object" ? address.port : 3000;
29
+
30
+ const appJsonPath = path.join(process.cwd(), "app.json");
31
+ if (fs.existsSync(appJsonPath)) {
32
+ try {
33
+ const appJson = JSON.parse(fs.readFileSync(appJsonPath, "utf-8"));
34
+ const firstDeployment = appJson.deployments?.[0];
35
+ const { org, project, baseUrl } = firstDeployment || {};
36
+
37
+ const parsedBaseUrl = baseUrl?.split("//")[1];
38
+
39
+ if (org && project && baseUrl) {
40
+ const fusionUrl = `https://${org}.fusion.cognite.com/${project}/streamlit-apps/dune/development/${port}?cluster=${parsedBaseUrl}&workspace=industrial-tools`;
41
+
42
+ openUrl(fusionUrl);
43
+ }
44
+ } catch (error) {
45
+ console.warn("Failed to read app.json for Fusion URL", error);
46
+ }
47
+ }
48
+ });
49
+ },
50
+ };
51
+ };
@@ -0,0 +1,2 @@
1
+ // Vite plugin exports
2
+ export { fusionOpenPlugin } from "./fusion-open-plugin";