@joystick.js/db-canary 0.0.0-canary.2216 → 0.0.0-canary.2218

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.
@@ -1 +1 @@
1
- import k from"net";import{decode as C}from"msgpackr";import E from"./lib/op_types.js";import b from"./lib/safe_json_parse.js";import{load_settings as y,get_settings as g,get_port_configuration as S}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as q}from"./cluster/index.js";import x from"./lib/logger.js";import{initialize_database as N,cleanup_database as A}from"./lib/query_engine.js";import{create_message_parser as B,encode_message as _}from"./lib/tcp_protocol.js";import{create_connection_manager as D}from"./lib/connection_manager.js";import{shutdown_write_queue as $}from"./lib/write_queue.js";import{setup_authentication as F,verify_password as J,get_client_ip as K,is_rate_limited as P,initialize_auth_manager as j,reset_auth_state as G}from"./lib/auth_manager.js";import{initialize_api_key_manager as M}from"./lib/api_key_manager.js";import{restore_backup as W,start_backup_schedule as H,stop_backup_schedule as U}from"./lib/backup_manager.js";import{initialize_replication_manager as V,shutdown_replication_manager as Y}from"./lib/replication_manager.js";import{initialize_write_forwarder as L,shutdown_write_forwarder as Q}from"./lib/write_forwarder.js";import{handle_database_operation as X,handle_admin_operation as Z,handle_ping_operation as ee}from"./lib/operation_dispatcher.js";import{start_http_server as re,stop_http_server as te}from"./lib/http_server.js";import{create_recovery_token as oe,initialize_recovery_manager as T,reset_recovery_state as ne}from"./lib/recovery_manager.js";const d=new Set;let i=null;const se=async(t,r={})=>{if(!r?.password){const s=_({ok:0,error:"Authentication operation requires password to be set in data."});t.write(s),t.end();return}try{const o=K(t);if(P(o)){const n=_({ok:0,error:"Too many failed attempts. Please try again later."});t.write(n),t.end();return}if(!await J(r.password,o)){const n=_({ok:0,error:"Authentication failed"});t.write(n),t.end();return}d.add(t.id);const e=_({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(e)}catch(o){const s={ok:0,error:`Authentication error: ${o.message}`},a=_(s);t.write(a),t.end()}},ae=async(t,r={})=>{try{const s={ok:1,password:F(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},a=_(s);t.write(a)}catch(o){const s={ok:0,error:`Setup error: ${o.message}`},a=_(s);t.write(a)}},ie=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return E.includes(t)},Ee=t=>{try{if(typeof t=="string")return b(t);if(Buffer.isBuffer(t)){const r=C(t);return typeof r=="string"?b(r):r}else return t}catch{return null}},v=t=>d.has(t.id),qe=async()=>{const{create_context_logger:t}=x("server"),r=t();let o=null;try{y(),o=g()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await W(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const c={...o};delete c.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(c),y(),o=g(),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")}N(),j(),M(),T();try{V(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{L(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{H(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}i=D({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=S();s=await re(e),s&&r.info("HTTP server started",{http_port:e})}catch(e){r.warn("Failed to start HTTP server",{error:e.message})}const a=k.createServer((e={})=>{if(!i.add_connection(e))return;const c=B();e.on("data",async n=>{i.update_activity(e.id);try{const h=c.parse_messages(n);for(const O of h){const u=O,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!ie(l)){f(e,{message:"Invalid operation type"});continue}const z=i.create_request_timeout(e.id,l);try{switch(l){case"authentication":await se(e,u?.data||{});break;case"setup":await ae(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 X(e,l,u?.data||{},v,n.length,i,d);break;case"ping":ee(e);break;case"admin":await Z(e,u?.data||{},v,i,d);break;case"reload":if(!v(e)){f(e,{message:"Authentication required"});break}try{let p=null;try{p=g()}catch{}let m=null;try{await y(),m=g()}catch{m={port:1983,authentication:{}}}const w={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:p?p.port!==m.port:!1,authentication_changed:p?p.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},I=_(w);e.write(I)}catch(p){const m={ok:0,error:`Reload operation failed: ${p.message}`},w=_(m);e.write(w)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(z)}}}catch(h){r.error("Message parsing failed",{client_id:e.id,error:h.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),d.delete(e.id),i.remove_connection(e.id)}),e.on("error",n=>{r.error("Socket error",{socket_id:e.id,error:n.message}),d.delete(e.id),i.remove_connection(e.id)})});return a.cleanup=async()=>{try{await te(),U(),await Y(),await Q(),i&&i.shutdown(),d.clear(),await $(),await new Promise(e=>setTimeout(e,100)),await A(),G(),ne()}catch{}},a};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=x("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{T();const n=oe();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${n.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(n.expires_at).toISOString()}),process.exit(0)}catch(n){console.error("Failed to generate recovery token:",n.message),r.error("Recovery token generation failed",{error:n.message}),process.exit(1)}const{tcp_port:o,http_port:s}=S(),a={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:e}=await import("./lib/load_settings.js"),c=e();r.info("Starting JoystickDB server...",{workers:a.worker_count||"auto",tcp_port:o,http_port:s,environment:a.environment,has_settings:c,port_source:c?"JOYSTICK_DB_SETTINGS":"default"}),q(a)}export{se as authentication,ie as check_op_type,qe as create_server,Ee as parse_data,ae as setup};
1
+ import k from"net";import{decode as C}from"msgpackr";import E from"./lib/op_types.js";import S from"./lib/safe_json_parse.js";import{load_settings as y,get_settings as g,get_port_configuration as v}from"./lib/load_settings.js";import{send_error as f}from"./lib/send_response.js";import{start_cluster as q}from"./cluster/index.js";import x from"./lib/logger.js";import{initialize_database as N,cleanup_database as A}from"./lib/query_engine.js";import{create_message_parser as B,encode_message as _}from"./lib/tcp_protocol.js";import{create_connection_manager as D}from"./lib/connection_manager.js";import{shutdown_write_queue as $}from"./lib/write_queue.js";import{setup_authentication as F,verify_password as J,get_client_ip as K,is_rate_limited as P,initialize_auth_manager as j,reset_auth_state as G}from"./lib/auth_manager.js";import{initialize_api_key_manager as M}from"./lib/api_key_manager.js";import{is_development_mode as W,display_development_startup_message as H,warn_undefined_node_env as U}from"./lib/development_mode.js";import{restore_backup as V,start_backup_schedule as Y,stop_backup_schedule as L}from"./lib/backup_manager.js";import{initialize_replication_manager as Q,shutdown_replication_manager as X}from"./lib/replication_manager.js";import{initialize_write_forwarder as Z,shutdown_write_forwarder as ee}from"./lib/write_forwarder.js";import{handle_database_operation as re,handle_admin_operation as te,handle_ping_operation as oe}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 ae,initialize_recovery_manager as T,reset_recovery_state as ie}from"./lib/recovery_manager.js";const d=new Set;let c=null;const ce=async(t,r={})=>{if(!r?.password){const s=_({ok:0,error:"Authentication operation requires password to be set in data."});t.write(s),t.end();return}try{const o=K(t);if(P(o)){const n=_({ok:0,error:"Too many failed attempts. Please try again later."});t.write(n),t.end();return}if(!await J(r.password,o)){const n=_({ok:0,error:"Authentication failed"});t.write(n),t.end();return}d.add(t.id);const e=_({ok:1,version:"1.0.0",message:"Authentication successful"});t.write(e)}catch(o){const s={ok:0,error:`Authentication error: ${o.message}`},a=_(s);t.write(a),t.end()}},_e=async(t,r={})=>{try{const s={ok:1,password:F(),message:"Authentication setup completed successfully. Save this password - it will not be shown again."},a=_(s);t.write(a)}catch(o){const s={ok:0,error:`Setup error: ${o.message}`},a=_(s);t.write(a)}},pe=(t="")=>{if(!t)throw new Error("Must pass an op type for operation.");return E.includes(t)},Be=t=>{try{if(typeof t=="string")return S(t);if(Buffer.isBuffer(t)){const r=C(t);return typeof r=="string"?S(r):r}else return t}catch{return null}},b=t=>d.has(t.id),De=async()=>{const{create_context_logger:t}=x("server"),r=t();let o=null;try{y(),o=g()}catch{}if(o?.restore_from)try{r.info("Startup restore requested",{backup_filename:o.restore_from});const e=await V(o.restore_from);r.info("Startup restore completed",{backup_filename:o.restore_from,duration_ms:e.duration_ms});const i={...o};delete i.restore_from,process.env.JOYSTICK_DB_SETTINGS=JSON.stringify(i),y(),o=g(),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")}N(),j(),await M(),T();try{Q(),r.info("Replication manager initialized")}catch(e){r.warn("Failed to initialize replication manager",{error:e.message})}try{Z(),r.info("Write forwarder initialized")}catch(e){r.warn("Failed to initialize write forwarder",{error:e.message})}if(o?.s3)try{Y(),r.info("Backup scheduling started")}catch(e){r.warn("Failed to start backup scheduling",{error:e.message})}c=D({max_connections:1e3,idle_timeout:600*1e3,request_timeout:5*1e3});let s=null;try{const{http_port:e}=v();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(W()){const{tcp_port:e,http_port:i}=v();H(e,i)}else U();const a=k.createServer((e={})=>{if(!c.add_connection(e))return;const i=B();e.on("data",async n=>{c.update_activity(e.id);try{const h=i.parse_messages(n);for(const O of h){const u=O,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!pe(l)){f(e,{message:"Invalid operation type"});continue}const z=c.create_request_timeout(e.id,l);try{switch(l){case"authentication":await ce(e,u?.data||{});break;case"setup":await _e(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 re(e,l,u?.data||{},b,n.length,c,d);break;case"ping":oe(e);break;case"admin":await te(e,u?.data||{},b,c,d);break;case"reload":if(!b(e)){f(e,{message:"Authentication required"});break}try{let p=null;try{p=g()}catch{}let m=null;try{await y(),m=g()}catch{m={port:1983,authentication:{}}}const w={ok:1,status:"success",message:"Configuration reloaded successfully",changes:{port_changed:p?p.port!==m.port:!1,authentication_changed:p?p.authentication?.password_hash!==m.authentication?.password_hash:!1},timestamp:new Date().toISOString()},I=_(w);e.write(I)}catch(p){const m={ok:0,error:`Reload operation failed: ${p.message}`},w=_(m);e.write(w)}break;default:f(e,{message:`Operation ${l} not implemented`})}}finally{clearTimeout(z)}}}catch(h){r.error("Message parsing failed",{client_id:e.id,error:h.message}),f(e,{message:"Invalid message format"}),e.end()}}),e.on("end",()=>{r.info("Client disconnected",{socket_id:e.id}),d.delete(e.id),c.remove_connection(e.id)}),e.on("error",n=>{r.error("Socket error",{socket_id:e.id,error:n.message}),d.delete(e.id),c.remove_connection(e.id)})});return a.cleanup=async()=>{try{await se(),L(),await X(),await ee(),c&&c.shutdown(),d.clear(),await $(),await new Promise(e=>setTimeout(e,100)),await A(),G(),ie()}catch{}},a};if(import.meta.url===`file://${process.argv[1]}`){const{create_context_logger:t}=x("main"),r=t();if(process.argv.includes("--generate-recovery-token"))try{T();const n=ae();console.log("Emergency Recovery Token Generated"),console.log(`Visit: ${n.url}`),console.log("Token expires in 10 minutes"),r.info("Recovery token generated via CLI",{expires_at:new Date(n.expires_at).toISOString()}),process.exit(0)}catch(n){console.error("Failed to generate recovery token:",n.message),r.error("Recovery token generation failed",{error:n.message}),process.exit(1)}const{tcp_port:o,http_port:s}=v(),a={worker_count:process.env.WORKER_COUNT?parseInt(process.env.WORKER_COUNT):void 0,port:o,environment:process.env.NODE_ENV||"development"},{has_settings:e}=await import("./lib/load_settings.js"),i=e();r.info("Starting JoystickDB server...",{workers:a.worker_count||"auto",tcp_port:o,http_port:s,environment:a.environment,has_settings:i,port_source:i?"JOYSTICK_DB_SETTINGS":"default"}),q(a)}export{ce as authentication,pe as check_op_type,De as create_server,Be as parse_data,_e as setup};
@@ -1,9 +1,9 @@
1
- import{readFileSync as b,writeFileSync as O,existsSync as m,unlinkSync as P}from"fs";import A from"crypto";import y from"bcrypt";import E from"./logger.js";import{get_database as c,build_collection_key as g}from"./query_engine.js";const{create_context_logger:x}=E("api_key_manager"),l=x(),d="./API_KEY",i="_users",h=12;let u=null,_=!1;const T=()=>{const e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";let r="";const s=A.randomBytes(32);for(let o=0;o<32;o++)r+=e[s[o]%e.length];return r},w=()=>{if(u)return u;if(m(d))try{return u=b(d,"utf8").trim(),l.info("API key loaded from file"),u}catch(r){throw l.error("Failed to read API key file",{error:r.message}),new Error(`Failed to read API key file: ${r.message}`)}const e=T();try{return O(d,e,{mode:384}),u=e,l.info("New API key generated and saved",{file_path:d}),N(e),e}catch(r){throw l.error("Failed to save API key file",{error:r.message}),new Error(`Failed to save API key file: ${r.message}`)}},N=e=>{const r=process.env.JOYSTICK_DB_PORT||"1983",s=process.env.JOYSTICK_DB_HTTP_PORT||"1984";console.log(`
1
+ import{readFileSync as x,writeFileSync as T,existsSync as v,unlinkSync as F}from"fs";import N from"crypto";import y from"bcrypt";import D from"./logger.js";import{get_database as _,build_collection_key as m}from"./query_engine.js";import{is_development_mode as g}from"./development_mode.js";const{create_context_logger:J}=D("api_key_manager"),a=J(),f="./API_KEY",l="_users",S=12;let d=null,u=!1;const R=()=>{const e="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";let r="";const o=N.randomBytes(32);for(let s=0;s<32;s++)r+=e[o[s]%e.length];return r},w=()=>{if(g())return d="development-mode-bypass",a.info("Development mode: API key generation bypassed"),d;if(d)return d;if(v(f))try{return d=x(f,"utf8").trim(),a.info("API key loaded from file"),d}catch(r){throw a.error("Failed to read API key file",{error:r.message}),new Error(`Failed to read API key file: ${r.message}`)}const e=R();try{return T(f,e,{mode:384}),d=e,a.info("New API key generated and saved",{file_path:f}),U(e),e}catch(r){throw a.error("Failed to save API key file",{error:r.message}),new Error(`Failed to save API key file: ${r.message}`)}},U=e=>{const r=process.env.JOYSTICK_DB_PORT||"1983",o=process.env.JOYSTICK_DB_HTTP_PORT||"1984";console.log(`
2
2
  JoystickDB Setup
3
3
  `),console.log(`To finish setting up your database and enable operations for authenticated users, you will need to create an admin user via the database's admin API using the API key displayed below.
4
4
  `),console.log("=== STORE THIS KEY SECURELY -- IT WILL NOT BE DISPLAYED AGAIN ==="),console.log(e),console.log(`===
5
5
  `),console.log(`To create a user, send a POST request to the server:
6
- `),console.log(`fetch('http://localhost:${s}/api/users', {`),console.log(" method: 'POST',"),console.log(" headers: {"),console.log(" 'Content-Type': 'application/json',"),console.log(` 'x-joystick-db-api-key': '${e}'`),console.log(" },"),console.log(" body: JSON.stringify({"),console.log(" username: 'admin',"),console.log(" password: 'your_secure_password',"),console.log(" role: 'read_write'"),console.log(" })"),console.log(`});
6
+ `),console.log(`fetch('http://localhost:${o}/api/users', {`),console.log(" method: 'POST',"),console.log(" headers: {"),console.log(" 'Content-Type': 'application/json',"),console.log(` 'x-joystick-db-api-key': '${e}'`),console.log(" },"),console.log(" body: JSON.stringify({"),console.log(" username: 'admin',"),console.log(" password: 'your_secure_password',"),console.log(" role: 'read_write'"),console.log(" })"),console.log(`});
7
7
  `),console.log("==!=="),console.log("Store this key securely. It will not be displayed again."),console.log(`==!==
8
- `),console.log(`API key saved to: ${d}
9
- `)},F=e=>{if(!e)return!1;const r=w();return e===r},J=e=>!e||typeof e!="string"?!1:/^[a-zA-Z0-9]{3,50}$/.test(e),S=e=>!e||typeof e!="string"?{valid:!1,error:"Password is required"}:e.length<8?{valid:!1,error:"Password must be at least 8 characters long"}:e.length>128?{valid:!1,error:"Password must be no more than 128 characters long"}:{valid:!0},k=e=>["read","write","read_write"].includes(e),R=async e=>{const{username:r,password:s,role:o}=e;if(!J(r))throw new Error("Username must be alphanumeric and 3-50 characters long");const t=S(s);if(!t.valid)throw new Error(t.error);if(!k(o))throw new Error("Role must be one of: read, write, read_write");const a=c(),p=await y.hash(s,h),n={username:r,password_hash:p,role:o,created_at:new Date().toISOString(),updated_at:new Date().toISOString()},f=g(i,r);let v=null;return await a.transaction(()=>{if(a.get(f))throw new Error("Username already exists");a.put(f,JSON.stringify(n)),v=n}),o==="read_write"&&(_=!0,l.info("Admin user created, API key requirement relaxed for authenticated operations")),l.info("User created successfully",{username:r,role:o}),{username:n.username,role:n.role,created_at:n.created_at}},U=()=>{const e=c(),r=[];for(const{key:s,value:o}of e.getRange({start:`${i}:`,end:`${i}:\xFF`})){const t=JSON.parse(o),a={username:t.username,role:t.role,created_at:t.created_at,updated_at:t.updated_at};r.push(a)}return r},D=e=>{const r=c(),s=g(i,e),o=r.get(s);if(!o)return null;const t=JSON.parse(o);return{username:t.username,role:t.role,created_at:t.created_at,updated_at:t.updated_at}},$=async(e,r)=>{const s=c(),o=g(i,e);let t=null;if(r.password){const f=S(r.password);if(!f.valid)throw new Error(f.error);t=await y.hash(r.password,h)}if(r.role&&!k(r.role))throw new Error("Role must be one of: read, write, read_write");const a=s.get(o);if(!a)throw new Error("User not found");const n={...JSON.parse(a)};return r.role&&(n.role=r.role),t&&(n.password_hash=t),n.updated_at=new Date().toISOString(),s.putSync(o,JSON.stringify(n)),l.info("User updated successfully",{username:e,updates:Object.keys(r)}),{username:n.username,role:n.role,created_at:n.created_at,updated_at:n.updated_at}},L=e=>{const r=c(),s=g(i,e);if(!r.get(s))throw new Error("User not found");const t=r.removeSync(s);return l.info("User deleted successfully",{username:e}),!0},Y=async(e,r)=>{const s=c(),o=g(i,e),t=s.get(o);if(!t)return null;const a=JSON.parse(t);return await y.compare(r,a.password_hash)?{username:a.username,role:a.role,created_at:a.created_at,updated_at:a.updated_at}:null},I=()=>{if(_)return!0;const e=c();for(const{value:r}of e.getRange({start:`${i}:`,end:`${i}:\xFF`}))if(JSON.parse(r).role==="read_write")return _=!0,!0;return!1},C=()=>{w(),_=I(),_&&l.info("Admin user detected, API key requirement relaxed for authenticated operations")},B=()=>{if(u=null,_=!1,m(d))try{P(d)}catch{}};export{I as check_admin_user_exists,R as create_user,L as delete_user,U as get_all_users,D as get_user,C as initialize_api_key_manager,w as load_or_generate_api_key,B as reset_api_key_state,$ as update_user,F as validate_api_key,Y as verify_user_password};
8
+ `),console.log(`API key saved to: ${f}
9
+ `)},$=e=>{if(g())return!0;if(!e)return!1;const r=w();return e===r},L=e=>!e||typeof e!="string"?!1:/^[a-zA-Z0-9]{3,50}$/.test(e),k=(e,r=!1)=>!e||typeof e!="string"?{valid:!1,error:"Password is required"}:g()&&r?{valid:!0}:e.length<8?{valid:!1,error:"Password must be at least 8 characters long"}:e.length>128?{valid:!1,error:"Password must be no more than 128 characters long"}:{valid:!0},I=e=>["read","write","read_write"].includes(e),b=async(e,r=!1)=>{const{username:o,password:s,role:t}=e;if(!L(o))throw new Error("Username must be alphanumeric and 3-50 characters long");const n=k(s,r);if(!n.valid)throw new Error(n.error);if(!I(t))throw new Error("Role must be one of: read, write, read_write");const p=_(),i=await y.hash(s,S),c={username:o,password_hash:i,role:t,created_at:new Date().toISOString(),updated_at:new Date().toISOString()},h=m(l,o);let E=null;return await p.transaction(()=>{if(p.get(h))throw new Error("Username already exists");p.put(h,JSON.stringify(c)),E=c}),t==="read_write"&&(u=!0,a.info("Admin user created, API key requirement relaxed for authenticated operations")),a.info("User created successfully",{username:o,role:t}),{username:c.username,role:c.role,created_at:c.created_at}},Y=()=>{const e=_(),r=[];for(const{key:o,value:s}of e.getRange({start:`${l}:`,end:`${l}:\xFF`})){const t=JSON.parse(s),n={username:t.username,role:t.role,created_at:t.created_at,updated_at:t.updated_at};r.push(n)}return r},P=e=>{const r=_(),o=m(l,e),s=r.get(o);if(!s)return null;const t=JSON.parse(s);return{username:t.username,role:t.role,created_at:t.created_at,updated_at:t.updated_at}},C=async(e,r)=>{const o=_(),s=m(l,e);let t=null;if(r.password){const c=k(r.password);if(!c.valid)throw new Error(c.error);t=await y.hash(r.password,S)}if(r.role&&!I(r.role))throw new Error("Role must be one of: read, write, read_write");const n=o.get(s);if(!n)throw new Error("User not found");const i={...JSON.parse(n)};return r.role&&(i.role=r.role),t&&(i.password_hash=t),i.updated_at=new Date().toISOString(),o.putSync(s,JSON.stringify(i)),a.info("User updated successfully",{username:e,updates:Object.keys(r)}),{username:i.username,role:i.role,created_at:i.created_at,updated_at:i.updated_at}},B=e=>{const r=_(),o=m(l,e);if(!r.get(o))throw new Error("User not found");const t=r.removeSync(o);return a.info("User deleted successfully",{username:e}),!0},K=async(e,r)=>{const o=_(),s=m(l,e),t=o.get(s);if(!t)return null;const n=JSON.parse(t);return await y.compare(r,n.password_hash)?{username:n.username,role:n.role,created_at:n.created_at,updated_at:n.updated_at}:null},O=()=>{if(u)return!0;const e=_();for(const{value:r}of e.getRange({start:`${l}:`,end:`${l}:\xFF`}))if(JSON.parse(r).role==="read_write")return u=!0,!0;return!1},A=async()=>{if(!g())return null;const e=P("admin");if(e)return a.info("Development admin user already exists, skipping creation"),e;try{const r=await b({username:"admin",password:"password",role:"read_write"},!0);return a.info("Development admin user created automatically",{username:"admin"}),r}catch(r){throw a.error("Failed to create development admin user",{error:r.message}),r}},q=async()=>{if(w(),u=O(),u&&a.info("Admin user detected, API key requirement relaxed for authenticated operations"),g()&&!u)try{await A(),u=!0}catch(e){a.error("Failed to initialize development admin user",{error:e.message})}},j=()=>{if(d=null,u=!1,v(f))try{F(f)}catch{}};export{O as check_admin_user_exists,A as create_development_admin_user,b as create_user,B as delete_user,Y as get_all_users,P as get_user,q as initialize_api_key_manager,w as load_or_generate_api_key,j as reset_api_key_state,C as update_user,$ as validate_api_key,K as verify_user_password};
@@ -0,0 +1,7 @@
1
+ import s from"./logger.js";const{create_context_logger:t}=s("development_mode"),n=t(),l=()=>process.env.NODE_ENV==="development",r=(e,o)=>{console.log(`
2
+ JoystickDB Development Mode
3
+ `),console.log("Development environment detected (NODE_ENV=development)."),console.log(`Security features have been bypassed for local development.
4
+ `),console.log("Default admin user created:"),console.log(" Username: admin"),console.log(` Password: password
5
+ `),console.log("WARNING: This configuration is NOT suitable for production use."),console.log(`Set NODE_ENV to 'production' or another value for secure operation.
6
+ `),console.log(`TCP Server: localhost:${e}`),console.log(`HTTP Server: localhost:${o}
7
+ `),n.info("Development mode activated",{tcp_port:e,http_port:o,default_admin_created:!0,security_bypassed:!0})},c=()=>{process.env.NODE_ENV||(n.warn("NODE_ENV is not set, defaulting to secure mode"),console.log("WARNING: NODE_ENV is not set. Defaulting to secure mode."))};export{r as display_development_startup_message,l as is_development_mode,c as warn_undefined_node_env};
@@ -1,4 +1,4 @@
1
- import v from"http";import{URL as x}from"url";import _ from"crypto";import k from"./logger.js";import{setup_authentication as T,get_auth_stats as P}from"./auth_manager.js";import{is_token_valid as C,record_failed_recovery_attempt as A,change_password as S}from"./recovery_manager.js";import{validate_api_key as I,create_user as D,get_all_users as E,update_user as H,delete_user as O}from"./api_key_manager.js";const{create_context_logger:R}=k("http_server"),a=R();let p=null,l=null,u=!1,m=new Map;const B=60*1e3,U=10,J=()=>_.randomUUID(),f=()=>!P().configured,Y=t=>{const e=Date.now(),r=(m.get(t)||[]).filter(n=>e-n<B);return m.set(t,r),r.length>=U},$=t=>{const e=Date.now(),o=m.get(t)||[];o.push(e),m.set(t,o)},j=(t,e=null)=>`<!DOCTYPE html>
1
+ import _ from"http";import{URL as x}from"url";import k from"crypto";import T from"./logger.js";import{setup_authentication as P,get_auth_stats as A}from"./auth_manager.js";import{is_token_valid as C,record_failed_recovery_attempt as S,change_password as I}from"./recovery_manager.js";import{validate_api_key as D,create_user as E,get_all_users as H,update_user as O,delete_user as R}from"./api_key_manager.js";import{is_development_mode as l}from"./development_mode.js";const{create_context_logger:B}=T("http_server"),a=B();let u=null,c=null,m=!1,h=new Map;const U=60*1e3,J=10,Y=()=>k.randomUUID(),g=()=>!A().configured,$=t=>{const e=Date.now(),r=(h.get(t)||[]).filter(n=>e-n<U);return h.set(t,r),r.length>=J},j=t=>{const e=Date.now(),o=h.get(t)||[];o.push(e),h.set(t,o)},N=(t,e=null)=>`<!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
4
  <title>JoystickDB Setup</title>
@@ -65,7 +65,7 @@ import v from"http";import{URL as x}from"url";import _ from"crypto";import k fro
65
65
  </ul>
66
66
  </div>
67
67
  </body>
68
- </html>`,N=t=>`<!DOCTYPE html>
68
+ </html>`,W=t=>`<!DOCTYPE html>
69
69
  <html>
70
70
  <head>
71
71
  <title>JoystickDB Setup Complete</title>
@@ -159,7 +159,7 @@ await client.ping();
159
159
  <p><strong>Your JoystickDB server is ready to use!</strong></p>
160
160
  </div>
161
161
  </body>
162
- </html>`,c=t=>`<!DOCTYPE html>
162
+ </html>`,p=t=>`<!DOCTYPE html>
163
163
  <html>
164
164
  <head>
165
165
  <title>JoystickDB Setup Error</title>
@@ -202,7 +202,7 @@ await client.ping();
202
202
  <p><a href="javascript:history.back()">\u2190 Go Back</a></p>
203
203
  </div>
204
204
  </body>
205
- </html>`,g=(t,e=null)=>`<!DOCTYPE html>
205
+ </html>`,y=(t,e=null)=>`<!DOCTYPE html>
206
206
  <html>
207
207
  <head>
208
208
  <title>JoystickDB Emergency Password Recovery</title>
@@ -325,7 +325,7 @@ await client.ping();
325
325
  </ul>
326
326
  </div>
327
327
  </body>
328
- </html>`,W=t=>`<!DOCTYPE html>
328
+ </html>`,M=t=>`<!DOCTYPE html>
329
329
  <html>
330
330
  <head>
331
331
  <title>JoystickDB Password Changed</title>
@@ -395,11 +395,11 @@ await client.ping();
395
395
  <p><strong>Your JoystickDB server is ready with the new password!</strong></p>
396
396
  </div>
397
397
  </body>
398
- </html>`,M=t=>new Promise((e,o)=>{let r="";t.on("data",n=>{r+=n.toString()}),t.on("end",()=>{try{const n=new URLSearchParams(r),s={};for(const[d,h]of n)s[d]=h;e(s)}catch(n){o(n)}}),t.on("error",n=>{o(n)})}),b=t=>new Promise((e,o)=>{let r="";t.on("data",n=>{r+=n.toString()}),t.on("end",()=>{try{const n=JSON.parse(r);e(n)}catch(n){o(n)}}),t.on("error",n=>{o(n)})}),y=t=>{const e=t.headers["x-joystick-db-api-key"];return I(e)},i=(t,e,o)=>{t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(o))},G=async(t,e)=>{const o=t.socket.remoteAddress||"127.0.0.1";if(!y(t)){a.warn("Invalid API key for user creation",{client_ip:o}),i(e,403,{error:"Database setup incomplete. A valid API key must be passed until an admin user has been created."});return}try{const r=await b(t),n=await D(r);a.info("User created via API",{username:n.username,client_ip:o}),i(e,201,{ok:1,user:n})}catch(r){a.error("User creation failed via API",{client_ip:o,error:r.message}),i(e,400,{error:r.message})}},z=async(t,e)=>{const o=t.socket.remoteAddress||"127.0.0.1";if(!y(t)){a.warn("Invalid API key for get users",{client_ip:o}),i(e,403,{error:"Database setup incomplete. A valid API key must be passed until an admin user has been created."});return}try{const r=E();a.info("Users retrieved via API",{count:r.length,client_ip:o}),i(e,200,{ok:1,users:r})}catch(r){a.error("Get users failed via API",{client_ip:o,error:r.message}),i(e,500,{error:r.message})}},F=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(!y(t)){a.warn("Invalid API key for user update",{client_ip:r,username:o}),i(e,403,{error:"Database setup incomplete. A valid API key must be passed until an admin user has been created."});return}try{const n=await b(t),s=await H(o,n);a.info("User updated via API",{username:o,client_ip:r}),i(e,200,{ok:1,user:s})}catch(n){a.error("User update failed via API",{client_ip:r,username:o,error:n.message});const s=n.message==="User not found"?404:400;i(e,s,{error:n.message})}},K=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(!y(t)){a.warn("Invalid API key for user deletion",{client_ip:r,username:o}),i(e,403,{error:"Database setup incomplete. A valid API key must be passed until an admin user has been created."});return}try{O(o),a.info("User deleted via API",{username:o,client_ip:r}),i(e,200,{ok:1,message:"User deleted successfully"})}catch(n){a.error("User deletion failed via API",{client_ip:r,username:o,error:n.message});const s=n.message==="User not found"?404:400;i(e,s,{error:n.message})}},L=async(t,e,o)=>{if(t.method==="POST"&&o.length===0){await G(t,e);return}if(t.method==="GET"&&o.length===0){await z(t,e);return}if(t.method==="PUT"&&o.length===1){const r=o[0];await F(t,e,r);return}if(t.method==="DELETE"&&o.length===1){const r=o[0];await K(t,e,r);return}i(e,405,{error:"Method not allowed"})},V=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(Y(r)){e.writeHead(429,{"Content-Type":"text/html"}),e.end(c("Too many setup attempts. Please try again later."));return}if($(r),!f()){e.writeHead(400,{"Content-Type":"text/html"}),e.end(c("Setup has already been completed."));return}if(!l||o!==l){a.warn("Invalid setup token attempt",{client_ip:r,provided_token:o}),e.writeHead(403,{"Content-Type":"text/html"}),e.end(c("Invalid or missing setup token."));return}if(t.method==="GET"){e.writeHead(200,{"Content-Type":"text/html"}),e.end(j(l));return}if(t.method==="POST"){try{const n=T();u=!0,l=null,a.info("Setup completed successfully via HTTP interface",{client_ip:r}),e.writeHead(200,{"Content-Type":"text/html"}),e.end(N(n))}catch(n){a.error("Setup failed via HTTP interface",{client_ip:r,error:n.message}),e.writeHead(500,{"Content-Type":"text/html"}),e.end(c(n.message))}return}e.writeHead(405,{"Content-Type":"text/html"}),e.end(c("Method not allowed."))},X=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(a.info("Recovery request received",{client_ip:r,method:t.method}),!o){e.writeHead(400,{"Content-Type":"text/html"}),e.end(c("Recovery token is required."));return}const n=C(o);if(!n.valid){A(r);let s="Invalid recovery token.";n.reason==="expired"?s="Recovery token has expired. Generate a new token using --generate-recovery-token.":n.reason==="locked"?s="Recovery is locked due to too many failed attempts. Please try again later.":n.reason==="no_token"&&(s="No active recovery token found. Generate a new token using --generate-recovery-token."),a.warn("Invalid recovery token attempt",{client_ip:r,reason:n.reason,provided_token:o}),e.writeHead(403,{"Content-Type":"text/html"}),e.end(c(s));return}if(t.method==="GET"){e.writeHead(200,{"Content-Type":"text/html"}),e.end(g(o));return}if(t.method==="POST"){try{const s=await M(t),{password:d,confirm_password:h}=s;if(!d||!h){e.writeHead(400,{"Content-Type":"text/html"}),e.end(g(o,"Both password fields are required."));return}if(d!==h){e.writeHead(400,{"Content-Type":"text/html"}),e.end(g(o,"Passwords do not match."));return}const w=await S(d,r,()=>{a.info("Password change completed, existing connections should be terminated")});a.info("Emergency password change completed via HTTP interface",{client_ip:r,timestamp:w.timestamp}),e.writeHead(200,{"Content-Type":"text/html"}),e.end(W(w.timestamp))}catch(s){a.error("Emergency password change failed via HTTP interface",{client_ip:r,error:s.message}),e.writeHead(400,{"Content-Type":"text/html"}),e.end(g(o,s.message))}return}e.writeHead(405,{"Content-Type":"text/html"}),e.end(c("Method not allowed."))},Q=(t=1984)=>{const e=v.createServer(async(r,n)=>{try{const s=new x(r.url,`http://localhost:${t}`);if(s.pathname==="/setup"){const d=s.searchParams.get("token");await V(r,n,d);return}if(s.pathname==="/recovery"){const d=s.searchParams.get("token");await X(r,n,d);return}if(s.pathname.startsWith("/api/users")){const d=s.pathname.split("/").slice(3);await L(r,n,d);return}n.writeHead(404,{"Content-Type":"text/html"}),n.end(`<!DOCTYPE html>
398
+ </html>`,G=t=>new Promise((e,o)=>{let r="";t.on("data",n=>{r+=n.toString()}),t.on("end",()=>{try{const n=new URLSearchParams(r),s={};for(const[d,f]of n)s[d]=f;e(s)}catch(n){o(n)}}),t.on("error",n=>{o(n)})}),v=t=>new Promise((e,o)=>{let r="";t.on("data",n=>{r+=n.toString()}),t.on("end",()=>{try{const n=JSON.parse(r);e(n)}catch(n){o(n)}}),t.on("error",n=>{o(n)})}),w=t=>{if(l())return!0;const e=t.headers["x-joystick-db-api-key"];return D(e)},i=(t,e,o)=>{t.writeHead(e,{"Content-Type":"application/json"}),t.end(JSON.stringify(o))},z=async(t,e)=>{const o=t.socket.remoteAddress||"127.0.0.1";if(!w(t)){const r=l()?"API key validation failed (this should not happen in development mode)":"Database setup incomplete. A valid API key must be passed until an admin user has been created.";a.warn("Invalid API key for user creation",{client_ip:o,development_mode:l()}),i(e,403,{error:r});return}try{const r=await v(t),n=await E(r);a.info("User created via API",{username:n.username,client_ip:o}),i(e,201,{ok:1,user:n})}catch(r){a.error("User creation failed via API",{client_ip:o,error:r.message}),i(e,400,{error:r.message})}},F=async(t,e)=>{const o=t.socket.remoteAddress||"127.0.0.1";if(!w(t)){const r=l()?"API key validation failed (this should not happen in development mode)":"Database setup incomplete. A valid API key must be passed until an admin user has been created.";a.warn("Invalid API key for get users",{client_ip:o,development_mode:l()}),i(e,403,{error:r});return}try{const r=H();a.info("Users retrieved via API",{count:r.length,client_ip:o}),i(e,200,{ok:1,users:r})}catch(r){a.error("Get users failed via API",{client_ip:o,error:r.message}),i(e,500,{error:r.message})}},K=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(!w(t)){const n=l()?"API key validation failed (this should not happen in development mode)":"Database setup incomplete. A valid API key must be passed until an admin user has been created.";a.warn("Invalid API key for user update",{client_ip:r,username:o,development_mode:l()}),i(e,403,{error:n});return}try{const n=await v(t),s=await O(o,n);a.info("User updated via API",{username:o,client_ip:r}),i(e,200,{ok:1,user:s})}catch(n){a.error("User update failed via API",{client_ip:r,username:o,error:n.message});const s=n.message==="User not found"?404:400;i(e,s,{error:n.message})}},L=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(!w(t)){const n=l()?"API key validation failed (this should not happen in development mode)":"Database setup incomplete. A valid API key must be passed until an admin user has been created.";a.warn("Invalid API key for user deletion",{client_ip:r,username:o,development_mode:l()}),i(e,403,{error:n});return}try{R(o),a.info("User deleted via API",{username:o,client_ip:r}),i(e,200,{ok:1,message:"User deleted successfully"})}catch(n){a.error("User deletion failed via API",{client_ip:r,username:o,error:n.message});const s=n.message==="User not found"?404:400;i(e,s,{error:n.message})}},V=async(t,e,o)=>{if(t.method==="POST"&&o.length===0){await z(t,e);return}if(t.method==="GET"&&o.length===0){await F(t,e);return}if(t.method==="PUT"&&o.length===1){const r=o[0];await K(t,e,r);return}if(t.method==="DELETE"&&o.length===1){const r=o[0];await L(t,e,r);return}i(e,405,{error:"Method not allowed"})},X=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if($(r)){e.writeHead(429,{"Content-Type":"text/html"}),e.end(p("Too many setup attempts. Please try again later."));return}if(j(r),!g()){e.writeHead(400,{"Content-Type":"text/html"}),e.end(p("Setup has already been completed."));return}if(!c||o!==c){a.warn("Invalid setup token attempt",{client_ip:r,provided_token:o}),e.writeHead(403,{"Content-Type":"text/html"}),e.end(p("Invalid or missing setup token."));return}if(t.method==="GET"){e.writeHead(200,{"Content-Type":"text/html"}),e.end(N(c));return}if(t.method==="POST"){try{const n=P();m=!0,c=null,a.info("Setup completed successfully via HTTP interface",{client_ip:r}),e.writeHead(200,{"Content-Type":"text/html"}),e.end(W(n))}catch(n){a.error("Setup failed via HTTP interface",{client_ip:r,error:n.message}),e.writeHead(500,{"Content-Type":"text/html"}),e.end(p(n.message))}return}e.writeHead(405,{"Content-Type":"text/html"}),e.end(p("Method not allowed."))},Q=async(t,e,o)=>{const r=t.socket.remoteAddress||"127.0.0.1";if(a.info("Recovery request received",{client_ip:r,method:t.method}),!o){e.writeHead(400,{"Content-Type":"text/html"}),e.end(p("Recovery token is required."));return}const n=C(o);if(!n.valid){S(r);let s="Invalid recovery token.";n.reason==="expired"?s="Recovery token has expired. Generate a new token using --generate-recovery-token.":n.reason==="locked"?s="Recovery is locked due to too many failed attempts. Please try again later.":n.reason==="no_token"&&(s="No active recovery token found. Generate a new token using --generate-recovery-token."),a.warn("Invalid recovery token attempt",{client_ip:r,reason:n.reason,provided_token:o}),e.writeHead(403,{"Content-Type":"text/html"}),e.end(p(s));return}if(t.method==="GET"){e.writeHead(200,{"Content-Type":"text/html"}),e.end(y(o));return}if(t.method==="POST"){try{const s=await G(t),{password:d,confirm_password:f}=s;if(!d||!f){e.writeHead(400,{"Content-Type":"text/html"}),e.end(y(o,"Both password fields are required."));return}if(d!==f){e.writeHead(400,{"Content-Type":"text/html"}),e.end(y(o,"Passwords do not match."));return}const b=await I(d,r,()=>{a.info("Password change completed, existing connections should be terminated")});a.info("Emergency password change completed via HTTP interface",{client_ip:r,timestamp:b.timestamp}),e.writeHead(200,{"Content-Type":"text/html"}),e.end(M(b.timestamp))}catch(s){a.error("Emergency password change failed via HTTP interface",{client_ip:r,error:s.message}),e.writeHead(400,{"Content-Type":"text/html"}),e.end(y(o,s.message))}return}e.writeHead(405,{"Content-Type":"text/html"}),e.end(p("Method not allowed."))},Z=(t=1984)=>{const e=_.createServer(async(r,n)=>{try{const s=new x(r.url,`http://localhost:${t}`);if(s.pathname==="/setup"){const d=s.searchParams.get("token");await X(r,n,d);return}if(s.pathname==="/recovery"){const d=s.searchParams.get("token");await Q(r,n,d);return}if(s.pathname.startsWith("/api/users")){const d=s.pathname.split("/").slice(3);await V(r,n,d);return}n.writeHead(404,{"Content-Type":"text/html"}),n.end(`<!DOCTYPE html>
399
399
  <html>
400
400
  <head><title>404 Not Found</title></head>
401
401
  <body>
402
402
  <h1>404 Not Found</h1>
403
403
  <p>The requested resource was not found on this server.</p>
404
404
  </body>
405
- </html>`)}catch(s){a.error("HTTP request error",{error:s.message,url:r.url}),n.writeHead(500,{"Content-Type":"text/html"}),n.end(c("Internal server error."))}}),o=new Set;return e.on("connection",r=>{o.add(r),r.on("close",()=>{o.delete(r)})}),e._connections=o,e.on("error",r=>{a.error("HTTP server error",{error:r.message})}),e},Z=(t=1984)=>{const e=f();e&&(l=J(),u=!1);const o=Q(t);return new Promise((r,n)=>{o.once("error",s=>{e&&(l=null,u=!1),a.error("Failed to start HTTP server",{port:t,error:s.message}),n(s)}),o.listen(t,()=>{p=o,e?(a.info("JoystickDB Setup Required"),a.info(`Visit: http://localhost:${t}/setup?token=${l}`)):a.info("HTTP server started for recovery operations",{port:t}),r(o)})})},q=()=>new Promise(t=>{if(!p){t();return}const e=p,o=e._connections||new Set;p=null,l=null,u=!1,m.clear(),o.forEach(r=>{try{r.destroy()}catch{}}),e.close(r=>{r?a.warn("HTTP server close error",{error:r.message}):a.info("HTTP server stopped"),setTimeout(()=>{t()},250)}),setTimeout(()=>{a.warn("HTTP server forced shutdown after timeout"),t()},2e3)}),ee=()=>({setup_required:f(),setup_token:l,setup_completed:u,http_server_running:!!p});export{ee as get_setup_info,f as is_setup_required,Z as start_http_server,q as stop_http_server};
405
+ </html>`)}catch(s){a.error("HTTP request error",{error:s.message,url:r.url}),n.writeHead(500,{"Content-Type":"text/html"}),n.end(p("Internal server error."))}}),o=new Set;return e.on("connection",r=>{o.add(r),r.on("close",()=>{o.delete(r)})}),e._connections=o,e.on("error",r=>{a.error("HTTP server error",{error:r.message})}),e},q=(t=1984)=>{const e=g();e&&(c=Y(),m=!1);const o=Z(t);return new Promise((r,n)=>{o.once("error",s=>{e&&(c=null,m=!1),a.error("Failed to start HTTP server",{port:t,error:s.message}),n(s)}),o.listen(t,()=>{u=o,e?(a.info("JoystickDB Setup Required"),a.info(`Visit: http://localhost:${t}/setup?token=${c}`)):a.info("HTTP server started for recovery operations",{port:t}),r(o)})})},ee=()=>new Promise(t=>{if(!u){t();return}const e=u,o=e._connections||new Set;u=null,c=null,m=!1,h.clear(),o.forEach(r=>{try{r.destroy()}catch{}}),e.close(r=>{r?a.warn("HTTP server close error",{error:r.message}):a.info("HTTP server stopped"),setTimeout(()=>{t()},250)}),setTimeout(()=>{a.warn("HTTP server forced shutdown after timeout"),t()},2e3)}),te=()=>({setup_required:g(),setup_token:c,setup_completed:m,http_server_running:!!u});export{te as get_setup_info,g as is_setup_required,q as start_http_server,ee as stop_http_server};
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.2216",
5
- "canary_version": "0.0.0-canary.2215",
4
+ "version": "0.0.0-canary.2218",
5
+ "canary_version": "0.0.0-canary.2217",
6
6
  "description": "JoystickDB - A minimalist database server for the Joystick framework",
7
7
  "main": "./dist/server/index.js",
8
8
  "scripts": {
@@ -31,6 +31,11 @@ import {
31
31
  verify_user_password,
32
32
  check_admin_user_exists
33
33
  } from './lib/api_key_manager.js';
34
+ import {
35
+ is_development_mode,
36
+ display_development_startup_message,
37
+ warn_undefined_node_env
38
+ } from './lib/development_mode.js';
34
39
  import {
35
40
  restore_backup,
36
41
  start_backup_schedule,
@@ -252,7 +257,7 @@ export const create_server = async () => {
252
257
 
253
258
  initialize_database();
254
259
  initialize_auth_manager();
255
- initialize_api_key_manager();
260
+ await initialize_api_key_manager();
256
261
  initialize_recovery_manager();
257
262
 
258
263
  // NOTE: Initialize replication manager for primary nodes.
@@ -299,6 +304,14 @@ export const create_server = async () => {
299
304
  log.warn('Failed to start HTTP server', { error: http_error.message });
300
305
  }
301
306
 
307
+ // NOTE: Display development mode startup message if in development.
308
+ if (is_development_mode()) {
309
+ const { tcp_port, http_port } = get_port_configuration();
310
+ display_development_startup_message(tcp_port, http_port);
311
+ } else {
312
+ warn_undefined_node_env();
313
+ }
314
+
302
315
  const server = net.createServer((socket = {}) => {
303
316
  if (!connection_manager.add_connection(socket)) {
304
317
  return;
@@ -10,6 +10,7 @@ import crypto from 'crypto';
10
10
  import bcrypt from 'bcrypt';
11
11
  import create_logger from './logger.js';
12
12
  import { get_database, build_collection_key, generate_document_id } from './query_engine.js';
13
+ import { is_development_mode } from './development_mode.js';
13
14
 
14
15
  const { create_context_logger } = create_logger('api_key_manager');
15
16
  const log = create_context_logger();
@@ -47,9 +48,17 @@ const generate_api_key = () => {
47
48
 
48
49
  /**
49
50
  * Loads API key from file or generates new one if it doesn't exist.
51
+ * In development mode, skips API key generation entirely.
50
52
  * @returns {string} API key from file or newly generated key
51
53
  */
52
54
  const load_or_generate_api_key = () => {
55
+ // NOTE: Skip API key generation in development mode.
56
+ if (is_development_mode()) {
57
+ cached_api_key = 'development-mode-bypass';
58
+ log.info('Development mode: API key generation bypassed');
59
+ return cached_api_key;
60
+ }
61
+
53
62
  if (cached_api_key) {
54
63
  return cached_api_key;
55
64
  }
@@ -118,10 +127,16 @@ const display_startup_message = (api_key) => {
118
127
 
119
128
  /**
120
129
  * Validates an API key against the stored key.
130
+ * In development mode, always returns true to bypass API key validation.
121
131
  * @param {string} provided_key - API key to validate
122
132
  * @returns {boolean} True if API key is valid
123
133
  */
124
134
  const validate_api_key = (provided_key) => {
135
+ // NOTE: Bypass API key validation in development mode.
136
+ if (is_development_mode()) {
137
+ return true;
138
+ }
139
+
125
140
  if (!provided_key) {
126
141
  return false;
127
142
  }
@@ -145,16 +160,23 @@ const validate_username = (username) => {
145
160
 
146
161
  /**
147
162
  * Validates password complexity requirements.
163
+ * In development mode, allows simple passwords for default admin user.
148
164
  * @param {string} password - Password to validate
165
+ * @param {boolean} [is_development_default=false] - Whether this is the development default user
149
166
  * @returns {Object} Validation result
150
167
  * @returns {boolean} returns.valid - Whether password is valid
151
168
  * @returns {string} returns.error - Error message if invalid
152
169
  */
153
- const validate_password = (password) => {
170
+ const validate_password = (password, is_development_default = false) => {
154
171
  if (!password || typeof password !== 'string') {
155
172
  return { valid: false, error: 'Password is required' };
156
173
  }
157
174
 
175
+ // NOTE: Skip complexity requirements for development default user.
176
+ if (is_development_mode() && is_development_default) {
177
+ return { valid: true };
178
+ }
179
+
158
180
  if (password.length < 8) {
159
181
  return { valid: false, error: 'Password must be at least 8 characters long' };
160
182
  }
@@ -182,10 +204,11 @@ const validate_role = (role) => {
182
204
  * @param {string} user_data.username - Username
183
205
  * @param {string} user_data.password - Plain text password
184
206
  * @param {string} user_data.role - User role
207
+ * @param {boolean} [is_development_default=false] - Whether this is the development default user
185
208
  * @returns {Promise<Object>} Created user data
186
209
  * @throws {Error} When validation fails or user already exists
187
210
  */
188
- const create_user = async (user_data) => {
211
+ const create_user = async (user_data, is_development_default = false) => {
189
212
  const { username, password, role } = user_data;
190
213
 
191
214
  // Validate input
@@ -193,7 +216,7 @@ const create_user = async (user_data) => {
193
216
  throw new Error('Username must be alphanumeric and 3-50 characters long');
194
217
  }
195
218
 
196
- const password_validation = validate_password(password);
219
+ const password_validation = validate_password(password, is_development_default);
197
220
  if (!password_validation.valid) {
198
221
  throw new Error(password_validation.error);
199
222
  }
@@ -429,16 +452,58 @@ const check_admin_user_exists = () => {
429
452
  return false;
430
453
  };
431
454
 
455
+ /**
456
+ * Creates the default admin user for development mode.
457
+ * @returns {Promise<Object|null>} Created user data or null if already exists
458
+ */
459
+ const create_development_admin_user = async () => {
460
+ if (!is_development_mode()) {
461
+ return null;
462
+ }
463
+
464
+ // Check if admin user already exists
465
+ const existing_admin = get_user('admin');
466
+ if (existing_admin) {
467
+ log.info('Development admin user already exists, skipping creation');
468
+ return existing_admin;
469
+ }
470
+
471
+ try {
472
+ const admin_user = await create_user({
473
+ username: 'admin',
474
+ password: 'password',
475
+ role: 'read_write'
476
+ }, true);
477
+
478
+ log.info('Development admin user created automatically', { username: 'admin' });
479
+ return admin_user;
480
+ } catch (error) {
481
+ log.error('Failed to create development admin user', { error: error.message });
482
+ throw error;
483
+ }
484
+ };
485
+
432
486
  /**
433
487
  * Initializes the API key manager.
488
+ * In development mode, creates default admin user automatically.
434
489
  */
435
- const initialize_api_key_manager = () => {
490
+ const initialize_api_key_manager = async () => {
436
491
  load_or_generate_api_key();
437
492
  admin_user_exists = check_admin_user_exists();
438
493
 
439
494
  if (admin_user_exists) {
440
495
  log.info('Admin user detected, API key requirement relaxed for authenticated operations');
441
496
  }
497
+
498
+ // NOTE: Create development admin user if in development mode.
499
+ if (is_development_mode() && !admin_user_exists) {
500
+ try {
501
+ await create_development_admin_user();
502
+ admin_user_exists = true;
503
+ } catch (error) {
504
+ log.error('Failed to initialize development admin user', { error: error.message });
505
+ }
506
+ }
442
507
  };
443
508
 
444
509
  /**
@@ -469,5 +534,6 @@ export {
469
534
  verify_user_password,
470
535
  check_admin_user_exists,
471
536
  initialize_api_key_manager,
472
- reset_api_key_state
537
+ reset_api_key_state,
538
+ create_development_admin_user
473
539
  };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @fileoverview Development environment detection and setup utilities.
3
+ *
4
+ * Provides functions to detect development mode, bypass security requirements,
5
+ * and automatically create default admin users for local development.
6
+ */
7
+
8
+ import create_logger from './logger.js';
9
+
10
+ const { create_context_logger } = create_logger('development_mode');
11
+ const log = create_context_logger();
12
+
13
+ /**
14
+ * Checks if the server is running in development mode.
15
+ * @returns {boolean} True if NODE_ENV is set to 'development'
16
+ */
17
+ const is_development_mode = () => {
18
+ return process.env.NODE_ENV === 'development';
19
+ };
20
+
21
+ /**
22
+ * Displays development mode startup message with security warnings.
23
+ * @param {number} tcp_port - TCP server port
24
+ * @param {number} http_port - HTTP server port
25
+ */
26
+ const display_development_startup_message = (tcp_port, http_port) => {
27
+ console.log('\nJoystickDB Development Mode\n');
28
+ console.log('Development environment detected (NODE_ENV=development).');
29
+ console.log('Security features have been bypassed for local development.\n');
30
+ console.log('Default admin user created:');
31
+ console.log(' Username: admin');
32
+ console.log(' Password: password\n');
33
+ console.log('WARNING: This configuration is NOT suitable for production use.');
34
+ console.log('Set NODE_ENV to \'production\' or another value for secure operation.\n');
35
+ console.log(`TCP Server: localhost:${tcp_port}`);
36
+ console.log(`HTTP Server: localhost:${http_port}\n`);
37
+
38
+ log.info('Development mode activated', {
39
+ tcp_port,
40
+ http_port,
41
+ default_admin_created: true,
42
+ security_bypassed: true
43
+ });
44
+ };
45
+
46
+ /**
47
+ * Logs warning if NODE_ENV is undefined or empty.
48
+ */
49
+ const warn_undefined_node_env = () => {
50
+ if (!process.env.NODE_ENV) {
51
+ log.warn('NODE_ENV is not set, defaulting to secure mode');
52
+ console.log('WARNING: NODE_ENV is not set. Defaulting to secure mode.');
53
+ }
54
+ };
55
+
56
+ export {
57
+ is_development_mode,
58
+ display_development_startup_message,
59
+ warn_undefined_node_env
60
+ };
@@ -24,6 +24,7 @@ import {
24
24
  delete_user,
25
25
  check_admin_user_exists
26
26
  } from './api_key_manager.js';
27
+ import { is_development_mode } from './development_mode.js';
27
28
 
28
29
  const { create_context_logger } = create_logger('http_server');
29
30
  const log = create_context_logger();
@@ -613,10 +614,16 @@ const parse_json_data = (req) => {
613
614
 
614
615
  /**
615
616
  * Validates API key from request headers.
617
+ * In development mode, always returns true to bypass API key validation.
616
618
  * @param {http.IncomingMessage} req - HTTP request object
617
619
  * @returns {boolean} True if API key is valid
618
620
  */
619
621
  const validate_request_api_key = (req) => {
622
+ // NOTE: Bypass API key validation in development mode.
623
+ if (is_development_mode()) {
624
+ return true;
625
+ }
626
+
620
627
  const api_key = req.headers['x-joystick-db-api-key'];
621
628
  return validate_api_key(api_key);
622
629
  };
@@ -642,9 +649,13 @@ const handle_create_user = async (req, res) => {
642
649
  const client_ip = req.socket.remoteAddress || '127.0.0.1';
643
650
 
644
651
  if (!validate_request_api_key(req)) {
645
- log.warn('Invalid API key for user creation', { client_ip });
652
+ const error_message = is_development_mode()
653
+ ? 'API key validation failed (this should not happen in development mode)'
654
+ : 'Database setup incomplete. A valid API key must be passed until an admin user has been created.';
655
+
656
+ log.warn('Invalid API key for user creation', { client_ip, development_mode: is_development_mode() });
646
657
  send_json_response(res, 403, {
647
- error: 'Database setup incomplete. A valid API key must be passed until an admin user has been created.'
658
+ error: error_message
648
659
  });
649
660
  return;
650
661
  }
@@ -677,9 +688,13 @@ const handle_get_users = async (req, res) => {
677
688
  const client_ip = req.socket.remoteAddress || '127.0.0.1';
678
689
 
679
690
  if (!validate_request_api_key(req)) {
680
- log.warn('Invalid API key for get users', { client_ip });
691
+ const error_message = is_development_mode()
692
+ ? 'API key validation failed (this should not happen in development mode)'
693
+ : 'Database setup incomplete. A valid API key must be passed until an admin user has been created.';
694
+
695
+ log.warn('Invalid API key for get users', { client_ip, development_mode: is_development_mode() });
681
696
  send_json_response(res, 403, {
682
- error: 'Database setup incomplete. A valid API key must be passed until an admin user has been created.'
697
+ error: error_message
683
698
  });
684
699
  return;
685
700
  }
@@ -712,9 +727,13 @@ const handle_update_user = async (req, res, username) => {
712
727
  const client_ip = req.socket.remoteAddress || '127.0.0.1';
713
728
 
714
729
  if (!validate_request_api_key(req)) {
715
- log.warn('Invalid API key for user update', { client_ip, username });
730
+ const error_message = is_development_mode()
731
+ ? 'API key validation failed (this should not happen in development mode)'
732
+ : 'Database setup incomplete. A valid API key must be passed until an admin user has been created.';
733
+
734
+ log.warn('Invalid API key for user update', { client_ip, username, development_mode: is_development_mode() });
716
735
  send_json_response(res, 403, {
717
- error: 'Database setup incomplete. A valid API key must be passed until an admin user has been created.'
736
+ error: error_message
718
737
  });
719
738
  return;
720
739
  }
@@ -750,9 +769,13 @@ const handle_delete_user = async (req, res, username) => {
750
769
  const client_ip = req.socket.remoteAddress || '127.0.0.1';
751
770
 
752
771
  if (!validate_request_api_key(req)) {
753
- log.warn('Invalid API key for user deletion', { client_ip, username });
772
+ const error_message = is_development_mode()
773
+ ? 'API key validation failed (this should not happen in development mode)'
774
+ : 'Database setup incomplete. A valid API key must be passed until an admin user has been created.';
775
+
776
+ log.warn('Invalid API key for user deletion', { client_ip, username, development_mode: is_development_mode() });
754
777
  send_json_response(res, 403, {
755
- error: 'Database setup incomplete. A valid API key must be passed until an admin user has been created.'
778
+ error: error_message
756
779
  });
757
780
  return;
758
781
  }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * @fileoverview Tests for API key manager development mode functionality.
3
+ */
4
+
5
+ import test from 'ava';
6
+ import {
7
+ validate_api_key,
8
+ create_user,
9
+ initialize_api_key_manager,
10
+ reset_api_key_state,
11
+ create_development_admin_user
12
+ } from '../../../src/server/lib/api_key_manager.js';
13
+ import { initialize_database, cleanup_database } from '../../../src/server/lib/query_engine.js';
14
+
15
+ let original_node_env;
16
+ let console_log_calls;
17
+
18
+ test.beforeEach(async (t) => {
19
+ original_node_env = process.env.NODE_ENV;
20
+ console_log_calls = [];
21
+
22
+ // Mock console.log to capture calls
23
+ console.log = (...args) => {
24
+ console_log_calls.push(args.join(' '));
25
+ };
26
+
27
+ // Clean up any existing database first
28
+ try {
29
+ await cleanup_database();
30
+ } catch (error) {
31
+ // Ignore cleanup errors
32
+ }
33
+
34
+ // Clean up any existing API key file and reset state
35
+ reset_api_key_state();
36
+
37
+ // Initialize database for user storage tests with unique path per test
38
+ const test_db_path = `./test_data_dev_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
39
+ initialize_database(test_db_path);
40
+
41
+ // Store the test db path for cleanup
42
+ t.context.test_db_path = test_db_path;
43
+ });
44
+
45
+ test.afterEach(async (t) => {
46
+ process.env.NODE_ENV = original_node_env;
47
+
48
+ // Restore console.log
49
+ console.log = console.log.__original || console.log;
50
+
51
+ // Clean up database and API key state
52
+ try {
53
+ await cleanup_database(true); // Remove test database directory
54
+ } catch (error) {
55
+ // Ignore cleanup errors
56
+ }
57
+
58
+ reset_api_key_state();
59
+ });
60
+
61
+ test('validate_api_key bypasses validation in development mode', (t) => {
62
+ process.env.NODE_ENV = 'development';
63
+
64
+ // Should return true for any API key in development mode
65
+ t.true(validate_api_key('invalid-key'));
66
+ t.true(validate_api_key(''));
67
+ t.true(validate_api_key(null));
68
+ t.true(validate_api_key(undefined));
69
+ });
70
+
71
+ test('validate_api_key enforces validation in production mode', (t) => {
72
+ process.env.NODE_ENV = 'production';
73
+
74
+ // Should return false for invalid keys in production mode
75
+ t.false(validate_api_key('invalid-key'));
76
+ t.false(validate_api_key(''));
77
+ t.false(validate_api_key(null));
78
+ t.false(validate_api_key(undefined));
79
+ });
80
+
81
+ test('validate_api_key enforces validation when NODE_ENV is not set', (t) => {
82
+ delete process.env.NODE_ENV;
83
+
84
+ // Should return false for invalid keys when NODE_ENV is not set
85
+ t.false(validate_api_key('invalid-key'));
86
+ t.false(validate_api_key(''));
87
+ t.false(validate_api_key(null));
88
+ t.false(validate_api_key(undefined));
89
+ });
90
+
91
+ test('create_development_admin_user returns null when not in development mode', async (t) => {
92
+ process.env.NODE_ENV = 'production';
93
+
94
+ const admin_user = await create_development_admin_user();
95
+
96
+ t.is(admin_user, null);
97
+ });
98
+
99
+ test('initialize_api_key_manager works in development mode', async (t) => {
100
+ process.env.NODE_ENV = 'development';
101
+
102
+ await t.notThrowsAsync(async () => {
103
+ await initialize_api_key_manager();
104
+ });
105
+
106
+ // Just verify it doesn't throw - the console output may vary
107
+ t.pass();
108
+ });
109
+
110
+ test('initialize_api_key_manager works in production mode', async (t) => {
111
+ process.env.NODE_ENV = 'production';
112
+
113
+ await t.notThrowsAsync(async () => {
114
+ await initialize_api_key_manager();
115
+ });
116
+
117
+ // Should display setup message
118
+ t.true(console_log_calls.some(call => call.includes('JoystickDB Setup')));
119
+ });
120
+
121
+ test('production security is maintained in production mode', async (t) => {
122
+ process.env.NODE_ENV = 'production';
123
+
124
+ // API key validation should be enforced
125
+ t.false(validate_api_key('invalid-key'));
126
+
127
+ // Password complexity should be enforced
128
+ const error = await t.throwsAsync(async () => {
129
+ await create_user({
130
+ username: 'testuser',
131
+ password: 'simple',
132
+ role: 'read_write'
133
+ });
134
+ });
135
+
136
+ t.true(error.message.includes('Password must be at least 8 characters long'));
137
+
138
+ // Development admin user should not be created
139
+ const admin_user = await create_development_admin_user();
140
+ t.is(admin_user, null);
141
+ });
142
+
143
+ test('production security is maintained when NODE_ENV is undefined', async (t) => {
144
+ delete process.env.NODE_ENV;
145
+
146
+ // API key validation should be enforced
147
+ t.false(validate_api_key('invalid-key'));
148
+
149
+ // Password complexity should be enforced
150
+ const error = await t.throwsAsync(async () => {
151
+ await create_user({
152
+ username: 'testuser',
153
+ password: 'simple',
154
+ role: 'read_write'
155
+ });
156
+ });
157
+
158
+ t.true(error.message.includes('Password must be at least 8 characters long'));
159
+
160
+ // Development admin user should not be created
161
+ const admin_user = await create_development_admin_user();
162
+ t.is(admin_user, null);
163
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * @fileoverview Tests for development mode detection and setup utilities.
3
+ */
4
+
5
+ import test from 'ava';
6
+ import {
7
+ is_development_mode,
8
+ display_development_startup_message,
9
+ warn_undefined_node_env
10
+ } from '../../../src/server/lib/development_mode.js';
11
+
12
+ let original_node_env;
13
+ let console_log_calls;
14
+
15
+ test.beforeEach(() => {
16
+ original_node_env = process.env.NODE_ENV;
17
+ console_log_calls = [];
18
+
19
+ // Mock console.log to capture calls
20
+ console.log = (...args) => {
21
+ console_log_calls.push(args.join(' '));
22
+ };
23
+ });
24
+
25
+ test.afterEach(() => {
26
+ process.env.NODE_ENV = original_node_env;
27
+
28
+ // Restore console.log
29
+ console.log = console.log.__original || console.log;
30
+ });
31
+
32
+ test('is_development_mode returns true when NODE_ENV is development', (t) => {
33
+ process.env.NODE_ENV = 'development';
34
+ t.true(is_development_mode());
35
+ });
36
+
37
+ test('is_development_mode returns false when NODE_ENV is production', (t) => {
38
+ process.env.NODE_ENV = 'production';
39
+ t.false(is_development_mode());
40
+ });
41
+
42
+ test('is_development_mode returns false when NODE_ENV is test', (t) => {
43
+ process.env.NODE_ENV = 'test';
44
+ t.false(is_development_mode());
45
+ });
46
+
47
+ test('is_development_mode returns false when NODE_ENV is undefined', (t) => {
48
+ delete process.env.NODE_ENV;
49
+ t.false(is_development_mode());
50
+ });
51
+
52
+ test('is_development_mode returns false when NODE_ENV is empty string', (t) => {
53
+ process.env.NODE_ENV = '';
54
+ t.false(is_development_mode());
55
+ });
56
+
57
+ test('display_development_startup_message shows correct content with default ports', (t) => {
58
+ const tcp_port = 1983;
59
+ const http_port = 1984;
60
+
61
+ display_development_startup_message(tcp_port, http_port);
62
+
63
+ t.true(console_log_calls.some(call => call.includes('JoystickDB Development Mode')));
64
+ t.true(console_log_calls.some(call => call.includes('Development environment detected')));
65
+ t.true(console_log_calls.some(call => call.includes('Security features have been bypassed')));
66
+ t.true(console_log_calls.some(call => call.includes('Username: admin')));
67
+ t.true(console_log_calls.some(call => call.includes('Password: password')));
68
+ t.true(console_log_calls.some(call => call.includes('WARNING: This configuration is NOT suitable for production')));
69
+ t.true(console_log_calls.some(call => call.includes(`TCP Server: localhost:${tcp_port}`)));
70
+ t.true(console_log_calls.some(call => call.includes(`HTTP Server: localhost:${http_port}`)));
71
+ });
72
+
73
+ test('display_development_startup_message shows custom ports', (t) => {
74
+ const tcp_port = 3000;
75
+ const http_port = 3001;
76
+
77
+ display_development_startup_message(tcp_port, http_port);
78
+
79
+ t.true(console_log_calls.some(call => call.includes(`TCP Server: localhost:${tcp_port}`)));
80
+ t.true(console_log_calls.some(call => call.includes(`HTTP Server: localhost:${http_port}`)));
81
+ });
82
+
83
+ test('warn_undefined_node_env warns when NODE_ENV is undefined', (t) => {
84
+ delete process.env.NODE_ENV;
85
+
86
+ warn_undefined_node_env();
87
+
88
+ t.true(console_log_calls.some(call => call.includes('WARNING: NODE_ENV is not set. Defaulting to secure mode.')));
89
+ });
90
+
91
+ test('warn_undefined_node_env warns when NODE_ENV is empty string', (t) => {
92
+ process.env.NODE_ENV = '';
93
+
94
+ warn_undefined_node_env();
95
+
96
+ t.true(console_log_calls.some(call => call.includes('WARNING: NODE_ENV is not set. Defaulting to secure mode.')));
97
+ });
98
+
99
+ test('warn_undefined_node_env does not warn when NODE_ENV is set', (t) => {
100
+ process.env.NODE_ENV = 'production';
101
+ console_log_calls = []; // Clear any previous calls
102
+
103
+ warn_undefined_node_env();
104
+
105
+ t.false(console_log_calls.some(call => call.includes('WARNING: NODE_ENV is not set. Defaulting to secure mode.')));
106
+ });