Blocked: Integrating External JS SDK (Retell AI) due to Content Security Policy (CSP) Blocking the NPM Plugin

Hello WeWeb Team and Community,

I’m working on an application that requires integrating a third-party, client-side JavaScript library: the Retell AI Web Call SDK (retell-client-js-sdk). I have successfully built and connected the entire backend through Xano, but I am completely blocked on the front-end integration due to what appears to be a conflict between the WeWeb NPM Plugin and the platform’s own Content Security Policy (CSP).

I’m hoping to find a solution or a recommended best practice from the team.

What I’m Trying to Achieve:

The goal is simple: a user clicks a “Talk to AI” button, and a web call is initiated in the browser using the Retell SDK. This requires the retell-client-js-sdk library to be loaded on the page.

My Debugging Journey & What I’ve Tried:

  1. Method 1: The Standard Tag
  • My first approach was to add a standard script tag to my page’s header, as recommended by many services.

  • Result: Failed. We discovered the CDN URL provided by the vendor was dead (net::ERR_NAME_NOT_RESOLVED).

  1. Method 2: The WeWeb NPM Plugin (The “WeWeb Way”)
  • This seemed like the perfect solution. I used the NPM Plugin to add the retell-client-js-sdk package. I also configured it to expose the library via a “Global property name” called RetellSDK.

  • Result: Failed. When the workflow runs, the browser console shows the error: TypeError: Cannot read properties of undefined (reading ‘RetellWebClient’) . This means the RetellSDK object is being created, but it’s an empty shell; the actual code from the library is not present inside it.

The Root Cause: Content Security Policy (CSP)

After inspecting the “Issues” tab in the browser’s developer console, I found the definitive reason for the failure:

“The Content Security Policy (CSP) of your site blocks the use of ‘eval’ in JavaScript.”

It appears the WeWeb NPM plugin relies on an eval-like function to load the package from UNPKG into the global scope. However, the platform’s own default security policy blocks eval, which silently neuters the NPM plugin and prevents the library from loading correctly.

(As a side note, my console also shows my project is still trying to load the dead CDN link, even after I’ve removed it from both the Project and Page custom code sections and deployed. This seems like a caching issue.)

My Question for the WeWeb Team & Community:

  1. What is the officially recommended, secure way to integrate a third-party client-side SDK (like retell-client-js-sdk) that is available on NPM but is being blocked by the default CSP?

  2. Is there a way for users to safely modify the Content Security Policy for our projects to allow unsafe-eval only for the trusted UNPKG scripts loaded by the NPM plugin?

  3. Is there a better architectural pattern for this that I am missing?

I am fully blocked on this critical feature. Any guidance or a confirmed solution would be immensely appreciated, not just for me but for anyone else trying to integrate similar modern JavaScript libraries.

Thank you

I used this method for the featurebase sdk applied in a on app load workflow

`if (!window[“scp-loading-99f346ad-45ec-4917-a025-e2fa02be5b3a”]) {

window[“scp-loading-99f346ad-45ec-4917-a025-e2fa02be5b3a”] = true;

let doc;

/* Adding script from null */

doc = document.createElement(“script”);

doc.innerHTML = `!(function(e,t){const a=“featurebase-sdk”;function n(){if(!t.getElementById(a)){var e=t.createElement(“script”);(e.id=a),(e.src=“https://do.featurebase.app/js/sdk.js"),t.getElementsByTagName(“script”)\\\[0\\\].parentNode.insertBefore(e,t.getElementsByTagName(“script”)\\\[0\\\])}}"function”!=typeof e.Featurebase&&(e.Featurebase=function(){(e.Featurebase.q=e.Featurebase.q||).push(arguments)}),“complete”===t.readyState||“interactive”===t.readyState?n():t.addEventListener(“DOMContentLoaded”,n)})(window,document);`;

document.body.appendChild(doc);

}if (!window[“scp-loading-99f346ad-45ec-4917-a025-e2fa02be5b3a”]) {

window[“scp-loading-99f346ad-45ec-4917-a025-e2fa02be5b3a”] = true;

let doc;

/* Adding script from null */

doc = document.createElement(“script”);

doc.innerHTML = `!(function(e,t){const a=“featurebase-sdk”;function n(){if(!t.getElementById(a)){var e=t.createElement(“script”);(e.id=a),(e.src=“https://do.featurebase.app/js/sdk.js"),t.getElementsByTagName(“script”)\\\[0\\\].parentNode.insertBefore(e,t.getElementsByTagName(“script”)\\\[0\\\])}}"function”!=typeof e.Featurebase&&(e.Featurebase=function(){(e.Featurebase.q=e.Featurebase.q||).push(arguments)}),“complete”===t.readyState||“interactive”===t.readyState?n():t.addEventListener(“DOMContentLoaded”,n)})(window,document);`;

document.body.appendChild(doc);

}`

2 Likes

I ended up finally getting it to work like this.

// This is the final, validated, and self-contained solution.

(async () => {

const button = document.getElementById('talk-to-hubert-btn');

button.disabled = true;

button.textContent = 'Loading SDK...';



function loadScript(url) {

    return new Promise((resolve, reject) => {

        if (document.querySelector(\`script\[src="${url}"\]\`)) return resolve();

        const script = document.createElement('script');

        script.src = url;

        script.onload = resolve;

        script.onerror = () => reject(new Error(\`Failed to load script: ${url}\`));

        document.body.appendChild(script);

    });

}



try {

    // Step 1: Load all dependencies

    await loadScript('https://unpkg.com/eventemitter3/dist/eventemitter3.umd.min.js');

    await loadScript('https://unpkg.com/livekit-client/dist/livekit-client.umd.js');



    // Step 2: The critical fix for the naming mismatch

    window.eventemitter3 = window.EventEmitter3;

    window.livekitClient = window.LivekitClient;



    // Step 3: Load the main Retell SDK

    await loadScript('https://unpkg.com/retell-client-js-sdk/dist/index.umd.js');



    // Step 4: Verify the SDK object has initialized

    if (!window.retellClientJsSdk || !window.retellClientJsSdk.RetellWebClient) {

        throw new Error('Retell SDK failed to initialize.');

    }



    button.textContent = 'Connecting...';



    // Step 5: Get the session from our backend

} const response = await fetch(‘[INSERT API URL]’, { method: ‘POST’ });

    if (!response.ok) {

        throw new Error(\`Failed to create session: ${response.statusText}\`);

    }

    const sessionData = await response.json();



    // Step 6: Initialize and start the call

    const sdk = new window.retellClientJsSdk.RetellWebClient();



    sdk.on('error', (error) => {

        console.error('Retell SDK error:', error);

        button.textContent = 'Talk to your AI Agent';

        button.disabled = false;

    });

    sdk.on('conversationStarted', () => button.textContent = 'Call in Progress...');

    sdk.on('conversationEnded', () => {

        button.textContent = 'Talk to your AI Agent';

        button.disabled = false;

    });



    // THE FINAL FIX: Create a new object with the correct camelCase property names.

    const startCallData = {

        callId: sessionData.call_id,

        accessToken: sessionData.access_token,

        sampleRate: sessionData.sample_rate

    };



    // Pass the correctly formatted object to the SDK.

    await sdk.startCall(startCallData);



} catch (error) {

    console.error("A fatal error occurred:", error);

    button.textContent = 'Error: Setup Failed';

}

})();

1 Like