474 lines
18 KiB
Markdown
474 lines
18 KiB
Markdown
# MASTER SYSTEM ARCHITECTURE PLAN & EXECUTION PROMPT
|
|
## E-Governance Cryptographic Licensing Engine & Multi-Tenant CA System
|
|
|
|
> **Target Workspace:** `E:\nextgen\next-gen`
|
|
> **SuperAdmin Instance:** `E:\nextgen\next-gen\superadmin` (`server/` and `client/`)
|
|
> **First Tenant Instance:** `E:\nextgen\next-gen\client` (React Vite PWA) and `E:\nextgen\next-gen\server` (Node/Express API Engine)
|
|
> **Document Purpose:** Complete, step-by-step technical architectural prompt for an execution agent to implement asymmetric Ed25519 licensing, hybrid dual-driver caching, global access middleware, and anti-tampering clock protection.
|
|
|
|
---
|
|
|
|
## EXECUTIVE ARCHITECTURE OVERVIEW
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ SUPERADMIN ENGINE (CA PORTAL) │
|
|
│ E:\nextgen\next-gen\superadmin │
|
|
│ - Holds Ed25519 PRIVATE KEY (SUPERADMIN_PRIVATE_KEY) │
|
|
│ - Signs License Tokens: Ed25519 (EdDSA) │
|
|
│ - Exposes JWKS Endpoint: /api/v1/auth/jwks │
|
|
│ - Stores Tenant Registry in Supabase Cloud (`tenants`, `tenant_licenses`)│
|
|
└───────────────────────────────────┬─────────────────────────────────────┘
|
|
│ Cryptographic License Token
|
|
│ (Ed25519 Signed JWT)
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ FIRST TENANT INSTANCE & DEPLOYMENTS │
|
|
│ Client: E:\nextgen\next-gen\client (React Vite PWA + Web Crypto) │
|
|
│ Server: E:\nextgen\next-gen\server (Node Express Backend API) │
|
|
│ │
|
|
│ - Holds Ed25519 PUBLIC KEY (SUPERADMIN_PUBLIC_KEY / VITE_...) │
|
|
│ - Storage: Local SQLite/Postgres `tenant_licenses` │
|
|
│ - Hybrid Dual-Driver Cache: Memory LRU (`lru-cache`) + Redis (`ioredis`)│
|
|
│ - AccessMiddleware: Enforces Read-Only on Expired Licenses (HTTP 402) │
|
|
│ - Clock Anti-Tamper: Heartbeat sync with SuperAdmin + NTP drift check │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 1. DATABASE SCHEMA & DATA MODELS
|
|
|
|
### 1.1 SuperAdmin Cloud Database (Postgres / Supabase)
|
|
Target Schema File: `E:\nextgen\supabase\supabase_full_schema.sql`
|
|
Service Integration: `E:\nextgen\next-gen\superadmin\server\src\services\SupabaseSuperAdminService.js`
|
|
|
|
```sql
|
|
-- Full Supabase Schema Updates for Asymmetric Ed25519 Licensing Engine
|
|
-- Located in: E:\nextgen\supabase\supabase_full_schema.sql
|
|
|
|
CREATE TABLE IF NOT EXISTS tenant_licenses (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
license_key TEXT UNIQUE NOT NULL,
|
|
term_name TEXT NOT NULL,
|
|
academic_year TEXT NOT NULL,
|
|
start_date DATE NOT NULL,
|
|
end_date DATE NOT NULL,
|
|
grace_period_days INTEGER DEFAULT 7,
|
|
max_students INTEGER DEFAULT 1000,
|
|
max_staff INTEGER DEFAULT 200,
|
|
signed_license_token TEXT,
|
|
algorithm TEXT DEFAULT 'EdDSA',
|
|
status TEXT DEFAULT 'active',
|
|
override_reason TEXT,
|
|
issued_by TEXT,
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL
|
|
);
|
|
|
|
-- Idempotent Column Additions & Performance Indexing
|
|
ALTER TABLE tenant_licenses ADD COLUMN IF NOT EXISTS signed_license_token TEXT;
|
|
ALTER TABLE tenant_licenses ADD COLUMN IF NOT EXISTS algorithm TEXT DEFAULT 'EdDSA';
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_licenses_active
|
|
ON tenant_licenses(tenant_id) WHERE status = 'active';
|
|
```
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_tenant_licenses_active
|
|
ON tenant_licenses(tenant_id) WHERE is_active = TRUE;
|
|
```
|
|
|
|
### 1.2 Tenant Local Database (SQLite / Knex)
|
|
Location: `E:\nextgen\next-gen\server\src\database\` & `E:\nextgen\next-gen\server\data\school.db`
|
|
|
|
```sql
|
|
-- SQLite Compatible Tenant License Table
|
|
CREATE TABLE IF NOT EXISTS tenant_licenses (
|
|
id TEXT PRIMARY KEY,
|
|
tenant_id TEXT NOT NULL,
|
|
term_name TEXT NOT NULL,
|
|
academic_year TEXT NOT NULL,
|
|
start_date TEXT NOT NULL,
|
|
end_date TEXT NOT NULL,
|
|
grace_period_days INTEGER DEFAULT 7,
|
|
max_students INTEGER DEFAULT 1000,
|
|
max_staff INTEGER DEFAULT 200,
|
|
license_key TEXT NOT NULL,
|
|
signed_license_token TEXT NOT NULL,
|
|
status TEXT DEFAULT 'ACTIVE',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
```
|
|
|
|
---
|
|
|
|
## 2. CRYPTOGRAPHIC LICENSING ENGINE (SUPERADMIN CA)
|
|
|
|
### 2.1 Ed25519 Asymmetric Key Generation & Signing Service
|
|
File to Update: `E:\nextgen\next-gen\superadmin\server\src\services\LicenseTokenGenerator.js`
|
|
|
|
```javascript
|
|
/**
|
|
* LicenseTokenGenerator.js
|
|
* Acts as the Certificate Authority (CA) using Ed25519 (EdDSA) asymmetric signing.
|
|
*/
|
|
const crypto = require('crypto');
|
|
const jwt = require('jsonwebtoken');
|
|
|
|
class LicenseTokenGenerator {
|
|
/**
|
|
* Generates or retrieves the Ed25519 Key Pair
|
|
*/
|
|
static getKeyPair() {
|
|
const privateKeyPem = process.env.SUPERADMIN_PRIVATE_KEY;
|
|
const publicKeyPem = process.env.SUPERADMIN_PUBLIC_KEY;
|
|
|
|
if (privateKeyPem && publicKeyPem) {
|
|
return { privateKey: privateKeyPem, publicKey: publicKeyPem };
|
|
}
|
|
|
|
// Fallback keypair generation for development if env variables are not supplied
|
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
|
|
publicKeyEncoding: { type: 'spki', format: 'pem' }
|
|
});
|
|
|
|
return { privateKey, publicKey };
|
|
}
|
|
|
|
/**
|
|
* Generate an Ed25519 cryptographically signed license token
|
|
*/
|
|
static generateToken(payload) {
|
|
const { privateKey } = this.getKeyPair();
|
|
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
const nbf = payload.nbf || Math.floor(new Date(payload.start_date).getTime() / 1000);
|
|
const exp = payload.exp || Math.floor(new Date(payload.end_date).getTime() / 1000);
|
|
|
|
const licensePayload = {
|
|
iss: 'super_admin_licensing_engine',
|
|
sub: payload.tenant_id,
|
|
tenant_code: payload.tenant_code,
|
|
term: payload.term_name,
|
|
academic_year: payload.academic_year,
|
|
max_students: payload.max_students || 1000,
|
|
max_staff: payload.max_staff || 200,
|
|
nbf: nbf,
|
|
exp: exp,
|
|
status: 'active',
|
|
iat: nowSeconds
|
|
};
|
|
|
|
// Sign payload using Ed25519 (EdDSA algorithm in JWT standard)
|
|
const token = jwt.sign(licensePayload, privateKey, {
|
|
algorithm: 'EdDSA'
|
|
});
|
|
|
|
const keyFormatted = `LIC-${(payload.term_name || 'TERM').toUpperCase().replace(/\s+/g, '')}-${payload.academic_year}-${token.slice(-16).toUpperCase()}`;
|
|
|
|
return {
|
|
token,
|
|
license_key: keyFormatted,
|
|
payload: licensePayload
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Verify an Ed25519 token using the Public Key
|
|
*/
|
|
static verifyToken(token) {
|
|
const { publicKey } = this.getKeyPair();
|
|
try {
|
|
const decoded = jwt.verify(token, publicKey, { algorithms: ['EdDSA'] });
|
|
return { isValid: true, payload: decoded };
|
|
} catch (err) {
|
|
return { isValid: false, error: err.message };
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = LicenseTokenGenerator;
|
|
```
|
|
|
|
### 2.2 SuperAdmin JWKS & Public Key Distribution Endpoint
|
|
File to Update: `E:\nextgen\next-gen\superadmin\server\src\routes\superadmin.routes.js`
|
|
|
|
Add route `/api/v1/auth/jwks` to expose the Ed25519 Public Key for online tenant bootstrapping:
|
|
|
|
```javascript
|
|
router.get('/v1/auth/jwks', (req, res) => {
|
|
const { publicKey } = LicenseTokenGenerator.getKeyPair();
|
|
res.json({
|
|
keys: [
|
|
{
|
|
kty: 'OKP',
|
|
crv: 'Ed25519',
|
|
use: 'sig',
|
|
alg: 'EdDSA',
|
|
pubKeyPem: publicKey
|
|
}
|
|
]
|
|
});
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 3. HYBRID DUAL-DRIVER CACHING LAYER (TENANT BACKEND)
|
|
|
|
File to Create: `E:\nextgen\next-gen\server\src\services\LicenseCacheService.js`
|
|
|
|
```javascript
|
|
/**
|
|
* LicenseCacheService.js
|
|
* Hybrid Dual-Driver Cache (In-Memory LRU default + Redis Adapter)
|
|
*/
|
|
const { LRUCache } = require('lru-cache');
|
|
const redis = require('redis');
|
|
|
|
// In-Memory LRU Cache Instance (default zero-dependency fallback)
|
|
const localMemoryCache = new LRUCache({
|
|
max: 500, // Maximum 500 cached tenant tokens
|
|
ttl: 1000 * 60 * 60 * 2 // 2 hours default local memory check interval
|
|
});
|
|
|
|
let redisClient = null;
|
|
|
|
if (process.env.REDIS_URL) {
|
|
redisClient = redis.createClient({ url: process.env.REDIS_URL });
|
|
redisClient.on('error', (err) => console.error('Redis License Cache Error:', err));
|
|
redisClient.connect().catch(() => {
|
|
console.warn('⚠️ Could not connect to Redis; defaulting to in-memory LRU cache.');
|
|
});
|
|
}
|
|
|
|
const LicenseCache = {
|
|
async get(tenantId) {
|
|
try {
|
|
if (redisClient && redisClient.isOpen) {
|
|
const data = await redisClient.get(`license:tenant:${tenantId}`);
|
|
return data ? JSON.parse(data) : null;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Redis GET failed, falling back to LRU cache:', e.message);
|
|
}
|
|
return localMemoryCache.get(tenantId) || null;
|
|
},
|
|
|
|
async set(tenantId, payload, ttlInSeconds) {
|
|
try {
|
|
if (redisClient && redisClient.isOpen) {
|
|
await redisClient.set(`license:tenant:${tenantId}`, JSON.stringify(payload), {
|
|
EX: Math.max(ttlInSeconds, 60)
|
|
});
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
console.warn('Redis SET failed, falling back to LRU cache:', e.message);
|
|
}
|
|
localMemoryCache.set(tenantId, payload, { ttl: Math.max(ttlInSeconds, 60) * 1000 });
|
|
},
|
|
|
|
async del(tenantId) {
|
|
if (redisClient && redisClient.isOpen) {
|
|
await redisClient.del(`license:tenant:${tenantId}`);
|
|
}
|
|
localMemoryCache.delete(tenantId);
|
|
}
|
|
};
|
|
|
|
module.exports = LicenseCache;
|
|
```
|
|
|
|
---
|
|
|
|
## 4. GLOBAL ACCESSMIDDLEWARE & CLOCK DRIFT PROTECTION
|
|
|
|
File to Update: `E:\nextgen\next-gen\server\src\middleware\licensingMiddleware.js`
|
|
|
|
```javascript
|
|
/**
|
|
* licensingMiddleware.js
|
|
* Intercepts incoming requests, verifies Ed25519 tokens via Hybrid Cache,
|
|
* checks system clock drift, and enforces Read-Only restriction (HTTP 402) on expired licenses.
|
|
*/
|
|
const jwt = require('jsonwebtoken');
|
|
const Database = require('better-sqlite3');
|
|
const path = require('path');
|
|
const LicenseCache = require('../services/LicenseCacheService');
|
|
|
|
// Public Key injected via env or fetched from JWKS
|
|
const SUPERADMIN_PUBLIC_KEY = process.env.SUPERADMIN_PUBLIC_KEY || `-----BEGIN PUBLIC KEY-----
|
|
MCowBQYDK2VwAyEA...
|
|
-----END PUBLIC KEY-----`;
|
|
|
|
let lastSuperAdminTimeSync = Date.now();
|
|
let serverTimeDriftOffsetMs = 0;
|
|
|
|
// Track trusted time sync with SuperAdmin
|
|
function updateSystemClockSync(superAdminTimestamp) {
|
|
if (superAdminTimestamp) {
|
|
const remoteTime = new Date(superAdminTimestamp).getTime();
|
|
serverTimeDriftOffsetMs = Math.abs(Date.now() - remoteTime);
|
|
lastSuperAdminTimeSync = Date.now();
|
|
}
|
|
}
|
|
|
|
function verifyEd25519Token(token) {
|
|
try {
|
|
const decoded = jwt.verify(token, SUPERADMIN_PUBLIC_KEY, { algorithms: ['EdDSA'] });
|
|
return { isValid: true, payload: decoded };
|
|
} catch (err) {
|
|
return { isValid: false, error: err.message };
|
|
}
|
|
}
|
|
|
|
async function verifyLicenseMiddleware(req, res, next) {
|
|
const tenantId =
|
|
req.headers['x-tenant-id'] ||
|
|
req.headers['x-tenant-code'] ||
|
|
process.env.TENANT_ID ||
|
|
'5b39bcb5-506a-456d-b7cb-91730b397cc1';
|
|
|
|
req.tenantId = tenantId;
|
|
|
|
// Bypass routes (Auth, SysAdmin, Health, Billing)
|
|
const PUBLIC_PATHS = ['/api/auth/login', '/api/auth/refresh', '/api/sysadmin', '/api/health', '/api/billing'];
|
|
if (PUBLIC_PATHS.some(p => req.path.startsWith(p)) || req.user?.role === 'systems_admin') {
|
|
return next();
|
|
}
|
|
|
|
// Anti-Clock-Tampering Guard: Lock mutations if system clock has drifted > 5 minutes (300,000 ms)
|
|
if (serverTimeDriftOffsetMs > 300000) {
|
|
console.error(`🚨 System Clock Drift Detected: ${serverTimeDriftOffsetMs}ms variance.`);
|
|
return blockMutationRequests(req, res, 'CLOCK_DRIFT_DETECTED', 'System clock tampering or significant time drift detected.');
|
|
}
|
|
|
|
try {
|
|
// 1. Query Hybrid Cache (Memory LRU / Redis)
|
|
let cachedPayload = await LicenseCache.get(tenantId);
|
|
|
|
// 2. Cache Miss Fallback: Read DB raw signed token & verify Ed25519 signature
|
|
if (!cachedPayload) {
|
|
const dbPath = process.env.DB_PATH || path.join(__dirname, '../../data/school.db');
|
|
const db = new Database(dbPath);
|
|
const row = db.prepare('SELECT * FROM tenant_licenses WHERE tenant_id = ? AND status = "ACTIVE" ORDER FROM rowid DESC LIMIT 1').get(tenantId);
|
|
db.close();
|
|
|
|
if (!row || !row.signed_license_token) {
|
|
return blockMutationRequests(req, res, 'NO_LICENSE', 'No active license token found for tenant.');
|
|
}
|
|
|
|
const verification = verifyEd25519Token(row.signed_license_token);
|
|
if (!verification.isValid) {
|
|
return blockMutationRequests(req, res, 'INVALID_SIGNATURE', 'License signature verification failed.');
|
|
}
|
|
|
|
cachedPayload = verification.payload;
|
|
const nowSec = Math.floor(Date.now() / 1000);
|
|
const remainingTTL = (cachedPayload.exp || 0) - nowSec;
|
|
|
|
if (remainingTTL > 0) {
|
|
await LicenseCache.set(tenantId, cachedPayload, remainingTTL);
|
|
}
|
|
}
|
|
|
|
// 3. Temporal Bound Check
|
|
const currentSeconds = Math.floor(Date.now() / 1000);
|
|
if (currentSeconds < cachedPayload.nbf || currentSeconds > cachedPayload.exp) {
|
|
return blockMutationRequests(req, res, 'LICENSE_EXPIRED', 'Your termly license has expired.');
|
|
}
|
|
|
|
// License is valid
|
|
res.setHeader('X-License-Status', 'ACTIVE');
|
|
return next();
|
|
} catch (err) {
|
|
console.error('License Middleware Error:', err);
|
|
return blockMutationRequests(req, res, 'EVALUATION_ERROR', err.message);
|
|
}
|
|
}
|
|
|
|
function blockMutationRequests(req, res, code, message) {
|
|
const isReadOnly = ['GET', 'HEAD', 'OPTIONS'].includes(req.method.toUpperCase());
|
|
|
|
if (isReadOnly) {
|
|
res.setHeader('X-License-Status', 'Expired-Read-Only');
|
|
return req.next ? req.next() : res.status(200);
|
|
}
|
|
|
|
return res.status(402).json({
|
|
error: 'Payment Required / Term License Expired',
|
|
code: code || 'LICENSE_EXPIRED',
|
|
message: message || 'Your termly license has expired. Please contact billing.',
|
|
billing_url: '/billing'
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
verifyLicenseMiddleware,
|
|
updateSystemClockSync
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
## 5. CLIENT-SIDE PWA OFFLINE VERIFICATION (REACT VITE)
|
|
|
|
Location: `E:\nextgen\next-gen\client\src\services\offlineLicenseVerifier.js`
|
|
|
|
```javascript
|
|
/**
|
|
* offlineLicenseVerifier.js
|
|
* Uses Browser Native Web Crypto API to verify Ed25519 license tokens offline in React PWA.
|
|
*/
|
|
export async function verifyLicenseOffline(signedToken, publicKeyPem) {
|
|
try {
|
|
const [headerB64, payloadB64, signatureB64] = signedToken.split('.');
|
|
const dataToVerify = new TextEncoder().encode(`${headerB64}.${payloadB64}`);
|
|
|
|
// Decode base64url signature
|
|
const signatureBin = Uint8Array.from(atob(signatureB64.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
|
|
const payloadJson = JSON.parse(atob(payloadB64.replace(/-/g, '+').replace(/_/g, '/')));
|
|
|
|
// Simple temporal validation
|
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
if (nowSeconds < payloadJson.nbf || nowSeconds > payloadJson.exp) {
|
|
return { isValid: false, reason: 'EXPIRED', payload: payloadJson };
|
|
}
|
|
|
|
return { isValid: true, payload: payloadJson };
|
|
} catch (e) {
|
|
return { isValid: false, reason: 'INVALID_FORMAT', error: e.message };
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. STEP-BY-STEP EXECUTION AGENT CHECKLIST
|
|
|
|
1. **Update Supabase Database Schema (`E:\nextgen\supabase\supabase_full_schema.sql`)**:
|
|
- Ensure `tenant_licenses` table includes `signed_license_token TEXT` and `algorithm TEXT DEFAULT 'EdDSA'`.
|
|
- Apply schema updates to Supabase PostgreSQL cloud instance via Supabase CLI or SQL Editor:
|
|
```bash
|
|
supabase db push --schema-file E:\nextgen\supabase\supabase_full_schema.sql
|
|
```
|
|
2. **Update SuperAdmin CA Generator (`E:\nextgen\next-gen\superadmin\server\src\services\LicenseTokenGenerator.js`)**:
|
|
- Replace symmetric HS256 with Ed25519 (EdDSA) signing using `crypto.generateKeyPairSync('ed25519')` or PEM environment keys.
|
|
3. **Expose JWKS Endpoint (`E:\nextgen\next-gen\superadmin\server\src\routes\superadmin.routes.js`)**:
|
|
- Add `/api/v1/auth/jwks` route returning public keys.
|
|
4. **Implement Hybrid Dual-Driver Cache (`E:\nextgen\next-gen\server\src\services\LicenseCacheService.js`)**:
|
|
- Implement in-memory LRU (`lru-cache`) with optional Redis fallback.
|
|
5. **Update Tenant AccessMiddleware (`E:\nextgen\next-gen\server\src\middleware\licensingMiddleware.js`)**:
|
|
- Integrate `LicenseCacheService`.
|
|
- Add Ed25519 asymmetric verification.
|
|
- Enforce HTTP 402 Payment Required for non-GET/HEAD/OPTIONS requests on expired licenses.
|
|
- Add 5-minute system clock drift threshold check.
|
|
6. **Configure First Tenant Environment (`E:\nextgen\next-gen\.env` & `E:\nextgen\next-gen\server\.env`)**:
|
|
- Populate `SUPERADMIN_PUBLIC_KEY` and `VITE_SUPERADMIN_PUBLIC_KEY`.
|
|
7. **Run Verification Commands**:
|
|
- Test license creation in SuperAdmin UI and confirm sync to Supabase `tenant_licenses` table.
|
|
- Test GET vs POST endpoints on `E:\nextgen\next-gen\server` under active vs expired license tokens.
|