@joystick.js/db-canary 0.0.0-canary.2220 → 0.0.0-canary.2222
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/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import C from"net";import{decode as E}from"msgpackr";import q 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 N}from"./cluster/index.js";import x from"./lib/logger.js";import{initialize_database as A,cleanup_database as B}from"./lib/query_engine.js";import{create_message_parser as D,encode_message as _}from"./lib/tcp_protocol.js";import{create_connection_manager as $}from"./lib/connection_manager.js";import{shutdown_write_queue as F}from"./lib/write_queue.js";import{setup_authentication as J,verify_password as K,get_client_ip as P,is_rate_limited as j,initialize_auth_manager as G,reset_auth_state as M}from"./lib/auth_manager.js";import{initialize_api_key_manager as W}from"./lib/api_key_manager.js";import{is_development_mode as T,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 O,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=P(t);if(j(o)){const n=_({ok:0,error:"Too many failed attempts. Please try again later."});t.write(n),t.end();return}if(!await K(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:J(),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 q.includes(t)},Be=t=>{try{if(typeof t=="string")return S(t);if(Buffer.isBuffer(t)){const r=E(t);return typeof r=="string"?S(r):r}else return t}catch{return null}},b=t=>T()?!0: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")}A(),G(),await W(),O();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=$({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(T()){const{tcp_port:e,http_port:i}=v();H(e,i)}else U();const a=C.createServer((e={})=>{if(!c.add_connection(e))return;const i=D();e.on("data",async n=>{c.update_activity(e.id);try{const h=i.parse_messages(n);for(const z of h){const u=z,l=u?.op||null;if(!l){f(e,{message:"Missing operation type"});continue}if(!pe(l)){f(e,{message:"Invalid operation type"});continue}const I=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()},R=_(w);e.write(R)}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(I)}}}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 F(),await new Promise(e=>setTimeout(e,100)),await B(),M(),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{O();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"}),N(a)}export{ce as authentication,pe as check_op_type,De as create_server,Be as parse_data,_e 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.2222",
|
|
5
|
+
"canary_version": "0.0.0-canary.2221",
|
|
6
6
|
"description": "JoystickDB - A minimalist database server for the Joystick framework",
|
|
7
7
|
"main": "./dist/server/index.js",
|
|
8
8
|
"scripts": {
|
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
|
|
|
@@ -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
|
+
});
|