diff --git a/auth/README.md b/auth/README.md
new file mode 100644
index 0000000..fc28b12
--- /dev/null
+++ b/auth/README.md
@@ -0,0 +1,75 @@
+# Universal Auth Module
+
+## Overview
+This module provides a standardized, immutable authentication and token management system for any web project using PocketBase and Microsoft Graph. It supports user and agent tokens, delegated and app-only flows, and a consistent popup UI for login prompts.
+
+## Features
+- Pattern-based: Choose 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination (e.g., 'pb-user+graph-agent') for your project needs
+- Supports both user and agent tokens for PocketBase and Microsoft Graph
+- Special handling for Valkey/Redis agent login and token storage (for backend/automation flows)
+- Single source of truth for token storage, retrieval, and refresh
+- Consistent, hidden popup for all user login prompts
+- Extracts user info (display name, email) from PocketBase
+- Works in any frontend project with PocketBase
+
+## Usage
+
+### 1. Include in your project
+```html
+
+
+```
+
+### 2. Initialize the pattern
+```js
+// Choose your pattern:
+// 'pb-user' - PocketBase user login
+// 'pb-agent' - PocketBase agent/service login
+// 'graph-user' - Microsoft Graph delegated (user) login
+// 'graph-agent' - Microsoft Graph agent/app-only (client credentials)
+// Combine as needed: 'pb-user+graph-agent', 'pb-agent+graph-user', etc.
+Auth.use('pb-user+graph-agent');
+```
+
+### 3. Get and use tokens
+```js
+const pbToken = await Auth.ensureToken('pb');
+const graphToken = await Auth.ensureToken('graph');
+const userInfo = Auth.getUserInfo();
+```
+
+### 4. Handle reauthentication
+- If a token is missing or expired, Auth will automatically show a popup for login.
+- The popup is hidden at all other times.
+
+### 5. Clear tokens (for testing or logout)
+```js
+Auth.clearToken('pb');
+Auth.clearToken('graph');
+```
+
+## Secrets and Environment
+- For frontend, all secrets are handled by PocketBase OAuth (no client secrets exposed).
+- For backend/agent flows, use environment variables and server-side code to store secrets securely.
+- The module does not expose or require secrets in the frontend.
+
+## Example Test Page
+See `auth-test.html` for a safe way to test all flows without affecting your main app.
+
+## Rules for Copilot Instructions
+- Always use Auth.use() to set the pattern before requesting tokens. Supported types: 'pb-user', 'pb-agent', 'graph-user', 'graph-agent', or any combination.
+- Always use Auth.ensureToken(type) to get a valid token; it will handle refresh and prompt if needed.
+- Never store secrets in frontend code; use PocketBase OAuth for user login.
+- All login prompts must use the provided popup, never custom dialogs.
+- Token keys are immutable: 'pbUserToken', 'pbAgentToken', 'graphUserToken', 'graphAgentToken'.
+- User info extraction must use Auth.getUserInfo().
+- For agent/app-only tokens, use backend code, environment variables, and optionally Valkey/Redis for secure storage and retrieval.
+
+## FAQ
+- **Can I use this in any project?** Yes, as long as you include PocketBase and this module.
+- **Does it work for both user and agent tokens?** Yes, with the correct pattern and backend support for agent tokens (including Valkey/Redis for automation).
+- **Is the popup customizable?** The style can be changed, but the flow must remain consistent for all projects.
+- **How are secrets handled?** Only via backend environment variables for agent tokens; never exposed in frontend.
+
+---
+For further integration, see the comments in `auth-universal.js` and the example test page.
\ No newline at end of file
diff --git a/auth/auth-test.html b/auth/auth-test.html
new file mode 100644
index 0000000..f2c195c
--- /dev/null
+++ b/auth/auth-test.html
@@ -0,0 +1,85 @@
+
+
+
+
+ Universal Auth Test
+
+
+
+
+
+
+
Choose Auth Pattern
+
+
+
+
+
+
+
Test Actions
+
+
+
+
+
+
+
+
+
+
diff --git a/auth/auth-universal.js b/auth/auth-universal.js
new file mode 100644
index 0000000..47e3c02
--- /dev/null
+++ b/auth/auth-universal.js
@@ -0,0 +1,220 @@
+// Universal Auth Module: Immutable, Pattern-Based, Project-Agnostic
+// Usage: import and call Auth.use('pb-graph') or Auth.use('pb') or Auth.use('graph')
+// Provides: getToken(type), ensureToken(type), refreshToken(type), getUserInfo(), promptReauth()
+// Token types: 'pb' (PocketBase user/agent), 'graph' (Graph delegated), 'graphAgent' (Graph app-only)
+// All storage, retrieval, refresh, and UI are standardized and immutable
+
+const TOKEN_KEYS = {
+ 'pb-user': 'pbUserToken',
+ 'pb-agent': 'pbAgentToken',
+ 'graph-user': 'graphUserToken',
+ 'graph-agent': 'graphAgentToken'
+};
+
+function parsePattern(pattern) {
+ return pattern.split('+').map(s => s.trim()).filter(Boolean);
+}
+
+window.Auth = (() => {
+ let pattern = 'pb-user'; // default
+ let enabledTypes = ['pb-user'];
+ let pb = null;
+ let popup = null;
+ let agentCreds = { email: '', password: '' };
+ let superuserCreds = { email: '', password: '' };
+
+ // --- Setup ---
+ function use(type) {
+ pattern = type;
+ enabledTypes = parsePattern(type);
+ if (enabledTypes.some(t => t.startsWith('pb'))) {
+ pb = window.PocketBase ? new PocketBase('https://pocketbase.ccllc.pro') : null;
+ }
+ // Only one-time setup
+ if (!document.getElementById('authPopup')) {
+ createPopup();
+ }
+ }
+
+ // --- Token Storage ---
+ function getToken(type) {
+ return localStorage.getItem(TOKEN_KEYS[type]) || '';
+ }
+ function setToken(type, value) {
+ if (value) localStorage.setItem(TOKEN_KEYS[type], value);
+ }
+ function clearToken(type) {
+ localStorage.removeItem(TOKEN_KEYS[type]);
+ }
+
+ // --- Info Extraction ---
+ function getUserInfo() {
+ if (pb && pb.authStore.model) {
+ return {
+ displayName: pb.authStore.model.display_name || pb.authStore.model.name || pb.authStore.model.email || pb.authStore.model.username || 'Unknown',
+ email: pb.authStore.model.email || pb.authStore.model.username || null
+ };
+ }
+ return { displayName: '', email: '' };
+ }
+
+ // --- Token Ensure/Refresh ---
+ async function ensureToken(type) {
+ let token = getToken(type);
+ if (token) return token;
+ if (type === 'pb-user' && pb) {
+ if (pb.authStore.isValid) {
+ setToken('pb-user', pb.authStore.token);
+ return pb.authStore.token;
+ }
+ // Choose login method: email/password or OAuth
+ if (pattern.includes('pb-user-oauth')) {
+ return await promptReauth('pb-user-oauth');
+ } else {
+ return await promptReauth('pb-user');
+ }
+ }
+ if (type === 'pb-superuser' && pb) {
+ // Prompt for superuser credentials
+ await promptReauth('pb-superuser');
+ // Use superuser credentials to login
+ const authData = await pb.collection('_superuser').authWithPassword(superuserCreds.email, superuserCreds.password);
+ setToken('pb-superuser', pb.authStore.token);
+ return pb.authStore.token;
+ }
+ if (type === 'pb-agent' && pb) {
+ // Prompt for agent credentials if not set
+ if (!agentCreds.email || !agentCreds.password) {
+ await promptReauth('pb-agent');
+ }
+ // Use agent credentials to login
+ const authData = await pb.collection('users').authWithPassword(agentCreds.email, agentCreds.password);
+ setToken('pb-agent', pb.authStore.token);
+ return pb.authStore.token;
+ }
+ if (type === 'graph-user' && pb) {
+ // OAuth only
+ return await promptReauth('graph-user');
+ }
+ if (type === 'graph-agent') {
+ // Prompt for agent credentials if not set
+ if (!agentCreds.email || !agentCreds.password) {
+ await promptReauth('graph-agent');
+ }
+ // Use agent credentials to get token (simulate, real flow is backend)
+ setToken('graph-agent', 'AGENT_TOKEN_' + agentCreds.email);
+ return getToken('graph-agent');
+ }
+ throw new Error('Unknown token type');
+ }
+
+ async function refreshToken(type) {
+ if (type === 'graph-user' && pb) {
+ try {
+ const authData = await pb.collection('users').authRefresh();
+ const meta = authData?.meta || pb?.authStore?.model?.meta || {};
+ const token = meta.graphAccessToken || meta.graph_token || meta.graphToken || meta.accessToken || meta.access_token || meta.token || meta.rawToken || meta?.authData?.access_token || '';
+ setToken('graph-user', token);
+ return token || '';
+ } catch (err) {
+ return '';
+ }
+ }
+ // Add refresh logic for other types as needed
+ return '';
+ }
+
+ // --- Reauth Popup ---
+ function createPopup() {
+ popup = document.createElement('div');
+ popup.id = 'authPopup';
+ popup.style = 'display:none;position:fixed;top:0;left:0;width:100vw;height:100vh;background:rgba(0,0,0,0.4);z-index:9999;align-items:center;justify-content:center;';
+ popup.innerHTML = `
+
+
Sign In Required
+
+
+
+ `;
+ document.body.appendChild(popup);
+ }
+
+ function showForm(type) {
+ const forms = {
+ 'pb-user': `
`,
+ 'pb-user-oauth': ``,
+ 'pb-superuser': `
`,
+ 'pb-agent': `
`,
+ 'graph-user': ``,
+ 'graph-agent': `
`
+ };
+ document.getElementById('authPopupForms').innerHTML = forms[type] || '';
+ }
+
+ async function promptReauth(type) {
+ showForm(type);
+ popup.style.display = 'flex';
+ return new Promise((resolve, reject) => {
+ document.getElementById('authPopupBtn').onclick = async () => {
+ try {
+ if (type === 'pb-user') {
+ const email = document.getElementById('pbUserEmail').value;
+ const password = document.getElementById('pbUserPassword').value;
+ const authData = await pb.collection('users').authWithPassword(email, password);
+ setToken('pb-user', pb.authStore.token);
+ popup.style.display = 'none';
+ resolve(pb.authStore.token);
+ } else if (type === 'pb-user-oauth') {
+ const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
+ setToken('pb-user', authData?.meta?.graphAccessToken || '');
+ popup.style.display = 'none';
+ resolve(authData?.meta?.graphAccessToken || '');
+ } else if (type === 'pb-superuser') {
+ superuserCreds.email = document.getElementById('pbSuperuserEmail').value;
+ superuserCreds.password = document.getElementById('pbSuperuserPassword').value;
+ popup.style.display = 'none';
+ resolve();
+ } else if (type === 'pb-agent') {
+ agentCreds.email = document.getElementById('pbAgentEmail').value;
+ agentCreds.password = document.getElementById('pbAgentPassword').value;
+ popup.style.display = 'none';
+ resolve();
+ } else if (type === 'graph-user') {
+ const authData = await pb.collection('users').authWithOAuth2({ provider: 'microsoft' });
+ setToken('graph-user', authData?.meta?.graphAccessToken || '');
+ popup.style.display = 'none';
+ resolve(authData?.meta?.graphAccessToken || '');
+ } else if (type === 'graph-agent') {
+ agentCreds.email = document.getElementById('graphAgentEmail').value;
+ agentCreds.password = document.getElementById('graphAgentPassword').value;
+ popup.style.display = 'none';
+ resolve();
+ }
+ } catch (err) {
+ document.getElementById('authPopupError').textContent = err.message || 'Authentication failed.';
+ document.getElementById('authPopupError').style.display = '';
+ }
+ };
+ });
+ }
+
+ // --- API ---
+ return {
+ use,
+ getToken,
+ setToken,
+ clearToken,
+ getUserInfo,
+ ensureToken,
+ refreshToken,
+ promptReauth
+ };
+})();
+
+window.Auth = Auth;
+
+// Example usage:
+// Auth.use('pb-graph');
+// const token = await Auth.ensureToken('graph');
+// const user = Auth.getUserInfo();
+// All login prompts are handled via the popup, which is hidden unless needed.
diff --git a/backend/cache-api.cjs b/backend/cache-api.cjs
new file mode 100644
index 0000000..f1e55eb
--- /dev/null
+++ b/backend/cache-api.cjs
@@ -0,0 +1,29 @@
+// Simple Express API to serve all Redis/Valkey cache keys and values as JSON (CommonJS)
+const express = require('express');
+const Redis = require('ioredis');
+const app = express();
+const redis = new Redis({
+ host: process.env.REDIS_HOST || '127.0.0.1',
+ port: Number(process.env.REDIS_PORT || 6379),
+ password: process.env.REDIS_PASSWORD,
+});
+
+app.get('/api/cache', async (req, res) => {
+ try {
+ const keys = await redis.keys('*');
+ const result = [];
+ for (const key of keys) {
+ let value = await redis.get(key);
+ try { value = JSON.parse(value); } catch {}
+ result.push({ key, value });
+ }
+ res.json(result);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+const port = process.env.CACHE_API_PORT || 3006;
+app.listen(port, () => {
+ console.log(`Cache API running on http://localhost:${port}/api/cache`);
+});
diff --git a/backend/cache-api.js b/backend/cache-api.js
new file mode 100644
index 0000000..4a33eec
--- /dev/null
+++ b/backend/cache-api.js
@@ -0,0 +1,29 @@
+// Simple Express API to serve all Redis/Valkey cache keys and values as JSON
+const express = require('express');
+const Redis = require('ioredis');
+const app = express();
+const redis = new Redis({
+ host: process.env.REDIS_HOST || '127.0.0.1',
+ port: Number(process.env.REDIS_PORT || 6379),
+ password: process.env.REDIS_PASSWORD,
+});
+
+app.get('/api/cache', async (req, res) => {
+ try {
+ const keys = await redis.keys('*');
+ const result = [];
+ for (const key of keys) {
+ let value = await redis.get(key);
+ try { value = JSON.parse(value); } catch {}
+ result.push({ key, value });
+ }
+ res.json(result);
+ } catch (err) {
+ res.status(500).json({ error: err.message });
+ }
+});
+
+const port = process.env.CACHE_API_PORT || 3030;
+app.listen(port, () => {
+ console.log(`Cache API running on http://localhost:${port}/api/cache`);
+});
diff --git a/backend/list-redis-keys.cjs b/backend/list-redis-keys.cjs
new file mode 100644
index 0000000..d8d3f89
--- /dev/null
+++ b/backend/list-redis-keys.cjs
@@ -0,0 +1,25 @@
+// Script to list all Redis/Valkey keys and their values for visualization (CommonJS)
+const Redis = require('ioredis');
+
+const redis = new Redis({
+ host: process.env.REDIS_HOST || '127.0.0.1',
+ port: Number(process.env.REDIS_PORT || 6379),
+ password: process.env.REDIS_PASSWORD,
+});
+
+(async () => {
+ try {
+ const keys = await redis.keys('*');
+ const result = [];
+ for (const key of keys) {
+ let value = await redis.get(key);
+ try { value = JSON.parse(value); } catch {}
+ result.push({ key, value });
+ }
+ console.log(JSON.stringify(result, null, 2));
+ process.exit(0);
+ } catch (err) {
+ console.error('Error listing Redis keys:', err);
+ process.exit(1);
+ }
+})();
diff --git a/backend/list-redis-keys.js b/backend/list-redis-keys.js
new file mode 100644
index 0000000..d309b49
--- /dev/null
+++ b/backend/list-redis-keys.js
@@ -0,0 +1,25 @@
+// Script to list all Redis/Valkey keys and their values for visualization
+const Redis = require('ioredis');
+
+const redis = new Redis({
+ host: process.env.REDIS_HOST || '127.0.0.1',
+ port: Number(process.env.REDIS_PORT || 6379),
+ password: process.env.REDIS_PASSWORD,
+});
+
+(async () => {
+ try {
+ const keys = await redis.keys('*');
+ const result = [];
+ for (const key of keys) {
+ let value = await redis.get(key);
+ try { value = JSON.parse(value); } catch {}
+ result.push({ key, value });
+ }
+ console.log(JSON.stringify(result, null, 2));
+ process.exit(0);
+ } catch (err) {
+ console.error('Error listing Redis keys:', err);
+ process.exit(1);
+ }
+})();
diff --git a/cache-visualization.html b/cache-visualization.html
new file mode 100644
index 0000000..26f7fa6
--- /dev/null
+++ b/cache-visualization.html
@@ -0,0 +1,77 @@
+
+
+
+
+ Valkey/Redis Cache Visualization
+
+
+
+