Building an application for Twitch Extensions

Hello, I’ve essentially completed an entire application for warzone loadouts, the code obtains a Twitch JWT token and assigns the token from the Creator who is accessing the extension and applies it as the twitchId partition key for new, existing, and the choice to delete a loadout.

The issue I am currently having is in my hosted test it is displaying

I’ve added my versions into my code/manifest.json file. According to the 400 Bad Request error in Developer Tools the error is from CloudFront.

The application has a fallback option so I can run the application without twitch.ext, but the defeats the purpose as I cannot call my loadouts.

Thank you!

So you

  • made a zip file
  • uploaded the zip file
  • went to hosted test

and the HTML is not loading?

The usual culprit here is that you upload a zip of a folder of files instead of a zip of files

And as a result the declared developer console paths to things doesn’t match

Thanks for the insight Barry, the zip file I made is the files contained in “build” after running my application through the npm run build command. Similarly to how you would upload the data to your S3 bucket in AWS. The files where zipped (static/css/files*, static/js/files*, asset-manifest.json, favicon, index.html, logo192.png, logo512.png, manifest.json, robots.txt)
Originally I was just uploading the zipped Build folder, but after testing the above method I am still receiving this error.

How did you make the zip file?

also

What code/manifest.json file do you refer to? One isn’t needed for extensions?

Your HTML file for the view in the console is set to index.html?

Here for exmple my view is set to panel/index.html

image

as my local dev process is s dumb/static server

I wonder if you have / instead of an explicit file

Compressed by highlighting the files in “build” and compressing it into a zip file.

The manifest.json file is populated when running the npm run build, all I did was simply add the version number.
{
“version”: “0.1.2”, // Added version field
“short_name”: “React App”,
“name”: “Create React App Sample”,
“icons”: [
{
// remainder of the file code here

and my html file for the view in the console is indeed set to index.html

If there is anything I missed, please let me know and I can upload/post the needed details.

I’m out of ideas then.

Your zip process seems correct
And your dev console seems correct.

But just to check you did go back to local test, and then upload and then back to hosted test?

And you did upload the right/new zip file?

And talking to the right version of the right extension?

Might also just be a weird state of caching.

But the usual suspect is a bad zip file.

So I moved back to Local Test, recompiled the zip (highlighting and compressing), uploaded those new files and saved the changes, and double checked that the index.html was still listed. Double checked I uploaded to the proper version (in this case 0.1.2).

The zip file “static” when opened looks like this. (just to clarify to see if maybe there is an issue in the zipping process)

That all seems correct so I’m out of the obvious ideas here.

Massively delayed response because I got distracted with my son, but I’ve managed to make the entire application properly upload, but when attempting to call my fetchLoadouts to deploy to the panel for viewers to see I receive this error. (I’ve checked the CSP policy according to Twitch, and the backend service being used doesn’t violate the policies, that I could find at least, so I’m curious what to do here)


Otherwise my next option would be running it through a backend server (node.js, probably relatively simple to keep it easier), but I am unsure if this would end up violating the CSP policy as well.

This error says that you didn’t add us-centra1-warzone-loadouts-ext.cloudfunctions.net to your CSP In the developer console.

You did add the domain to your EBS to the CSP for Allowlist for URL Fetching Domains?

This suggest you added it somewhere but not the correct place.

If my link is variable based on the SelectedCreator (determined within the application)
Would this be the link used:
// Construct the URL to call the Google Cloud Function dynamically based on the selected creator
const FUNCTION_URL = https://us-central1-warzone-loadouts-ext.cloudfunctions.net/fetchLoadouts?range=${encodeURIComponent(selectedCreator)}!A1:K;

I have the link placed in URL fetching config and panel urls just to be safe, but still processes as a no-no.

The link is dynamic since at any point you could have more than one creator (myself + whomever) wanting to load their loadout profile.

CSP only as a minum the domain https://us-central1-warzone-loadouts-ext.cloudfunctions.net

You need/want an “open” policy of just the domain not a restrictive policy of the domain+path

Must’ve been the lack of caffeine I had typed https://us-centra1-warzone-loadouts-ext.cloudfunctions.net instead of https://us-central1-warzone-loadouts-ext.cloudfunctions.net - Happy Monday! Thanks again Barry!

Started running into a separate error now, changed the application around to automate the configuration loading/creation and I cannot seem to figure out how to fix these errors.

Essentially the application upon loading the Configuration page on the Extension makes a call to the Twitch API (obtains an OAuth Token, and calls to grab the TwitchUsername which is then stored to be used next) This is a process that will either do 1 of 2 things. 1) Load the configuration that has already existed because they have used the application before. 2) Creates a brand new configuration under their TwitchUsername making it editable by them and only them (except Admin access).

A 401 response from the Get Users endpoint would indicate an unauthorized request. You haven’t shown your code so it’s hard to tell what specifically may be the issue, but when you say “upon loading the Configuration page”, are you making the request immediate as it’s loaded, or waiting for onAuthorized?

So you onAuthorized

then used the helixToken to call the GetUsers API with for the user name? (And used the correct prefix?)
And used the JWT to load the configuration from the configuration service?

Either way all your logged here is the status code, the body of the response hasmore information

You should be storing/fetching via userID as userID never changes but users can change the username on their account which results in you losing their configuration.

Or if the case of a user renaming to a username you have on file, stealing someone elses configuration

Do you have a link for the userID call? I cannot seem to find it, I would like to automate this entirely, for the mean time I reverted back to preloaded configuration files. I could not find anything that specifically states the userID call. I appreciate the help and feedback you and Dist provided.

I was not aware we could provide code(entire or snippets of), so I can provide the needed code that is housed in my config.js file as the Configuration page is the one doing the fetch/check/store portion of the application.

If you are storing the broadcaster’s data then you don’t need an API call at all

The channelID is encoded in the JWT.

Normally an extension when it makes a request to it’s EBS will pass over the JWT, as the JWT is used to verify that it is traffic from the Extension.

So the userID you are likely after is:

window.Twitch.ext.onAuthorized((data) => {
    let channelId = data.channelId;

    goCallMyEbs();
});

This may have been the solution to my problem, going to update my files and get back to you if the solution works, or if the problem persisted. Thank you again Barry! I appreciate the assistance.

So I’ve managed to be able to grab the channelId and use that as the piece to create/load the configurations. I’m now running into this error:


I’ve blacked out the API key as I will rotate this into a secrets manager with a new key after this is becomes a successful setup.

Script file 67:
const response = await fetch(url, {
method: ‘POST’,
headers: {
‘Authorization’: Bearer ${API_KEY},
‘Content-Type’: ‘application/json’
},
body: JSON.stringify(body)
});

    if (!response.ok) {
        throw new Error('Failed to create new sheet');
    }
    console.log(`Sheet ${channelId} created successfully`);
};

