From 11f96b44d6f72fe61979aae8e87855bce7390c2e Mon Sep 17 00:00:00 2001 From: Sosthene Date: Fri, 25 Jul 2025 15:23:35 +0200 Subject: [PATCH] init commit --- .gitignore | 3 + SERVER_README.md | 171 ++++++++++++++++++++ package-lock.json | 257 ++++++++++++++++++++++++++++++ package.json | 25 +++ src/config.ts | 19 +++ src/index.ts | 8 + src/models.ts | 40 +++++ src/service.ts | 214 +++++++++++++++++++++++++ src/simple-server.ts | 372 +++++++++++++++++++++++++++++++++++++++++++ src/utils.ts | 7 + test-server.js | 39 +++++ tsconfig.json | 25 +++ 12 files changed, 1180 insertions(+) create mode 100644 .gitignore create mode 100644 SERVER_README.md create mode 100644 package-lock.json create mode 100755 package.json create mode 100644 src/config.ts create mode 100644 src/index.ts create mode 100644 src/models.ts create mode 100644 src/service.ts create mode 100644 src/simple-server.ts create mode 100644 src/utils.ts create mode 100644 test-server.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d55725 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +pkg +dist \ No newline at end of file diff --git a/SERVER_README.md b/SERVER_README.md new file mode 100644 index 0000000..66559c2 --- /dev/null +++ b/SERVER_README.md @@ -0,0 +1,171 @@ +# 4NK Protocol Server + +A high-availability server that automatically handles protocol operations for the 4NK network. + +## Features + +- **High Availability**: Runs continuously on a server +- **Automatic Operations**: Handles UPDATE_PROCESS, NOTIFY_UPDATE, and VALIDATE_STATE operations +- **Protocol Compatible**: Uses the same message format as the browser client +- **WebSocket Interface**: Real-time communication with clients + +## Quick Start + +### 1. Install Dependencies +```bash +npm install +``` + +### 2. Build the Server +```bash +npm run build:server +``` + +### 3. Start the Server +```bash +npm run start:server +``` + +### 4. Development Mode +```bash +npm run dev:server +``` + +## Configuration + +Create a `.env` file in the root directory: + +```env +PORT=8080 +JWT_SECRET_KEY=your-secret-key-here +DATABASE_PATH=./data/server.db +RELAY_URLS=ws://localhost:8090,ws://relay2.example.com:8090 +LOG_LEVEL=info +``` + +## API Usage + +Connect to the WebSocket server and send messages in the same format as the browser client: + +### Example: Update Process +```javascript +const ws = new WebSocket('ws://localhost:8080'); + +ws.onopen = () => { + ws.send(JSON.stringify({ + type: 'UPDATE_PROCESS', + processId: 'your-process-id', + newData: { field: 'value' }, + privateFields: [], + roles: {}, + accessToken: 'your-access-token', + messageId: 'unique-message-id' + })); +}; + +ws.onmessage = (event) => { + const response = JSON.parse(event.data); + console.log('Response:', response); +}; +``` + +### Example: Notify Update +```javascript +ws.send(JSON.stringify({ + type: 'NOTIFY_UPDATE', + processId: 'your-process-id', + stateId: 'your-state-id', + accessToken: 'your-access-token', + messageId: 'unique-message-id' +})); +``` + +### Example: Validate State +```javascript +ws.send(JSON.stringify({ + type: 'VALIDATE_STATE', + processId: 'your-process-id', + stateId: 'your-state-id', + accessToken: 'your-access-token', + messageId: 'unique-message-id' +})); +``` + +## Deployment + +### Systemd Service (Linux) +```bash +# Create service file +sudo tee /etc/systemd/system/4nk-server.service > /dev/null <=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.1.0.tgz", + "integrity": "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w==", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100755 index 0000000..292d20e --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "sdk_signer", + "version": "1.0.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build_wasm": "wasm-pack build --out-dir ../sdk_signer/pkg ../sdk_client --target nodejs --dev", + "build": "tsc", + "start": "node dist/index.js", + "dev": "ts-node src/index.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.3.3", + "ts-node": "^10.9.2" + }, + "dependencies": { + "ws": "^8.14.2", + "@types/ws": "^8.5.10", + "dotenv": "^16.3.1" + } +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..27b48f3 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,19 @@ +import dotenv from 'dotenv'; + +// Load environment variables from .env file +dotenv.config(); + +export const config = { + port: parseInt(process.env.PORT || '8080'), + apiKey: process.env.API_KEY || 'your-api-key-change-this', + databasePath: process.env.DATABASE_PATH || './data/server.db', + relayUrls: process.env.RELAY_URLS?.split(',') || ['ws://localhost:8090'], + autoRestart: process.env.AUTO_RESTART === 'true', + maxRestarts: parseInt(process.env.MAX_RESTARTS || '10'), + logLevel: process.env.LOG_LEVEL || 'info' +}; + +// Validate required environment variables +if (!config.apiKey || config.apiKey === 'your-api-key-change-this') { + console.warn('āš ļø Warning: Using default API key. Set API_KEY environment variable for production.'); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d7bdf1e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,8 @@ +// Main entry point for the SDK Signer server +export { Service } from './service'; +export { config } from './config'; +export { MessageType } from './models'; +export { isValid32ByteHex } from './utils'; + +// Re-export the main server class +export { Server } from './simple-server'; \ No newline at end of file diff --git a/src/models.ts b/src/models.ts new file mode 100644 index 0000000..de90b03 --- /dev/null +++ b/src/models.ts @@ -0,0 +1,40 @@ +// Server-specific model definitions +export enum MessageType { + // Establish connection and keep alive + LISTENING = 'LISTENING', + REQUEST_LINK = 'REQUEST_LINK', + LINK_ACCEPTED = 'LINK_ACCEPTED', + ERROR = 'ERROR', + VALIDATE_TOKEN = 'VALIDATE_TOKEN', + RENEW_TOKEN = 'RENEW_TOKEN', + // Get various information + GET_PAIRING_ID = 'GET_PAIRING_ID', + GET_PROCESSES = 'GET_PROCESSES', + GET_MY_PROCESSES = 'GET_MY_PROCESSES', + PROCESSES_RETRIEVED = 'PROCESSES_RETRIEVED', + RETRIEVE_DATA = 'RETRIEVE_DATA', + DATA_RETRIEVED = 'DATA_RETRIEVED', + DECODE_PUBLIC_DATA = 'DECODE_PUBLIC_DATA', + PUBLIC_DATA_DECODED = 'PUBLIC_DATA_DECODED', + GET_MEMBER_ADDRESSES = 'GET_MEMBER_ADDRESSES', + MEMBER_ADDRESSES_RETRIEVED = 'MEMBER_ADDRESSES_RETRIEVED', + // Processes + CREATE_PROCESS = 'CREATE_PROCESS', + PROCESS_CREATED = 'PROCESS_CREATED', + UPDATE_PROCESS = 'UPDATE_PROCESS', + PROCESS_UPDATED = 'PROCESS_UPDATED', + NOTIFY_UPDATE = 'NOTIFY_UPDATE', + UPDATE_NOTIFIED = 'UPDATE_NOTIFIED', + VALIDATE_STATE = 'VALIDATE_STATE', + STATE_VALIDATED = 'STATE_VALIDATED', + // Hash and merkle proof + HASH_VALUE = 'HASH_VALUE', + VALUE_HASHED = 'VALUE_HASHED', + GET_MERKLE_PROOF = 'GET_MERKLE_PROOF', + MERKLE_PROOF_RETRIEVED = 'MERKLE_PROOF_RETRIEVED', + VALIDATE_MERKLE_PROOF = 'VALIDATE_MERKLE_PROOF', + MERKLE_PROOF_VALIDATED = 'MERKLE_PROOF_VALIDATED', + // Account management + ADD_DEVICE = 'ADD_DEVICE', + DEVICE_ADDED = 'DEVICE_ADDED', +} \ No newline at end of file diff --git a/src/service.ts b/src/service.ts new file mode 100644 index 0000000..141cd2f --- /dev/null +++ b/src/service.ts @@ -0,0 +1,214 @@ +// Simple server service with core protocol methods using WASM SDK +import * as wasm from '../pkg/sdk_client'; +import { ApiReturn, Device, HandshakeMessage, Member, MerkleProofResult, OutPointProcessMap, Process, ProcessState, RoleDefinition, SecretsStore, UserDiff } from '../pkg/sdk_client'; + +export class Service { + private static instance: Service; + private processes: Map = new Map(); + private membersList: any = {}; + + private constructor() { + console.log('šŸ”§ Service initialized'); + this.initWasm(); + } + + private initWasm() { + try { + console.log('šŸ”§ Initializing WASM SDK...'); + wasm.setup(); + console.log('āœ… WASM SDK initialized successfully'); + } catch (error) { + console.error('āŒ Failed to initialize WASM SDK:', error); + throw error; + } + } + + static getInstance(): Service { + if (!Service.instance) { + Service.instance = new Service(); + } + return Service.instance; + } + + // Core protocol method: Create PRD Update + async createPrdUpdate(processId: string, stateId: string): Promise { + console.log(`šŸ“¢ Creating PRD update for process ${processId}, state ${stateId}`); + + try { + // Get the process from cache + const process = this.processes.get(processId); + if (!process) { + throw new Error('Process not found'); + } + + // Find the state + const state = process.states.find((s: any) => s.state_id === stateId); + if (!state) { + throw new Error('State not found'); + } + + // Use WASM function to create update message + const result = wasm.create_update_message(process, stateId, this.membersList); + + if (result.updated_process) { + // Update our cache + this.processes.set(processId, result.updated_process.current_process); + + return result; + } else { + throw new Error('Failed to create update message'); + } + } catch (error) { + throw new Error(`WASM error: ${error}`); + } + } + + // Core protocol method: Approve Change (Validate State) + async approveChange(processId: string, stateId: string): Promise { + console.log(`āœ… Approving change for process ${processId}, state ${stateId}`); + + try { + // Get the process from cache + const process = this.processes.get(processId); + if (!process) { + throw new Error('Process not found'); + } + + // Find the state + const state = process.states.find((s: any) => s.state_id === stateId); + if (!state) { + throw new Error('State not found'); + } + + // Use WASM function to validate state + const result = wasm.validate_state(process, stateId, this.membersList); + + if (result.updated_process) { + // Update our cache + this.processes.set(processId, result.updated_process.current_process); + + return result; + } else { + throw new Error('Failed to validate state'); + } + } catch (error) { + throw new Error(`WASM error: ${error}`); + } + } + + // Core protocol method: Update Process + async updateProcess( + process: any, + privateData: Record, + publicData: Record, + roles: Record | null + ): Promise { + console.log(`šŸ”„ Updating process ${process.states[0]?.state_id || 'unknown'}`); + console.log('Private data:', privateData); + console.log('Public data:', publicData); + console.log('Roles:', roles); + + try { + // Convert data to WASM format + const newAttributes = wasm.encode_json(privateData); + const newPublicData = wasm.encode_json(publicData); + const newRoles = roles || process.states[0]?.roles || {}; + + // Use WASM function to update process + const result = wasm.update_process(process, newAttributes, newRoles, newPublicData, this.membersList); + + if (result.updated_process) { + // Update our cache + this.processes.set(process.states[0]?.state_id || 'unknown', result.updated_process.current_process); + + return result; + } else { + throw new Error('Failed to update process'); + } + } catch (error) { + throw new Error(`WASM error: ${error}`); + } + } + + // Utility method: Get Process + async getProcess(processId: string): Promise { + return this.processes.get(processId) || null; + } + + // Utility method: Create a test process + createTestProcess(processId: string): any { + console.log(`šŸ”§ Creating test process: ${processId}`); + + try { + // Create test data + const privateData = wasm.encode_json({ secret: 'initial_secret' }); + const publicData = wasm.encode_json({ name: 'Test Process', created: Date.now() }); + const roles = { admin: { members: [], validation_rules: [], storages: [] } }; + const relayAddress = 'test_relay_address'; + const feeRate = 1; + + // Use WASM to create new process + const result = wasm.create_new_process(privateData, roles, publicData, relayAddress, feeRate, this.membersList); + + if (result.updated_process) { + const process = result.updated_process.current_process; + this.processes.set(processId, process); + console.log(`āœ… Test process created: ${processId}`); + return process; + } else { + throw new Error('Failed to create test process'); + } + } catch (error) { + console.error('Error creating test process:', error); + throw error; + } + } + + // Utility method: Check if device is paired + isPaired(): boolean { + try { + return wasm.is_paired(); + } catch (error) { + console.error('Error checking if paired:', error); + throw error; + } + } + + // Utility method: Get last committed state + getLastCommitedState(process: any): any { + return process.states.find((s: any) => s.commited_in) || null; + } + + // Utility method: Get last committed state index + getLastCommitedStateIndex(process: any): number | null { + const index = process.states.findIndex((s: any) => s.commited_in); + return index >= 0 ? index : null; + } + + // Utility method: Check if roles contain current user + rolesContainsUs(roles: Record): boolean { + try { + // This would need to be implemented based on your user management + // For now, return true for testing + return true; + } catch (error) { + console.error('Error checking roles:', error); + return true; // Fallback to true for testing + } + } + + // Utility method: Add member to the members list + addMember(outpoint: string, member: any) { + this.membersList[outpoint] = member; + } + + // Utility method: Get device address + getDeviceAddress(): string { + try { + return wasm.get_address(); + } catch (error) { + console.error('Error getting device address:', error); + throw error; + } + } +} diff --git a/src/simple-server.ts b/src/simple-server.ts new file mode 100644 index 0000000..cd23097 --- /dev/null +++ b/src/simple-server.ts @@ -0,0 +1,372 @@ +import WebSocket from 'ws'; +import { MessageType } from './models'; +import { config } from './config'; +import { Service } from './service'; +import { ApiReturn } from '../pkg/sdk_client'; + +interface ServerMessageEvent { + data: { + type: MessageType; + messageId?: string; + apiKey?: string; + [key: string]: any; + }; + clientId: string; +} + +interface ServerResponse { + type: MessageType; + messageId?: string; + [key: string]: any; +} + +class SimpleProcessHandlers { + private apiKey: string; + private service: Service; + + constructor(apiKey: string, service: Service) { + this.apiKey = apiKey; + this.service = service; + } + + private errorResponse = (errorMsg: string, clientId: string, messageId?: string): ServerResponse => { + return { + type: MessageType.ERROR, + error: errorMsg, + messageId + }; + }; + + private validateApiKey(apiKey: string): boolean { + return apiKey === this.apiKey; + } + + async handleNotifyUpdate(event: ServerMessageEvent): Promise { + if (event.data.type !== MessageType.NOTIFY_UPDATE) { + throw new Error('Invalid message type'); + } + + try { + const { processId, stateId, apiKey } = event.data; + + if (!apiKey || !this.validateApiKey(apiKey)) { + throw new Error('Invalid API key'); + } + + // Check if device is paired + if (!this.service.isPaired()) { + throw new Error('Device not paired'); + } + + // Create test process if it doesn't exist + let process = await this.service.getProcess(processId); + if (!process) { + process = this.service.createTestProcess(processId); + } + + let res: ApiReturn; + try { + res = await this.service.createPrdUpdate(processId, stateId); + } catch (e) { + throw new Error(e as string); + } + + return { + type: MessageType.UPDATE_NOTIFIED, + messageId: event.data.messageId + }; + } catch (e) { + const errorMsg = `Failed to notify update for process: ${e}`; + return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + } + } + + async handleValidateState(event: ServerMessageEvent): Promise { + if (event.data.type !== MessageType.VALIDATE_STATE) { + throw new Error('Invalid message type'); + } + + try { + const { processId, stateId, apiKey } = event.data; + + if (!apiKey || !this.validateApiKey(apiKey)) { + throw new Error('Invalid API key'); + } + + // Check if device is paired + if (!this.service.isPaired()) { + throw new Error('Device not paired'); + } + + // Create test process if it doesn't exist + let process = await this.service.getProcess(processId); + if (!process) { + process = this.service.createTestProcess(processId); + } + + // Execute actual protocol logic + let res: ApiReturn; + try { + res = await this.service.approveChange(processId, stateId); + + } catch (e) { + throw new Error(e as string); + } + + return { + type: MessageType.STATE_VALIDATED, + validatedProcess: res.updated_process, + messageId: event.data.messageId + }; + } catch (e) { + const errorMsg = `Failed to validate process: ${e}`; + return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + } + } + + async handleUpdateProcess(event: ServerMessageEvent): Promise { + if (event.data.type !== MessageType.UPDATE_PROCESS) { + throw new Error('Invalid message type'); + } + + try { + const { processId, newData, privateFields, roles, apiKey } = event.data; + + if (!apiKey || !this.validateApiKey(apiKey)) { + throw new Error('Invalid API key'); + } + + // Check if device is paired + if (!this.service.isPaired()) { + throw new Error('Device not paired'); + } + + // Get or create the process + let process = await this.service.getProcess(processId); + if (!process) { + process = this.service.createTestProcess(processId); + } + + // Get the last committed state + let lastState = this.service.getLastCommitedState(process); + if (!lastState) { + const firstState = process.states[0]; + const roles = firstState.roles; + if (this.service.rolesContainsUs(roles)) { + const approveChangeRes = await this.service.approveChange(processId, firstState.state_id); + const prdUpdateRes = await this.service.createPrdUpdate(processId, firstState.state_id); + } else { + if (firstState.validation_tokens.length > 0) { + const res = await this.service.createPrdUpdate(processId, firstState.state_id); + } + } + // Wait a couple seconds + await new Promise(resolve => setTimeout(resolve, 2000)); + const updatedProcess = await this.service.getProcess(processId); + if (!updatedProcess) { + throw new Error('Failed to get updated process'); + } + process = updatedProcess; + lastState = this.service.getLastCommitedState(process); + if (!lastState) { + throw new Error('Process doesn\'t have a committed state yet'); + } + } + + const lastStateIndex = this.service.getLastCommitedStateIndex(process); + if (lastStateIndex === null) { + throw new Error('Process doesn\'t have a committed state yet'); + } + + // Split data into private and public + const privateData: Record = {}; + const publicData: Record = {}; + + for (const field of Object.keys(newData)) { + // Public data are carried along each new state + if (lastState.public_data[field]) { + publicData[field] = newData[field]; + continue; + } + + // If it's not a public data, it may be either a private data update, or a new field + if (privateFields.includes(field)) { + privateData[field] = newData[field]; + continue; + } + + // Check if field exists in previous states private data + for (let i = lastStateIndex; i >= 0; i--) { + const state = process.states[i]; + if (state.pcd_commitment[field]) { + privateData[field] = newData[field]; + break; + } + } + + if (privateData[field]) continue; + + // It's a new public field + publicData[field] = newData[field]; + } + + let res: ApiReturn; + try { + res = await this.service.updateProcess(process, privateData, publicData, roles); + } catch (e) { + throw new Error(e as string); + } + + return { + type: MessageType.PROCESS_UPDATED, + updatedProcess: res.updated_process, + messageId: event.data.messageId + }; + } catch (e) { + const errorMsg = `Failed to update process: ${e}`; + return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + } + } + + async handleMessage(event: ServerMessageEvent): Promise { + try { + switch (event.data.type) { + case MessageType.NOTIFY_UPDATE: + return await this.handleNotifyUpdate(event); + case MessageType.VALIDATE_STATE: + return await this.handleValidateState(event); + case MessageType.UPDATE_PROCESS: + return await this.handleUpdateProcess(event); + default: + throw new Error(`Unhandled message type: ${event.data.type}`); + } + } catch (error) { + const errorMsg = `Error handling message: ${error}`; + return this.errorResponse(errorMsg, event.clientId, event.data.messageId); + } + } +} + +export class Server { + private wss: WebSocket.Server; + private handlers!: SimpleProcessHandlers; + private clients: Map = new Map(); + + constructor(port: number = 8080) { + this.wss = new WebSocket.Server({ port }); + this.init(); + } + + private async init() { + try { + console.log('šŸš€ Initializing Simple 4NK Protocol Server...'); + + // Initialize service + const service = Service.getInstance(); + + // Initialize handlers with API key and service + this.handlers = new SimpleProcessHandlers(config.apiKey, service); + + // Setup WebSocket handlers + this.setupWebSocketHandlers(); + + console.log(`āœ… Simple server running on port ${this.wss.options.port}`); + console.log('šŸ“‹ Supported operations: UPDATE_PROCESS, NOTIFY_UPDATE, VALIDATE_STATE'); + console.log('šŸ”‘ Authentication: API key required for all operations'); + console.log('šŸ”§ Services: Integrated with SimpleService protocol logic'); + + } catch (error) { + console.error('āŒ Failed to initialize server:', error); + process.exit(1); + } + } + + private setupWebSocketHandlers() { + this.wss.on('connection', (ws: WebSocket, req) => { + const clientId = this.generateClientId(); + this.clients.set(ws, clientId); + + console.log(`šŸ”— Client connected: ${clientId} from ${req.socket.remoteAddress}`); + + // Send listening message + this.sendToClient(ws, { + type: MessageType.LISTENING, + clientId + }); + + ws.on('message', async (data: WebSocket.Data) => { + try { + const message = JSON.parse(data.toString()); + console.log(`šŸ“Ø Received message from ${clientId}:`, message.type); + + const serverEvent: ServerMessageEvent = { + data: message, + clientId + }; + + const response = await this.handlers.handleMessage(serverEvent); + this.sendToClient(ws, response); + + } catch (error) { + console.error(`āŒ Error handling message from ${clientId}:`, error); + this.sendToClient(ws, { + type: MessageType.ERROR, + error: `Server error: ${error instanceof Error ? error.message : String(error)}`, + messageId: JSON.parse(data.toString())?.messageId + }); + } + }); + + ws.on('close', () => { + console.log(`šŸ”Œ Client disconnected: ${clientId}`); + this.clients.delete(ws); + }); + + ws.on('error', (error) => { + console.error(`āŒ WebSocket error for ${clientId}:`, error); + this.clients.delete(ws); + }); + }); + + this.wss.on('error', (error) => { + console.error('āŒ WebSocket server error:', error); + }); + } + + private sendToClient(ws: WebSocket, response: ServerResponse) { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify(response)); + } + } + + private generateClientId(): string { + return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + public shutdown() { + console.log('šŸ›‘ Shutting down server...'); + this.wss.close(() => { + console.log('āœ… Server shutdown complete'); + process.exit(0); + }); + } +} + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nšŸ›‘ Received SIGINT, shutting down gracefully...'); + if (server) { + server.shutdown(); + } +}); + +process.on('SIGTERM', () => { + console.log('\nšŸ›‘ Received SIGTERM, shutting down gracefully...'); + if (server) { + server.shutdown(); + } +}); + +// Start the server +const port = parseInt(process.env.PORT || '8080'); +const server = new Server(port); \ No newline at end of file diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..cc243bb --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,7 @@ +// Server-specific utility functions + +export function isValid32ByteHex(value: string): boolean { + // Check if the value is a valid 32-byte hex string (64 characters) + const hexRegex = /^[0-9a-fA-F]{64}$/; + return hexRegex.test(value); +} \ No newline at end of file diff --git a/test-server.js b/test-server.js new file mode 100644 index 0000000..a0c5d5f --- /dev/null +++ b/test-server.js @@ -0,0 +1,39 @@ +const WebSocket = require('ws'); + +console.log('šŸ” Testing WebSocket server connection...'); + +const ws = new WebSocket('ws://localhost:8080'); + +ws.on('open', function open() { + console.log('āœ… Connected to server!'); + + // Send a simple test message + const testMessage = { + type: 'LISTENING', + messageId: 'test-123' + }; + + ws.send(JSON.stringify(testMessage)); +}); + +ws.on('message', function message(data) { + console.log('šŸ“Ø Received from server:', data.toString()); + + // Close connection after receiving response + setTimeout(() => { + ws.close(); + console.log('šŸ”Œ Connection closed'); + process.exit(0); + }, 1000); +}); + +ws.on('error', function error(err) { + console.error('āŒ Connection error:', err.message); + process.exit(1); +}); + +// Timeout after 5 seconds +setTimeout(() => { + console.error('āŒ Connection timeout'); + process.exit(1); +}, 5000); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..20a0603 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "target": "ES2020", + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "lib": ["ES2020"], + "declaration": false, + "sourceMap": true + }, + "include": [ + "src/**/*", + "pkg/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file