Enigmatic
A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.
Architecture
The diagram above shows the interaction between the client (browser), Bun server, and external services (Auth0 and Cloudflare R2/S3).
Quick Start
Using client.js via CDN
Include client.js in any HTML file using the unpkg CDN:
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/enigmatic@0.34.0/public/client.js"></script> <script> // Configure your API URL window.api_url = 'https://your-server.com'; // Define custom elements window.custom = { "hello-world": (data) => `Hello ${data || 'World'}` }; </script> </head> <body> <hello-world data="message"></hello-world> <script> window.state.message = "Hello World"; </script> </body> </html>
Note: Replace 0.34.0 with the latest version number from npm.
Using the Bun Server
The Bun server provides a complete backend implementation with:
- Key-value storage (using BeeMap)
- File storage (using Cloudflare R2/S3)
- Authentication (using Auth0)
- Static file serving
Installation
-
Install Bun (if not already installed):
curl -fsSL https://bun.sh/install | bash -
Install dependencies:
-
Generate HTTPS certificates (for local development):
cd server ./generate-certs.sh cd ..
Environment Variables
Create a .env file in the project root with the following variables:
# Auth0 Configuration AUTH0_DOMAIN=your-tenant.auth0.com AUTH0_CLIENT_ID=your-client-id AUTH0_CLIENT_SECRET=your-client-secret # Cloudflare R2 Configuration (optional, for file storage) CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key CLOUDFLARE_BUCKET_NAME=your-bucket-name CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com
Running the Server
Start the server with hot reload:
npm start
# or
bun --hot ./bun-server.jsThe server will start on https://localhost:3000 (HTTPS is required for Auth0 cookies).
Server Features
- Static File Serving: Automatically serves any files from the
public/folder - Key-Value Storage: Per-user KV storage using BeeMap (persisted to JSONL files)
- File Storage: Per-user file storage using Cloudflare R2 (or compatible S3)
- Authentication: OAuth2 flow with Auth0
- CORS: Enabled for all origins (configurable)
Server Endpoints
GET /orGET /index.html- Servespublic/index.htmlGET /{path}- Serves static files frompublic/folderGET /login- Initiates Auth0 login flowGET /callback- Auth0 callback handlerGET /logout- Logs out userGET /{key}- Retrieves KV value (requires auth)POST /{key}- Stores KV value (requires auth)DELETE /{key}- Deletes KV value (requires auth)PUT /{key}- Uploads file to R2 (requires auth)PURGE /{key}- Deletes file from R2 (requires auth)PROPFIND /- Lists files in R2 (requires auth)PATCH /{key}- Downloads file from R2 (requires auth)
Overview
client.js is a client-side JavaScript library that provides utilities for DOM manipulation, reactive state management, and API interactions with a backend server. It automatically initializes custom HTML elements and provides a simple API for key-value storage, file operations, and authentication.
Core Utilities
DOM Selectors
window.$ // Alias for document.querySelector window.$$ // Alias for document.querySelectorAll window.$c // Alias for element.closest (requires $0 context)
Usage:
const element = window.$('#my-id'); const elements = window.$$('.my-class');
API Base URL
window.api_url = "https://localhost:3000"
Configures the base URL for all API requests. Modify this to point to your server.
Reactive State Management
window.state is a Proxy object that automatically updates DOM elements when properties change.
How it works:
- Set a property:
window.state.myKey = 'value' - Elements with
data="myKey"attribute are automatically updated - The system looks for custom element handlers in
window.custom[tagName] - Only elements with matching custom element handlers are updated
- Supports both function and object-based custom elements
Example:
<my-element data="message">Initial</my-element> <script> window.custom['my-element'] = (data) => `<div>${data}</div>`; window.state.message = "Updated!"; // Automatically updates the element </script>
Custom Element Integration:
- If
window.custom[tagName]is a function: callsf(value)and setsinnerHTML - If
window.custom[tagName]is an object: callsf.render(value)and setsinnerHTML
API Functions
All API functions are async and return Promises. They use window.api_url as the base URL.
KV Storage Operations
window.get(key)
Retrieves a value from the server's key-value store.
const value = await window.get('my-key');
HTTP Method: GET
Endpoint: {api_url}/{key}
Returns: Parsed JSON response
window.set(key, value)
Stores a value in the server's key-value store.
await window.set('my-key', 'my-value'); await window.set('my-key', { json: 'object' });
HTTP Method: POST
Endpoint: {api_url}/{key}
Body: String values sent as-is, objects are JSON stringified
Returns: Parsed JSON response
window.delete(key)
Deletes a key from the server's key-value store.
await window.delete('my-key');
HTTP Method: DELETE
Endpoint: {api_url}/{key}
Returns: Parsed JSON response
R2 Storage Operations (File Storage)
window.put(key, body)
Uploads a file or data to R2 storage.
await window.put('filename.txt', 'file content'); await window.put('image.png', blob); await window.put('data.json', { json: 'data' });
HTTP Method: PUT
Endpoint: {api_url}/{key}
Body: Accepts Blob, string, or JSON-serializable objects
Returns: Parsed JSON response
window.purge(key)
Deletes a file from R2 storage.
await window.purge('filename.txt');
HTTP Method: PURGE
Endpoint: {api_url}/{key}
Returns: Parsed JSON response
window.list()
Lists all files in the current user's R2 storage.
const files = await window.list(); // Returns: [{ name: 'file1.txt', lastModified: '...', size: 123 }, ...]
HTTP Method: PROPFIND
Endpoint: {api_url}/ (base URL, no key)
Returns: Array of file objects with name, lastModified, and size properties
window.download(key)
Downloads a file from R2 storage and triggers browser download.
await window.download('filename.txt');
HTTP Method: PATCH
Endpoint: {api_url}/{key}
Behavior:
- Fetches file as blob
- Creates temporary download URL
- Triggers browser download
- Cleans up temporary URL
Note: Uses PATCH method due to browser limitations with custom HTTP methods.
Authentication
window.login()
Redirects to the server's login endpoint.
Behavior: Sets window.location.href to {api_url}/login
window.logout()
Redirects to the server's logout endpoint.
Behavior: Sets window.location.href to {api_url}/logout
Custom Elements System
Custom elements are defined in window.custom object and automatically initialized when the DOM loads or when elements are added dynamically.
Initialization
The library automatically:
- Waits for DOM to be ready (
DOMContentLoadedor immediate if already loaded) - Iterates through all keys in
window.custom - Finds all matching HTML elements by tag name
- Calls the custom element handler and sets
innerHTML - Watches for new elements added to the DOM via MutationObserver and initializes them automatically
Proxy Behavior
window.custom is a Proxy that automatically initializes matching elements when you add a new custom element definition:
// Adding a new custom element automatically initializes all matching elements in the DOM window.custom['my-element'] = (data) => `<div>${data}</div>`; // All <my-element> tags are immediately initialized
Defining Custom Elements
Function-based Custom Element
window.custom = { "my-element": async (data) => { return `<div>Content: ${data}</div>`; } };
HTML Usage:
<my-element></my-element>
When used with reactive state, the function receives the state value:
<my-element data="myKey"></my-element> <script> window.state.myKey = 'value'; // Function is called with 'value' </script>
The function receives the state value as the first parameter. If no state value is set, it receives undefined.
Object-based Custom Element
window.custom = { "my-element": { prop: (data) => `Processed: ${data}`, render: function(data) { return `<div>${this.prop(data)}</div>`; } } };
HTML Usage:
<my-element></my-element>
When used with reactive state, the render method is called with the state value.
Example: File Widget
window.custom = { "file-widget": async () => { const list = await window.list(); // Returns HTML string with file list and upload button return `<div>...</div>`; } };
HTML Usage:
<file-widget></file-widget>
This custom element:
- Fetches file list using
window.list() - Renders file items with download and delete buttons
- Includes an upload button
- Uses inline event handlers that call
window.download(),window.purge(), andwindow.put()
Error Handling
All API functions throw errors if the request fails. Use try-catch or .catch():
try { await window.get('nonexistent'); } catch (err) { console.error('Error:', err); } // Or with promises window.get('key').catch(err => console.error(err));
Complete Example
<!DOCTYPE html> <html> <head> <script src="https://unpkg.com/enigmatic@0.34.0/public/client.js"></script> <script> // Configure API URL window.api_url = 'https://localhost:3000'; // Define custom elements window.custom = { "hello-world": (data) => `Hello ${data || 'World'}` }; </script> </head> <body> <!-- Custom element with reactive state --> <hello-world data="message"></hello-world> <script> // Set reactive state (triggers updates to elements with data="message") window.state.message = "Hello World"; // Use API functions (async () => { await window.set('test', 'value'); const value = await window.get('test'); console.log(value); // Upload file const fileInput = document.querySelector('input[type="file"]'); fileInput.onchange = async (e) => { const file = e.target.files[0]; await window.put(file.name, file); }; // List files const files = await window.list(); console.log(files); })(); </script> </body> </html>
Dependencies
- Requires a backend server that implements the API endpoints (or use the included Bun server)
- Requires browser support for:
fetchAPIProxyAPIBlobAPIURL.createObjectURLMutationObserverAPI
Note: Custom element definitions can be loaded before or after client.js - the Proxy system will handle initialization either way.
Notes
- All API functions automatically encode keys using
encodeURIComponent - The
window.download()function uses PATCH method internally (browsers don't support custom HTTP methods) - Custom elements are automatically initialized:
- On page load (when DOM is ready)
- When new custom element definitions are added to
window.custom - When new matching elements are added to the DOM (via MutationObserver)
- The reactive state system only updates elements with matching
dataattributes - Custom element handlers can be async functions
- When a custom element has a
dataattribute, it automatically reads fromwindow.state[dataValue]if no explicit value is provided
Development
Running Tests
Building
The library is ready to use as-is. Simply include client.js via the unpkg CDN as shown in the Quick Start section.
License
MIT