Script line 37 (inside a function):
const checkIfSheetExists = async (channelId) => {
const SHEET_ID = ‘SHEET_ID’;
const apiUrl = https://sheets.googleapis.com/v4/spreadsheets/${SHEET_ID}/values/${channelId}!A1?key=${API_KEY};

    try {
        const response = await fetch(apiUrl);
        return response.ok;
    } catch (error) {
        console.error('Error checking if sheet exists:', error);
        throw error;
    }
};

If need be I can post the entire config.js file to make it easier. But this is the last piece that is holding me up.

my test.html file however will create it successfully.

Test Google Sheets Function body { font-family: Arial, sans-serif; margin: 40px; } input, button { margin: 10px 0; padding: 8px; font-size: 16px; } .message { margin-top: 20px; padding: 10px; border-radius: 5px; } .success { background-color: #d4edda; color: #155724; } .error { background-color: #f8d7da; color: #721c24; }

Test Google Sheets Function

<label for="channelId">Enter Channel ID:</label><br>
<input type="text" id="channelId" placeholder="Enter Channel ID"><br>

<label for="email">Enter Email (optional for permissions):</label><br>
<input type="email" id="email" placeholder="Enter Email (optional)"><br>

<button onclick="testFunction()">Test</button>

<div id="message" class="message"></div>

<script>
    async function testFunction() {
        const channelId = document.getElementById('channelId').value.trim();
        const email = document.getElementById('email').value.trim();
        const messageDiv = document.getElementById('message');

        if (!channelId) {
            messageDiv.textContent = "Please enter a Channel ID.";
            messageDiv.className = "message error";
            return;
        }

        try {
            const response = await fetch(`Link_to_my_Function?channelId=${channelId}`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify({ email: email || null })
            });

            if (response.ok) {
                const data = await response.json();
                messageDiv.textContent = "Sheet was successfully found or created.";
                messageDiv.className = "message success";
                console.log("Response Data:", data);
            } else {
                const errorText = await response.text();
                messageDiv.textContent = `Failed to check or create sheet: ${errorText}`;
                messageDiv.className = "message error";
            }
        } catch (error) {
            messageDiv.textContent = `Error: ${error.message}`;
            messageDiv.className = "message error";
            console.error("Fetch Error:", error);
        }
    }
</script>