@joystick.js/db-canary 0.0.0-canary.2221 → 0.0.0-canary.2223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/cluster/master.js +4 -4
- package/dist/server/cluster/worker.js +1 -1
- package/dist/server/index.js +1 -1
- package/package.json +2 -2
- package/src/server/cluster/master.js +13 -2
- package/src/server/cluster/worker.js +14 -2
- package/src/server/index.js +14 -2
- package/tests/server/integration/development_mode_authentication.test.js +195 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import a from"cluster";import l from"os";import{EventEmitter as u}from"events";import{writeFileSync as p}from"fs";import{load_settings as _,get_port_configuration as h}from"../lib/load_settings.js";import{restore_backup as
|
|
1
|
+
import a from"cluster";import l from"os";import{EventEmitter as u}from"events";import{writeFileSync as p}from"fs";import{load_settings as _,get_settings as w,get_port_configuration as h}from"../lib/load_settings.js";import{restore_backup as g,start_backup_schedule as f,stop_backup_schedule as m}from"../lib/backup_manager.js";import k from"../lib/logger.js";import{initialize_database as y,check_and_grow_map_size as b,cleanup_database as v}from"../lib/query_engine.js";import{setup_authentication as q,verify_password as D,initialize_auth_manager as x}from"../lib/auth_manager.js";import E from"../lib/operations/insert_one.js";import T from"../lib/operations/update_one.js";import S from"../lib/operations/delete_one.js";import W from"../lib/operations/bulk_write.js";import F from"../lib/operations/find_one.js";import N from"../lib/operations/find.js";import P from"../lib/operations/create_index.js";import z from"../lib/operations/drop_index.js";import O from"../lib/operations/get_indexes.js";import{start_http_server as R,stop_http_server as A,is_setup_required as I}from"../lib/http_server.js";import{is_development_mode as j,display_development_startup_message as L}from"../lib/development_mode.js";import{initialize_api_key_manager as M}from"../lib/api_key_manager.js";class $ extends u{constructor(r={}){super(),this.workers=new Map,this.write_queue=[],this.processing_writes=!1,this.authenticated_sessions=new Map,this.worker_count=r.worker_count||l.cpus().length,this.port=r.port||1983,this.settings_file=r.settings_file||"settings.db.json",this.settings=null,this.pending_writes=new Map,this.write_id_counter=0,this.shutting_down=!1,this.master_id=`master_${Date.now()}_${Math.random()}`;const{create_context_logger:e}=k("master");this.log=e({port:this.port,worker_count:this.worker_count,master_id:this.master_id}),this.setup_master()}setup_master(){a.setupPrimary({exec:new URL("./index.js",import.meta.url).pathname,args:[],silent:!1}),a.on("exit",(r,e,o)=>{this.log.warn("Worker died",{worker_pid:r.process.pid,exit_code:e,signal:o}),this.handle_worker_death(r)}),a.on("message",(r,e)=>{this.handle_worker_message(r,e)})}async start(){const r=Date.now();try{this.settings=_(this.settings_file),this.log.info("Settings loaded successfully",{settings_file:this.settings_file});let e="./data";try{const t=w();t?.data_path&&(e=t.data_path)}catch{}if(y(e),x(),await M(),this.log.info("Database and auth manager initialized"),this.settings?.restore_from)try{this.log.info("Startup restore requested",{backup_filename:this.settings.restore_from});const t=await g(this.settings.restore_from);this.log.info("Startup restore completed",{backup_filename:this.settings.restore_from,duration_ms:t.duration_ms});const s={...this.settings};delete s.restore_from,p(this.settings_file,JSON.stringify(s,null,2)),this.settings=_(this.settings_file),this.log.info("Removed restore_from from settings after successful restore")}catch(t){this.log.error("Startup restore failed",{backup_filename:this.settings.restore_from,error:t.message}),this.log.info("Continuing with existing database after restore failure")}if(this.settings?.s3)try{f(),this.log.info("Backup scheduling started")}catch(t){this.log.warn("Failed to start backup scheduling",{error:t.message})}if(I())try{const{http_port:t}=h();await R(t)&&this.log.info("HTTP setup server started",{http_port:t})}catch(t){this.log.warn("Failed to start HTTP setup server",{error:t.message})}for(let t=0;t<this.worker_count;t++)this.spawn_worker();if(j()){const{tcp_port:t,http_port:s}=h();L(t,s)}const o=Date.now()-r;this.log.info("Master process started successfully",{workers_spawned:this.worker_count,startup_duration_ms:o})}catch(e){this.log.error("Failed to start master process",{error:e.message}),process.exit(1)}}spawn_worker(){const r=Date.now();this.log.info("Spawning worker");const e=a.fork({WORKER_PORT:this.port,WORKER_SETTINGS:JSON.stringify(this.settings)});this.workers.set(e.id,{worker:e,connections:0,last_heartbeat:Date.now(),status:"starting"});const o=Date.now()-r;return this.log.info("Worker spawned successfully",{worker_id:e.id,worker_pid:e.process.pid,spawn_duration_ms:o}),e}handle_worker_death(r){this.workers.delete(r.id),this.shutting_down||(this.log.info("Respawning worker after death",{dead_worker_id:r.id,respawn_delay_ms:1e3}),setTimeout(()=>{this.spawn_worker()},1e3))}handle_worker_message(r,e){switch(e.type){case"worker_ready":this.handle_worker_ready_for_config(r,e);break;case"server_ready":this.handle_worker_server_ready(r,e);break;case"write_request":this.handle_write_request(r,e);break;case"auth_request":this.handle_auth_request(r,e);break;case"setup_request":this.handle_setup_request(r,e);break;case"connection_count":this.update_worker_connections(r,e);break;case"heartbeat":this.handle_worker_heartbeat(r,e);break;default:this.log.warn("Unknown message type received",{message_type:e.type,worker_id:r.id})}}handle_worker_ready_for_config(r,e){this.log.info("Worker ready for config, sending configuration",{worker_id:r.id,worker_pid:r.process.pid,master_id:this.master_id}),r.send({type:"config",data:{port:this.port,settings:this.settings,master_id:this.master_id}})}handle_worker_server_ready(r,e){const o=this.workers.get(r.id);o&&(o.status="ready",this.log.info("Worker server ready",{worker_id:r.id,worker_pid:r.process.pid}))}async handle_write_request(r,e){if(this.shutting_down){r.send({type:"write_response",data:{write_id:e.data.write_id,success:!1,error:"Server is shutting down"}});return}const{write_id:o,op_type:t,data:s,socket_id:i}=e.data;try{const n={write_id:o,worker_id:r.id,op_type:t,data:s,socket_id:i,timestamp:Date.now()};this.write_queue.push(n),this.process_write_queue()}catch(n){r.send({type:"write_response",data:{write_id:o,success:!1,error:n.message}})}}async process_write_queue(){if(!(this.processing_writes||this.write_queue.length===0)){for(this.processing_writes=!0;this.write_queue.length>0;){const r=this.write_queue.shift();await this.execute_write_operation(r)}this.processing_writes=!1,this.shutting_down&&this.write_queue.length===0&&this.emit("writes_completed")}}async execute_write_operation(r){const{write_id:e,worker_id:o,op_type:t,data:s,socket_id:i}=r,n=this.workers.get(o);if(!n){this.log.error("Worker not found for write operation",{worker_id:o});return}try{const c=await this.perform_database_operation(t,s);n.worker.send({type:"write_response",data:{write_id:e,success:!0,result:c}}),this.broadcast_write_notification(t,s,o)}catch(c){this.log.error("Write operation failed",{write_id:e,op_type:t,worker_id:o,error_message:c.message}),n.worker.send({type:"write_response",data:{write_id:e,success:!1,error:c.message}})}}async perform_database_operation(r,e){const o=Date.now();this.log.info("Executing database operation",{op_type:r});try{let t;const s=e.database||"default";switch(r){case"insert_one":t=await E(s,e.collection,e.document,e.options);break;case"update_one":t=await T(s,e.collection,e.filter,e.update,e.options);break;case"delete_one":t=await S(s,e.collection,e.filter,e.options);break;case"bulk_write":t=await W(s,e.collection,e.operations,e.options);break;case"find_one":t=await F(s,e.collection,e.filter,e.options);break;case"find":t=await N(s,e.collection,e.filter,e.options);break;case"create_index":t=await P(s,e.collection,e.field,e.options);break;case"drop_index":t=await z(s,e.collection,e.field);break;case"get_indexes":t=await O(s,e.collection);break;default:throw new Error(`Unsupported database operation: ${r}`)}const i=Date.now()-o;return this.log.log_operation(r,i,{result:t}),["find_one","find","get_indexes"].includes(r)||setImmediate(()=>b()),t}catch(t){const s=Date.now()-o;throw this.log.error("Database operation failed",{op_type:r,duration_ms:s,error_message:t.message}),t}}broadcast_write_notification(r,e,o){const t={type:"write_notification",data:{op_type:r,data:e,timestamp:Date.now()}};for(const[s,i]of this.workers)s!==o&&i.status==="ready"&&i.worker.send(t)}async handle_auth_request(r,e){const{auth_id:o,socket_id:t,password:s}=e.data;try{const i=await D(s,"cluster_client");i&&this.authenticated_sessions.set(t,{authenticated_at:Date.now(),worker_id:r.id}),r.send({type:"auth_response",data:{auth_id:o,success:i,message:i?"Authentication successful":"Authentication failed"}})}catch(i){r.send({type:"auth_response",data:{auth_id:o,success:!1,message:`Authentication error: ${i.message}`}})}}handle_setup_request(r,e){const{setup_id:o,socket_id:t}=e.data;try{const s=q(),i=`===
|
|
2
2
|
JoystickDB Setup
|
|
3
3
|
|
|
4
4
|
Your database has been setup. Follow the instructions below carefully to avoid issues.
|
|
5
5
|
|
|
6
6
|
Password:
|
|
7
|
-
${
|
|
7
|
+
${s}
|
|
8
8
|
|
|
9
9
|
Store this password in your environment settings or another secure location. When connecting a client, make sure to provide this password via the client's options object like this:
|
|
10
10
|
|
|
@@ -13,8 +13,8 @@ import joystickdb from '@joystickdb/client';
|
|
|
13
13
|
const client = joystickdb.client({
|
|
14
14
|
host: 'localhost',
|
|
15
15
|
port: 1983,
|
|
16
|
-
password: '${
|
|
16
|
+
password: '${s}'
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
await client.ping();
|
|
20
|
-
===`;
|
|
20
|
+
===`;r.send({type:"setup_response",data:{setup_id:o,success:!0,password:s,instructions:i,message:"Authentication setup completed successfully"}})}catch(s){r.send({type:"setup_response",data:{setup_id:o,success:!1,error:s.message}})}}update_worker_connections(r,e){const o=this.workers.get(r.id);o&&(o.connections=e.data.count)}handle_worker_heartbeat(r,e){const o=this.workers.get(r.id);o&&(o.last_heartbeat=Date.now())}get_cluster_stats(){const r={master_pid:process.pid,worker_count:this.workers.size,total_connections:0,write_queue_length:this.write_queue.length,authenticated_sessions:this.authenticated_sessions.size,workers:[]};for(const[e,o]of this.workers)r.total_connections+=o.connections,r.workers.push({id:e,pid:o.worker.process.pid,connections:o.connections,status:o.status,last_heartbeat:o.last_heartbeat});return r}async shutdown(){const r=Date.now();this.log.info("Initiating graceful shutdown"),this.shutting_down=!0;try{await A(),this.log.info("HTTP server stopped")}catch(t){this.log.warn("Failed to stop HTTP server",{error:t.message})}try{m(),this.log.info("Backup scheduling stopped")}catch(t){this.log.warn("Failed to stop backup scheduling",{error:t.message})}for(const[t,s]of this.workers)try{s.worker.send({type:"shutdown"})}catch(i){this.log.warn("Error sending shutdown signal to worker",{worker_id:t,error:i.message})}this.write_queue.length>0&&(this.log.info("Waiting for pending writes to complete",{pending_writes:this.write_queue.length}),await new Promise(t=>{const s=setTimeout(()=>{this.log.warn("Timeout waiting for writes to complete, proceeding with shutdown"),t()},process.env.NODE_ENV==="test"?1e3:5e3);this.once("writes_completed",()=>{clearTimeout(s),t()})})),this.log.info("All writes completed, disconnecting workers");for(const[t,s]of this.workers)try{s.worker.disconnect()}catch(i){this.log.warn("Error disconnecting worker",{worker_id:t,error:i.message})}const e=process.env.NODE_ENV==="test"?500:3e3;await new Promise(t=>{const s=setTimeout(()=>{for(const[n,c]of this.workers){this.log.warn("Force killing worker after timeout",{worker_id:n});try{c.worker.kill("SIGKILL")}catch(d){this.log.warn("Error force killing worker",{worker_id:n,error:d.message})}}this.workers.clear(),t()},e),i=()=>{this.workers.size===0?(clearTimeout(s),t()):setTimeout(i,50)};i()});try{v(),this.log.info("Database cleanup completed")}catch(t){this.log.warn("Error during database cleanup",{error:t.message})}if(this.authenticated_sessions.clear(),this.write_queue.length=0,this.pending_writes.clear(),process.env.NODE_ENV==="test")try{for(const t in a.workers){const s=a.workers[t];if(s&&!s.isDead()){this.log.info("Force killing remaining cluster worker",{worker_id:t,worker_pid:s.process.pid});try{s.kill("SIGKILL")}catch(i){this.log.warn("Error force killing remaining worker",{worker_id:t,error:i.message})}}}for(const t in a.workers)delete a.workers[t];a.removeAllListeners(),this.log.info("Aggressive cluster cleanup completed for test environment")}catch(t){this.log.warn("Error during aggressive cluster cleanup",{error:t.message})}const o=Date.now()-r;this.log.info("Shutdown complete",{shutdown_duration_ms:o})}}var de=$;export{de as default};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import h from"net";import p from"../lib/op_types.js";import{send_success as u,send_error as d,send_message as l}from"../lib/send_response.js";import{shutdown_write_queue as g}from"../lib/write_queue.js";import{create_message_parser as w,encode_message as c}from"../lib/tcp_protocol.js";import m from"../lib/logger.js";import{initialize_database as f,cleanup_database as y}from"../lib/query_engine.js";import{handle_admin_operation as b,handle_ping_operation as v}from"../lib/operation_dispatcher.js";class
|
|
1
|
+
import h from"net";import p from"../lib/op_types.js";import{send_success as u,send_error as d,send_message as l}from"../lib/send_response.js";import{shutdown_write_queue as g}from"../lib/write_queue.js";import{create_message_parser as w,encode_message as c}from"../lib/tcp_protocol.js";import m from"../lib/logger.js";import{initialize_database as f,cleanup_database as y}from"../lib/query_engine.js";import{handle_admin_operation as b,handle_ping_operation as v}from"../lib/operation_dispatcher.js";import{get_settings as k}from"../lib/load_settings.js";class ${constructor(){this.server=null,this.connections=new Map,this.connection_count=0,this.settings=null,this.port=null,this.write_id_counter=0,this.pending_writes=new Map,this.authenticated_clients=new Set,this.heartbeat_interval=null;const{create_context_logger:e}=m("worker");this.log=e({worker_pid:process.pid}),this.setup_worker()}setup_worker(){process.on("message",e=>{this.handle_master_message(e)}),process.on("SIGTERM",()=>{this.shutdown()}),process.on("SIGINT",()=>{this.shutdown()}),this.send_heartbeat(),this.heartbeat_interval=setInterval(()=>{this.send_heartbeat()},5e3),process.connected&&process.send({type:"worker_ready"})}handle_master_message(e){switch(e.type){case"config":this.handle_config(e);break;case"write_response":this.handle_write_response(e);break;case"auth_response":this.handle_auth_response(e);break;case"setup_response":this.handle_setup_response(e);break;case"write_notification":this.handle_write_notification(e);break;case"shutdown":this.shutdown();break;default:this.log.warn("Unknown message type received from master",{message_type:e.type})}}handle_config(e){const t=e.data.master_id;if(this.master_id&&this.master_id!==t){this.log.info("Worker already configured by different master, ignoring config message",{current_master_id:this.master_id,incoming_master_id:t,current_port:this.port,new_port:e.data.port});return}if(this.port!==null&&this.master_id===t){this.log.info("Worker already configured by same master, ignoring duplicate config message",{master_id:t,current_port:this.port,new_port:e.data.port});return}this.log.info("Received config message",{port:e.data.port,master_id:t}),this.port=e.data.port,this.settings=e.data.settings,this.master_id=t;try{let s="./data";try{const r=k();r?.data_path&&(s=r.data_path)}catch{}f(s),this.log.info("Database initialized in worker process",{database_path:s})}catch(s){this.log.error("Failed to initialize database in worker process",{error:s.message})}this.log.info("Starting server",{port:this.port}),this.start_server()}start_server(){this.server=h.createServer(e=>{this.handle_connection(e)}),this.server.listen(this.port,()=>{this.log.info("Server listening",{port:this.port}),process.connected&&process.send({type:"server_ready"})}),this.server.on("error",e=>{this.log.error("Server error",{error:e.message})})}handle_connection(e){const t=`${process.pid}_${Date.now()}_${Math.random()}`;e.id=t,e.message_parser=w(),this.connections.set(t,e),this.connection_count++,this.update_connection_count(),e.on("data",s=>{this.handle_socket_data(e,s)}),e.on("end",()=>{this.handle_socket_end(e)}),e.on("error",s=>{this.log.error("Socket error",{socket_id:t,error_message:s.message}),this.handle_socket_end(e)})}handle_socket_data(e,t){try{const s=e.message_parser.parse_messages(t);for(const r of s){const a=r,i=a?.op||null;if(!i){d(e,{message:"Missing operation type"});continue}if(!this.check_op_type(i)){d(e,{message:"Invalid operation type"});continue}this.route_operation(e,i,a?.data||{})}}catch(s){this.log.error("Data parsing error",{socket_id:e.id,error_message:s.message}),d(e,{message:"Invalid data format"})}}handle_socket_end(e){e.id&&(this.connections.delete(e.id),this.authenticated_clients.delete(e.id),this.connection_count--,this.update_connection_count()),this.log.info("Client disconnected",{socket_id:e.id})}check_op_type(e=""){return e?p.includes(e):!1}route_operation(e,t,s){switch(t){case"authentication":this.handle_authentication(e,s);break;case"setup":this.handle_setup(e,s);break;case"find_one":case"find":case"get_indexes":this.handle_read_operation(e,t,s);break;case"create_index":case"drop_index":this.handle_write_operation(e,t,s);break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":this.handle_write_operation(e,t,s);break;case"ping":v(e);break;case"admin":b(e,s,this.is_authenticated.bind(this));break;default:d(e,{message:`Unsupported operation: ${t}`})}}handle_authentication(e,t){if(this.is_authenticated(e))l(e,"Already authenticated");else{const s=`${e.id}_${Date.now()}`;process.send({type:"auth_request",data:{auth_id:s,socket_id:e.id,password:t.password}}),this.pending_writes.set(s,{socket:e,type:"auth"})}}handle_setup(e,t){const s=`${e.id}_${Date.now()}`;process.send({type:"setup_request",data:{setup_id:s,socket_id:e.id}}),this.pending_writes.set(s,{socket:e,type:"setup"})}handle_read_operation(e,t,s){if(!this.is_authenticated(e)){d(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"read"})}handle_write_operation(e,t,s){if(!this.is_authenticated(e)){d(e,{message:"Authentication required"});return}const r=`${e.id}_${++this.write_id_counter}`;process.send({type:"write_request",data:{write_id:r,op_type:t,data:s,socket_id:e.id}}),this.pending_writes.set(r,{socket:e,type:"write"})}handle_write_response(e){const{write_id:t,success:s,result:r,error:a}=e.data,i=this.pending_writes.get(t);if(!i){this.log.warn("No pending write found",{write_id:t});return}const{socket:o}=i;if(this.pending_writes.delete(t),o.destroyed||!o.writable){this.log.warn("Socket disconnected before response could be sent",{write_id:t});return}try{if(s){let n;Array.isArray(r)?n={ok:1,documents:r}:r&&typeof r=="object"?n={ok:1,...r}:n={ok:1,result:r};const _=c(n);o.write(_)}else{const _=c({ok:0,error:a});o.write(_)}}catch(n){this.log.error("Error sending response to client",{write_id:t,error:n.message})}}handle_auth_response(e){const{auth_id:t,success:s,message:r}=e.data,a=this.pending_writes.get(t);if(!a){this.log.warn("No pending auth found",{auth_id:t});return}const{socket:i}=a;if(this.pending_writes.delete(t),i.destroyed||!i.writable){this.log.warn("Socket disconnected before auth response could be sent",{auth_id:t});return}try{if(s){this.authenticated_clients.add(i.id);const n=c({ok:1,version:"1.0.0",message:r});i.write(n)}else d(i,{message:r}),i.end()}catch(o){this.log.error("Error sending auth response to client",{auth_id:t,error:o.message})}}handle_setup_response(e){const{setup_id:t,success:s,password:r,message:a,error:i}=e.data,o=this.pending_writes.get(t);if(!o){this.log.warn("No pending setup found",{setup_id:t});return}const{socket:n}=o;this.pending_writes.delete(t),s?u(n,{password:r,message:a}):d(n,{message:i})}handle_write_notification(e){this.log.info("Received write notification",{op_type:e.data.op_type,timestamp:e.data.timestamp})}is_authenticated(e){return this.authenticated_clients.has(e.id)}update_connection_count(){process.connected&&process.send({type:"connection_count",data:{count:this.connection_count}})}send_heartbeat(){if(process.connected)try{process.send({type:"heartbeat",data:{timestamp:Date.now()}})}catch{clearInterval(this.heartbeat_interval)}}async shutdown(){const e=Date.now();this.log.info("Initiating graceful shutdown");try{await g(),this.log.info("Write queue shutdown complete")}catch(s){this.log.error("Error shutting down write queue",{error:s.message})}try{await y(),this.log.info("Database cleanup complete")}catch(s){this.log.error("Error cleaning up database",{error:s.message})}this.server&&this.server.close(()=>{this.log.info("Server closed")});for(const[s,r]of this.connections)r.end();const t=process.env.NODE_ENV==="test"?100:5e3;setTimeout(()=>{const s=Date.now()-e;this.log.info("Worker shutdown complete",{shutdown_duration_ms:s}),process.exit(0)},t)}}const z=new $;
|
package/dist/server/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import E from"net";import{decode as q}from"msgpackr";import N from"./lib/op_types.js";import x from"./lib/safe_json_parse.js";import{load_settings as v,get_settings as h,get_port_configuration as b}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as A}from"./cluster/index.js";import T from"./lib/logger.js";import{initialize_database as B,cleanup_database as D}from"./lib/query_engine.js";import{create_message_parser as $,encode_message as c}from"./lib/tcp_protocol.js";import{create_connection_manager as F}from"./lib/connection_manager.js";import{shutdown_write_queue as J}from"./lib/write_queue.js";import{setup_authentication as K,verify_password as P,get_client_ip as j,is_rate_limited as G,initialize_auth_manager as M,reset_auth_state as W}from"./lib/auth_manager.js";import{initialize_api_key_manager as H}from"./lib/api_key_manager.js";import{is_development_mode as O,display_development_startup_message as U,warn_undefined_node_env as V}from"./lib/development_mode.js";import{restore_backup as Y,start_backup_schedule as L,stop_backup_schedule as Q}from"./lib/backup_manager.js";import{initialize_replication_manager as X,shutdown_replication_manager as Z}from"./lib/replication_manager.js";import{initialize_write_forwarder as ee,shutdown_write_forwarder as re}from"./lib/write_forwarder.js";import{handle_database_operation as te,handle_admin_operation as oe,handle_ping_operation as ae}from"./lib/operation_dispatcher.js";import{start_http_server as ne,stop_http_server as se}from"./lib/http_server.js";import{create_recovery_token as ie,initialize_recovery_manager as z,reset_recovery_state as ce}from"./lib/recovery_manager.js";const p=new Set;let i=null;const _e=async(t,r={})=>{if(!r?.password){const n=c({ok:0,error:"Authentication operation requires password to be set in data."});t.write(n),t.end();return}try{const o=j(t);if(G(o)){const a=c({ok:0,error:"Too many failed attempts. Please try again later."});t.write(a),t.end();return}if(!await P(r.password,o)){const a=c({ok:0,error:"Authentication failed"});t.write(a),t.end();return}p.add(t.id);const d=c({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(d)}catch(o){const n={ok:0,error:`Authentication error: ${o.message}`},s=c(n);t.write(s),t.end()}},pe=async(t,r={})=>{try{const n={ok:1,password:K(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},s=c(n);t.write(s)}catch(o){const n={ok:0,error:`Setup error: ${o.message}`},s=c(n);t.write(s)}},de=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return N.includes(t)},De=t=>{try{if(typeof t=="string")return x(t);if(Buffer.isBuffer(t)){const r=q(t);return typeof r=="string"?x(r):r}else return t}catch{return null}},S=t=>O()?!0:p.has(t.id),$e=async()=>{const{create_context_logger:t}=T("server"),r=t();let o=null;try{v(),o=h()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await Y(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const a={...o};delete a.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(a),v(),o=h(),r.info("Removed restore_from from settings after successful restore")}catch(e){r.error("Startup restore failed",{backup_filename:o.restore_from,error:e.message}),r.info("Continuing with fresh database after restore failure")}let n="./data";o?.data_path&&(n=o.data_path),B(n),M(),await H(),z();try{X(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{ee(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{L(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}i=F({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=b();s=await ne(e),s&&r.info("HTTP server started",{http_port:e})}catch(e){r.warn("Failed to start HTTP server",{error:e.message})}if(O()){const{tcp_port:e,http_port:a}=b();U(e,a)}else V();const d=E.createServer((e={})=>{if(!i.add_connection(e))return;const a=$();e.on("data",async g=>{i.update_activity(e.id);try{const w=a.parse_messages(g);for(const I of w){const u=I,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!de(l)){f(e,{message:"Invalid operation type"});continue}const R=i.create_request_timeout(e.id,l);try{switch(l){case"authentication":await _e(e,u?.data||{});break;case"setup":await pe(e,u?.data||{});break;case"insert_one":case"update_one":case"delete_one":case"bulk_write":case"find_one":case"find":case"create_index":case"drop_index":case"get_indexes":await te(e,l,u?.data||{},S,g.length,i,p);break;case"ping":ae(e);break;case"admin":await oe(e,u?.data||{},S,i,p);break;case"reload":if(!S(e)){f(e,{message:"Authentication required"});break}try{let _=null;try{_=h()}catch{}let m=null;try{await v(),m=h()}catch{m={port:1983,authentication:{}}}const y={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:_?_.port!==m.port:!1,authentication_changed:_?_.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},k=c(y);e.write(k)}catch(_){const m={ok:0,error:`Reload operation failed: ${_.message}`},y=c(m);e.write(y)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(R)}}}catch(w){r.error("Message parsing failed",{client_id:e.id,error:w.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),p.delete(e.id),i.remove_connection(e.id)}),e.on("error",g=>{r.error("Socket error",{socket_id:e.id,error:g.message}),p.delete(e.id),i.remove_connection(e.id)})});return d.cleanup=async()=>{try{await se(),Q(),await Z(),await re(),i&&i.shutdown(),p.clear(),await J(),await new Promise(e=>setTimeout(e,100)),await D(),W(),ce()}catch{}},d};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=T("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{z();const a=ie();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${a.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(a.expires_at).toISOString()}),process.exit(0)}catch(a){console.error("Failed to generate recovery token:",a.message),r.error("Recovery token generation failed",{error:a.message}),process.exit(1)}const{tcp_port:o,http_port:n}=b(),s={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:d}=await import("./lib/load_settings.js"),e=d();r.info("Starting JoystickDB server...",{workers:s.worker_count||"auto",tcp_port:o,http_port:n,environment:s.environment,has_settings:e,port_source:e?"JOYSTICK_DB_SETTINGS":"default"}),A(s)}export{_e as authentication,de as check_op_type,$e as create_server,De as parse_data,pe as setup};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joystick.js/db-canary",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.0-canary.
|
|
5
|
-
"canary_version": "0.0.0-canary.
|
|
4
|
+
"version": "0.0.0-canary.2223",
|
|
5
|
+
"canary_version": "0.0.0-canary.2222",
|
|
6
6
|
"description": "JoystickDB - A minimalist database server for the Joystick framework",
|
|
7
7
|
"main": "./dist/server/index.js",
|
|
8
8
|
"scripts": {
|
|
@@ -2,7 +2,7 @@ import cluster from 'cluster';
|
|
|
2
2
|
import os from 'os';
|
|
3
3
|
import { EventEmitter } from 'events';
|
|
4
4
|
import { writeFileSync } from 'fs';
|
|
5
|
-
import { load_settings, get_port_configuration } from '../lib/load_settings.js';
|
|
5
|
+
import { load_settings, get_settings, get_port_configuration } from '../lib/load_settings.js';
|
|
6
6
|
import {
|
|
7
7
|
restore_backup,
|
|
8
8
|
start_backup_schedule,
|
|
@@ -107,7 +107,18 @@ class ClusterMaster extends EventEmitter {
|
|
|
107
107
|
this.log.info('Settings loaded successfully', { settings_file: this.settings_file });
|
|
108
108
|
|
|
109
109
|
// NOTE: Initialize database and auth manager.
|
|
110
|
-
|
|
110
|
+
// NOTE: Initialize database with data_path from settings if available.
|
|
111
|
+
let database_path = './data'; // Default path
|
|
112
|
+
try {
|
|
113
|
+
const settings = get_settings();
|
|
114
|
+
if (settings?.data_path) {
|
|
115
|
+
database_path = settings.data_path;
|
|
116
|
+
}
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// NOTE: Settings not available, use default path.
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
initialize_database(database_path);
|
|
111
122
|
initialize_auth_manager();
|
|
112
123
|
await initialize_api_key_manager();
|
|
113
124
|
this.log.info('Database and auth manager initialized');
|
|
@@ -12,6 +12,7 @@ import { create_message_parser, encode_message } from '../lib/tcp_protocol.js';
|
|
|
12
12
|
import create_logger from '../lib/logger.js';
|
|
13
13
|
import { initialize_database, cleanup_database } from '../lib/query_engine.js';
|
|
14
14
|
import { handle_admin_operation, handle_ping_operation } from '../lib/operation_dispatcher.js';
|
|
15
|
+
import { get_settings } from '../lib/load_settings.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Cluster worker process that handles TCP connections and routes operations to master.
|
|
@@ -131,8 +132,19 @@ class ClusterWorker {
|
|
|
131
132
|
|
|
132
133
|
// NOTE: Initialize database for read operations in worker process.
|
|
133
134
|
try {
|
|
134
|
-
|
|
135
|
-
|
|
135
|
+
// NOTE: Initialize database with data_path from settings if available.
|
|
136
|
+
let database_path = './data'; // Default path
|
|
137
|
+
try {
|
|
138
|
+
const settings = get_settings();
|
|
139
|
+
if (settings?.data_path) {
|
|
140
|
+
database_path = settings.data_path;
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
// NOTE: Settings not available, use default path.
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
initialize_database(database_path);
|
|
147
|
+
this.log.info('Database initialized in worker process', { database_path });
|
|
136
148
|
} catch (error) {
|
|
137
149
|
this.log.error('Failed to initialize database in worker process', { error: error.message });
|
|
138
150
|
}
|
package/src/server/index.js
CHANGED
|
@@ -181,10 +181,16 @@ export const parse_data = (raw_data) => {
|
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
183
|
* Checks if a socket connection is authenticated.
|
|
184
|
+
* In development mode, authentication is bypassed for TCP connections.
|
|
184
185
|
* @param {net.Socket} socket - Socket connection to check
|
|
185
|
-
* @returns {boolean} True if socket is authenticated
|
|
186
|
+
* @returns {boolean} True if socket is authenticated or in development mode
|
|
186
187
|
*/
|
|
187
188
|
const check_authentication = (socket) => {
|
|
189
|
+
// NOTE: Bypass authentication in development mode for TCP connections.
|
|
190
|
+
if (is_development_mode()) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
188
194
|
return authenticated_clients.has(socket.id);
|
|
189
195
|
};
|
|
190
196
|
|
|
@@ -255,7 +261,13 @@ export const create_server = async () => {
|
|
|
255
261
|
}
|
|
256
262
|
}
|
|
257
263
|
|
|
258
|
-
|
|
264
|
+
// NOTE: Initialize database with data_path from settings if available.
|
|
265
|
+
let database_path = './data'; // Default path
|
|
266
|
+
if (settings?.data_path) {
|
|
267
|
+
database_path = settings.data_path;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
initialize_database(database_path);
|
|
259
271
|
initialize_auth_manager();
|
|
260
272
|
await initialize_api_key_manager();
|
|
261
273
|
initialize_recovery_manager();
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import test from 'ava';
|
|
2
|
+
import net from 'net';
|
|
3
|
+
import { create_server } from '../../../src/server/index.js';
|
|
4
|
+
import { encode_message, create_message_parser } from '../../../src/server/lib/tcp_protocol.js';
|
|
5
|
+
import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js';
|
|
6
|
+
import { reset_auth_state } from '../../../src/server/lib/auth_manager.js';
|
|
7
|
+
|
|
8
|
+
// Dynamic port allocation
|
|
9
|
+
let current_port = 4000;
|
|
10
|
+
const get_next_port = () => {
|
|
11
|
+
return ++current_port;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
let server;
|
|
15
|
+
let port;
|
|
16
|
+
let original_node_env;
|
|
17
|
+
|
|
18
|
+
test.beforeEach(async () => {
|
|
19
|
+
// Store original NODE_ENV
|
|
20
|
+
original_node_env = process.env.NODE_ENV;
|
|
21
|
+
|
|
22
|
+
// Set NODE_ENV to development
|
|
23
|
+
process.env.NODE_ENV = 'development';
|
|
24
|
+
|
|
25
|
+
// Reset auth state and clean up environment variables
|
|
26
|
+
reset_auth_state();
|
|
27
|
+
delete process.env.JOYSTICK_DB_SETTINGS;
|
|
28
|
+
|
|
29
|
+
// Initialize database for testing
|
|
30
|
+
initialize_database();
|
|
31
|
+
|
|
32
|
+
// Create server with dynamic port
|
|
33
|
+
const test_port = get_next_port();
|
|
34
|
+
server = await create_server();
|
|
35
|
+
|
|
36
|
+
// Start server on specific port
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
server.listen(test_port, () => {
|
|
39
|
+
port = test_port;
|
|
40
|
+
setTimeout(resolve, 100);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test.afterEach(async () => {
|
|
46
|
+
// Restore original NODE_ENV
|
|
47
|
+
process.env.NODE_ENV = original_node_env;
|
|
48
|
+
|
|
49
|
+
if (server) {
|
|
50
|
+
await server.cleanup();
|
|
51
|
+
server.close();
|
|
52
|
+
server = null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Clean up database
|
|
56
|
+
try {
|
|
57
|
+
await cleanup_database(true); // Remove test database directory
|
|
58
|
+
} catch (error) {
|
|
59
|
+
// Ignore cleanup errors
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Clean up environment variables
|
|
63
|
+
reset_auth_state();
|
|
64
|
+
delete process.env.JOYSTICK_DB_SETTINGS;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const create_client = () => {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
const client = net.createConnection(port, 'localhost');
|
|
70
|
+
const parser = create_message_parser();
|
|
71
|
+
|
|
72
|
+
client.on('connect', () => {
|
|
73
|
+
resolve({
|
|
74
|
+
client,
|
|
75
|
+
send: (data) => {
|
|
76
|
+
const encoded = encode_message(data);
|
|
77
|
+
client.write(encoded);
|
|
78
|
+
},
|
|
79
|
+
receive: () => {
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
const handler = (data) => {
|
|
82
|
+
try {
|
|
83
|
+
const messages = parser.parse_messages(data);
|
|
84
|
+
for (const message of messages) {
|
|
85
|
+
client.off('data', handler);
|
|
86
|
+
resolve(message);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
// Continue listening
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
client.on('data', handler);
|
|
94
|
+
});
|
|
95
|
+
},
|
|
96
|
+
close: () => {
|
|
97
|
+
client.end();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
client.on('error', reject);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
test('development mode - database operations work without authentication', async (t) => {
|
|
107
|
+
const { client, send, receive, close } = await create_client();
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// Try ping operation without authentication - should work in development mode
|
|
111
|
+
send({ op: 'ping' });
|
|
112
|
+
const response = await receive();
|
|
113
|
+
|
|
114
|
+
t.is(response.ok, 1);
|
|
115
|
+
// Ping operation just returns { ok: 1 } without a message
|
|
116
|
+
} finally {
|
|
117
|
+
close();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('development mode - find operation works without authentication', async (t) => {
|
|
122
|
+
const { client, send, receive, close } = await create_client();
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
// Try find operation without authentication - should work in development mode
|
|
126
|
+
send({ op: 'find', data: { collection: 'users', filter: {} } });
|
|
127
|
+
const response = await receive();
|
|
128
|
+
|
|
129
|
+
t.is(response.ok, 1);
|
|
130
|
+
t.true(Array.isArray(response.documents));
|
|
131
|
+
} finally {
|
|
132
|
+
close();
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('development mode - insert operation works without authentication', async (t) => {
|
|
137
|
+
const { client, send, receive, close } = await create_client();
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Try insert operation without authentication - should work in development mode
|
|
141
|
+
send({
|
|
142
|
+
op: 'insert_one',
|
|
143
|
+
data: {
|
|
144
|
+
collection: 'users',
|
|
145
|
+
document: { name: 'Test User', email: 'test@example.com' }
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
const response = await receive();
|
|
149
|
+
|
|
150
|
+
t.is(response.ok, 1);
|
|
151
|
+
t.true(typeof response.inserted_id === 'string');
|
|
152
|
+
} finally {
|
|
153
|
+
close();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('development mode - admin operation works without authentication', async (t) => {
|
|
158
|
+
const { client, send, receive, close } = await create_client();
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Try admin operation without authentication - should work in development mode
|
|
162
|
+
send({ op: 'admin' });
|
|
163
|
+
const response = await receive();
|
|
164
|
+
|
|
165
|
+
t.true(typeof response.authentication === 'object');
|
|
166
|
+
t.is(response.authentication.authenticated_clients, 0); // No clients authenticated since we bypassed auth
|
|
167
|
+
t.true(typeof response.server === 'object');
|
|
168
|
+
t.true(typeof response.database === 'object');
|
|
169
|
+
} finally {
|
|
170
|
+
close();
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('development mode - authentication still works if explicitly used', async (t) => {
|
|
175
|
+
const { client, send, receive, close } = await create_client();
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Setup authentication first
|
|
179
|
+
send({ op: 'setup' });
|
|
180
|
+
const setup_response = await receive();
|
|
181
|
+
|
|
182
|
+
t.true(setup_response.ok === 1 || setup_response.ok === true);
|
|
183
|
+
t.is(typeof setup_response.password, 'string');
|
|
184
|
+
|
|
185
|
+
// Now authenticate with the password
|
|
186
|
+
send({ op: 'authentication', data: { password: setup_response.password } });
|
|
187
|
+
const auth_response = await receive();
|
|
188
|
+
|
|
189
|
+
t.is(auth_response.ok, 1);
|
|
190
|
+
t.is(auth_response.version, '1.0.0');
|
|
191
|
+
t.is(auth_response.message, 'Authentication successful');
|
|
192
|
+
} finally {
|
|
193
|
+
close();
|
|
194
|
+
}
|
|
195
|
+
});
|