Why non-graphical browsers are useful for webapps/LLMOps/MLOps
Managing a webapp where users can use an AI model or database can be tricky because some functions/processes can be run only in the Frontend browser, and other processes can only be run on a Backend PC/server. As mentioned in a few of my other posts, one important Frontend browser library that is used to encrypt/decrypt text and/or data is the window.crypto.subtle library because one can protect data safely in public repositories without fear that the data will be stolen [1].
An even safer way to keep encrypted text data safe in a public repository is to re-encrypt the data on a schedule; meaning that one must create new encryption keys are regularly and re-encrypt the data with these new keys.
Since the window.crypto.subtle library only works in the Frontend browser, how can one run browser processes on a schedule? The answer is non-graphical browsers, like Node.js Playwright or Puppeteer or Python Selenium! Non-graphical browsers can run webpage user interface operations/processes without the graphical user interface, in an automated manner on a Backend PC/server.
In this post, a webpage process is run on a GitHub Actions schedule using Puppeteer in Node.js.
Step 0: Create the needed files
- Create a GitHub repository,
- Create a index.html file,
- Create a .github/workflows/main.yaml file.
Step 1: index.html
In the index.html file, create a function to generate window.crypto.subtle keys and a button to run the function. The script below is a custom script that creates keys, I use two libraries that I wrote to encrypt the keys and save them in a file in the repository. The script is just an example, so feel free to save the keys as you wish.
The important parts of the index.html script that allows Puppeteer to work correctly in the GitHub Action script are:
- The button needs to have a class name (ie: I call it buttonClass) because Puppeteer uses the class name (called a selector) to find the button in the GitHub Action script.
- The button has to be visible on the page, or have style=”display:block”, in order for Puppeteer to run without an error. I made the button really small so that the page looks blank, to prevent people from pushing the button.
- After the function is run, be sure to refresh the page so that the Puppeteer page.waitForNavigation function knows when to stop. I used a window.location.href assignment to reload the page after the key generation function finishes.
<!DOCTYPE html>
<html>
<head></head>
<body>
<button class="buttonClass" id="Generate_keys_crypto" type="button" style="display:block; width:2px; height: 2px;"></button>
<!-- --------------------------------------------------- -->
<script type="module" src="./index.js"></script>
<!-- --------------------------------------------------- -->
<script>
document.getElementById("Generate_keys_crypto").addEventListener("click", async () => {
await Generate_keys_crypto();
// Change the page so that it is known that the function is finished
// Works if await Promise.all([page.waitForNavigation({timeout: 30000, waitUntil: 'load'}), page.click('.buttonClass'), ]);
window.location.href = window.location.href + '?nocache=' + new Date().getTime();
});
// ----------------------------------------------------
async function Generate_keys_crypto() {
// STEP 0: Generate public and private keys (public key=for encrypting, private key=for decrypting)
var keyPair_obj = await window.crypto.subtle.generateKey({name: "RSA-OAEP", modulusLength: 2048, publicExponent: new Uint8Array([0x01, 0x00, 0x01]), hash: {name: "SHA-256"}}, true, ["encrypt", "decrypt"]);
// STEP 1: Obtain RSA key pair as individual objects - https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/generateKey
// STEP 2: Convert public key and private key into a JSON string (JsonWebKey)
// -----------------------------
var RepoAobj = {};
RepoAobj.repoOwner = 'CodeSolutions2';
RepoAobj.repoA_name = 'frontend_backend_message_passing_central_repository_v1';
RepoAobj.foldername = '.github'; // foldername in RepoB
RepoAobj.repoB_name = 'frontend_backend_message_passing_central_repository_v1';
RepoAobj.type_of_encryption = "salt_scramble";
// -----------------------------
// Generate a new publicKey_jwk_str
var publicKey_jwk = await window.crypto.subtle.exportKey("jwk", keyPair_obj.publicKey);
RepoAobj.input_text = JSON.stringify(publicKey_jwk);
// console.log("non-encrypted publicKey_jwk_str: ", RepoAobj.input_text.slice(0,5));
// Assign the class where input_text is the generated publicKey_jwk_str
const classObj0 = new encrypted_CRUD_file_storage(RepoAobj);
// Encrypt the publicKey_jwk_str
RepoAobj.input_text = await classObj0.encrypt_text_string()
// console.log("encrypted_publicKey_jwk_str: ", RepoAobj.input_text.slice(0,5));
// Save the encrypted string to the .public_window_crypto_subtle
RepoAobj.filename = '.public_window_crypto_subtle'; // filename to create in RepoB, in foldername
await run_backend_process(RepoAobj);
// -----------------------------
// Generate a new privateKey_jwk
var privateKey_jwk = await window.crypto.subtle.exportKey("jwk", keyPair_obj.privateKey);
RepoAobj.input_text = JSON.stringify(privateKey_jwk);
// console.log("non-encrypted privateKey_jwk_str: ", RepoAobj.input_text.slice(0,5));
// Assign the class where input_text is the generated privateKey_jwk
const classObj1 = new encrypted_CRUD_file_storage(RepoAobj);
// Encrypt the privateKey_jwk_str
RepoAobj.input_text = await classObj1.encrypt_text_string();
// console.log("encrypted_privateKey_jwk_str: ", RepoAobj.input_text.slice(0,5));
// Save the encrypted string to the .private_window_crypto_subtle
RepoAobj.filename = '.private_window_crypto_subtle'; // filename to create in RepoB, in foldername
await run_backend_process(RepoAobj);
// -----------------------------
}
// ----------------------------------------------------
</script>
</body>
</html>
Step 2: main.yaml
In this file, Node.js is installed and Puppeteer is installed. Then Puppeteer loads the repository page and clicks on the button two times a day using the class name .buttonClass. The page.waitForNavigation function waits for 30 seconds before throwing an error, and stops (returns the promise) when it detect a page load event, essentially a page redirection or change event which is performed by the window.location.href after the html/js function finishes.
name: Updating window.crypto.subtle keys on a schedule
on:
schedule:
- cron: '0 3,15 * * *'
jobs:
update_token:
runs-on: ubuntu-latest
permissions: write-all
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v3
with:
node-version: 20
- run: npm install puppeteer;
- name: Load webpage using a headless browser
uses: actions/github-script@v6
with:
script: |
const puppeteer = require('puppeteer');
try {
const URL = "https://RepoOwnerName.github.io/RepoName/index.html";
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
page.on('load', () => { console.log('Page loaded successfully'); });
await page.goto(URL);
await Promise.all([page.waitForNavigation({timeout: 30000, waitUntil: 'load'}), page.click('.buttonClass'), ]);
await browser.close();
} catch (error) {
console.log('error: ', error);
}
Summary
Clicking a button is just one basic task that a non-graphical user interface browser can perform. The Puppeteer API [2] can automate almost any user interface operation, and allow you to run any Frontend process automatically on the Backend!
Happy Practicing! 👋
References
- How to make an encrypted CRUD file database on GitHub: https://medium.com/towardsdev/how-to-make-an-encrypted-crud-file-database-on-github-79c8ede13f13
- Puppeteer API: https://pptr.dev/api/puppeteer.puppeteernode