creepercli 1.0.0 → 1.1.0

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/src/client.js CHANGED
@@ -1,127 +1,137 @@
1
- /*
2
- * Copyright 2026 DemonZDevelopment
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- 'use strict';
18
-
19
- const net = require('net');
20
- const crypto = require('crypto');
21
- const { EventEmitter } = require('events');
22
- const { StringDecoder } = require('string_decoder');
23
- const { ProtocolError, ERR } = require('./protocol');
24
-
25
- class CreeperClient extends EventEmitter {
26
- constructor({ host, port, timeoutMs = 300000 }) {
27
- super();
28
- this.host = host;
29
- this.port = port;
30
- this.timeoutMs = timeoutMs;
31
- this.socket = null;
32
- this.buffer = '';
33
- this._decoder = new StringDecoder('utf8');
34
- this.pending = new Map();
35
- }
36
-
37
- connect() {
38
- return new Promise((resolve, reject) => {
39
- const socket = net.connect({ host: this.host, port: this.port });
40
- this.socket = socket;
41
- socket.setNoDelay(true);
42
- socket.on('connect', () => resolve());
43
- socket.on('error', (err) => reject(err));
44
- socket.on('close', () => this._onClose());
45
- socket.on('data', (chunk) => this._onData(chunk));
46
- });
47
- }
1
+
2
+
3
+ 'use strict';
4
+
5
+ const net = require('net');
6
+ const crypto = require('crypto');
7
+ const { EventEmitter } = require('events');
8
+ const { StringDecoder } = require('string_decoder');
9
+ const { ProtocolError, ERR, PROTOCOL_VERSION } = require('./protocol');
48
10
 
11
+ const MAX_INCOMING_BUFFER_BYTES = 16 * 1024 * 1024;
12
+
13
+ class CreeperClient extends EventEmitter {
14
+ constructor({ host, port, timeoutMs = 300000 }) {
15
+ super();
16
+ this.host = host;
17
+ this.port = port;
18
+ this.timeoutMs = timeoutMs;
19
+ this.socket = null;
20
+ this.buffer = '';
21
+ this._decoder = new StringDecoder('utf8');
22
+ this.pending = new Map();
23
+ }
24
+
25
+ connect() {
26
+ return new Promise((resolve, reject) => {
27
+ const socket = net.connect({ host: this.host, port: this.port });
28
+ this.socket = socket;
29
+ socket.setNoDelay(true);
30
+ socket.on('connect', () => resolve());
31
+ socket.on('error', (err) => reject(err));
32
+ socket.on('close', () => {
33
+ if (this.socket === socket) this.socket = null;
34
+ this._onClose();
35
+ });
36
+ socket.on('data', (chunk) => this._onData(chunk));
37
+ });
38
+ }
39
+
49
40
  _onData(chunk) {
50
41
  this.buffer += this._decoder.write(chunk);
51
- let idx;
52
- while ((idx = this.buffer.indexOf('\n')) !== -1) {
53
- const line = this.buffer.slice(0, idx).trim();
54
- this.buffer = this.buffer.slice(idx + 1);
55
- if (!line) continue;
56
- this._handleLine(line);
42
+ if (Buffer.byteLength(this.buffer, 'utf8') > MAX_INCOMING_BUFFER_BYTES) {
43
+ this._failAll(new ProtocolError(ERR.PAYLOAD_TOO_LARGE, 'Server frame exceeded client buffer limit'));
44
+ this.close();
45
+ return;
46
+ }
47
+ let idx;
48
+ while ((idx = this.buffer.indexOf('\n')) !== -1) {
49
+ const line = this.buffer.slice(0, idx).trim();
50
+ this.buffer = this.buffer.slice(idx + 1);
51
+ if (!line) continue;
52
+ this._handleLine(line);
57
53
  }
58
54
  }
59
-
60
- _handleLine(line) {
61
- let frame;
62
- try {
63
- frame = JSON.parse(line);
55
+
56
+ _handleLine(line) {
57
+ let frame;
58
+ try {
59
+ frame = JSON.parse(line);
64
60
  } catch {
65
61
  return;
66
62
  }
67
- if (frame.type === 'response') {
63
+ if (frame.v !== PROTOCOL_VERSION) {
68
64
  const entry = this.pending.get(frame.id);
69
- if (!entry) return;
70
- this.pending.delete(frame.id);
71
- clearTimeout(entry.timer);
72
- if (frame.ok) {
73
- entry.resolve(frame.data || {});
74
- } else {
75
- entry.reject(new ProtocolError(frame.error && frame.error.code, frame.error && frame.error.message));
65
+ if (entry) {
66
+ this.pending.delete(frame.id);
67
+ clearTimeout(entry.timer);
68
+ entry.reject(new ProtocolError(ERR.BAD_REQUEST,
69
+ `Unsupported protocol version ${frame.v} (expected ${PROTOCOL_VERSION})`));
76
70
  }
77
- } else if (frame.type === 'event') {
78
- this.emit('event', frame.event, frame.data || {});
79
- }
80
- }
81
-
82
- async flush() {
83
- while (this.pending.size > 0) {
84
- await new Promise((r) => setTimeout(r, 25));
85
- }
86
- }
87
-
88
- request(action, params = {}, { timeoutMs } = {}) {
89
- if (!this.socket) {
90
- return Promise.reject(new ProtocolError(ERR.INTERNAL, 'Connection closed'));
71
+ return;
91
72
  }
92
- const id = crypto.randomUUID();
93
- return new Promise((resolve, reject) => {
94
- const timer = setTimeout(() => {
95
- this.pending.delete(id);
96
- reject(new ProtocolError(ERR.TIMEOUT, `Request timed out (${action})`));
97
- }, timeoutMs || this.timeoutMs);
98
- this.pending.set(id, { resolve, reject, timer });
99
- const frame = JSON.stringify({ v: 1, type: 'request', id, action, params });
100
- try {
101
- this.socket.write(frame + '\n');
102
- } catch (err) {
103
- clearTimeout(timer);
104
- this.pending.delete(id);
105
- reject(new ProtocolError(ERR.INTERNAL, `Connection closed (${err.code || err.message})`));
106
- }
107
- });
73
+ if (frame.type === 'response') {
74
+ const entry = this.pending.get(frame.id);
75
+ if (!entry) return;
76
+ this.pending.delete(frame.id);
77
+ clearTimeout(entry.timer);
78
+ if (frame.ok) {
79
+ entry.resolve(frame.data || {});
80
+ } else {
81
+ entry.reject(new ProtocolError(frame.error && frame.error.code, frame.error && frame.error.message));
82
+ }
83
+ } else if (frame.type === 'event') {
84
+ this.emit('event', frame.event, frame.data || {});
85
+ }
86
+ }
87
+
88
+ async flush() {
89
+ while (this.pending.size > 0) {
90
+ await new Promise((r) => setTimeout(r, 25));
91
+ }
92
+ }
93
+
94
+ request(action, params = {}, { timeoutMs } = {}) {
95
+ if (!this.socket || this.socket.destroyed === true || this.socket.writable === false) {
96
+ return Promise.reject(new ProtocolError(ERR.INTERNAL, 'Connection closed'));
97
+ }
98
+ const id = crypto.randomUUID();
99
+ return new Promise((resolve, reject) => {
100
+ const timer = setTimeout(() => {
101
+ this.pending.delete(id);
102
+ reject(new ProtocolError(ERR.TIMEOUT, `Request timed out (${action})`));
103
+ }, timeoutMs || this.timeoutMs);
104
+ this.pending.set(id, { resolve, reject, timer });
105
+ const frame = JSON.stringify({ v: PROTOCOL_VERSION, type: 'request', id, action, params });
106
+ try {
107
+ this.socket.write(frame + '\n');
108
+ } catch (err) {
109
+ clearTimeout(timer);
110
+ this.pending.delete(id);
111
+ reject(new ProtocolError(ERR.INTERNAL, `Connection closed (${err.code || err.message})`));
112
+ }
113
+ });
114
+ }
115
+
116
+ _onClose() {
117
+ this._failAll(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
108
118
  }
109
119
 
110
- _onClose() {
120
+ _failAll(error) {
111
121
  for (const { reject, timer } of this.pending.values()) {
112
122
  clearTimeout(timer);
113
- reject(new ProtocolError(ERR.INTERNAL, 'Connection closed by server'));
123
+ reject(error);
114
124
  }
115
125
  this.pending.clear();
116
- }
117
-
118
- close() {
119
- if (this.socket) {
120
- this.socket.end();
121
- this.socket.destroy();
122
- this.socket = null;
123
- }
124
- }
125
- }
126
-
127
- module.exports = { CreeperClient };
126
+ }
127
+
128
+ close() {
129
+ if (this.socket) {
130
+ this.socket.end();
131
+ this.socket.destroy();
132
+ this.socket = null;
133
+ }
134
+ }
135
+ }
136
+
137
+ module.exports = { CreeperClient };
@@ -1,108 +1,94 @@
1
- /*
2
- * Copyright 2026 DemonZDevelopment
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- 'use strict';
18
-
19
- const { question, readSecret } = require('../prompts');
20
- const session = require('../session');
21
- const config = require('../config');
22
-
23
- function saveCreds(cfg, res) {
24
- session.save({
25
- host: cfg.host,
26
- port: cfg.port,
27
- token: res.token,
28
- username: res.username,
29
- expiresAt: Date.now() + res.expiresInMinutes * 60000,
30
- });
31
- config.save({ host: cfg.host, port: cfg.port });
32
- }
33
-
34
- async function doLogin(ctx) {
35
- const user = await question('Username: ');
36
- const pass = await readSecret('Password: ');
37
- let res;
38
- try {
39
- res = await ctx.client.request('auth.login', { username: user, password: pass }, { timeoutMs: 30000 });
40
- } catch (err) {
41
- if (err.code === 'E_TOTP_REQUIRED') {
42
- const code = await question('TOTP code: ');
43
- res = await ctx.client.request('auth.login', { username: user, password: pass, totp: code }, { timeoutMs: 30000 });
44
- } else {
45
- throw err;
46
- }
47
- }
48
- saveCreds(ctx.cfg, res);
49
- ctx.username = res.username;
50
- ctx.cwd = '/';
51
- console.log(`Authenticated as ${res.username}. Session valid for ${res.expiresInMinutes} minutes.`);
52
- return res;
53
- }
54
-
55
- async function login(ctx) {
56
- await doLogin(ctx);
57
- }
58
-
59
- async function logout(ctx) {
60
- const creds = session.load(ctx.cfg.host, ctx.cfg.port);
61
- await ctx.client.request('auth.logout', { token: creds ? creds.token : null }).catch(() => {});
62
- session.clear();
63
- ctx.username = null;
64
- console.log('Logged out. Local credentials cleared.');
65
- }
66
-
67
- async function whoami(ctx) {
68
- const res = await ctx.client.request('auth.whoami');
69
- console.log(`User ${res.username} (IP ${res.ip}), cwd ${res.cwd}, session expires in ${Math.floor(res.expiresInSeconds / 60)}m${res.totpEnabled ? ', 2FA enabled' : ', 2FA disabled'}`);
70
- }
71
-
72
- async function ping(ctx) {
73
- const res = await ctx.client.request('ping', {}, { timeoutMs: 15000 });
74
- console.log(`pong (protocol v${res.protocol}, plugin v${res.pluginVersion}, server time ${new Date(res.serverTime).toISOString()})`);
75
- }
76
-
77
- async function passwd(ctx) {
78
- const oldP = await readSecret('Current password: ');
79
- const newP = await readSecret('New password: ');
80
- if (newP.length < 8) throw new Error('New password must be at least 8 characters');
81
- await ctx.client.request('auth.passwd', { oldPassword: oldP, newPassword: newP });
82
- console.log('Password changed. Other sessions were invalidated.');
83
- }
84
-
85
- async function totp(ctx, args) {
86
- const sub = args[0];
87
- if (sub === 'setup') {
88
- const pass = await readSecret('Password: ');
89
- const res = await ctx.client.request('auth.totp.setup', { password: pass });
90
- console.log('Scan the QR below with your authenticator app, or enter the secret manually:');
91
- console.log(`Secret: ${res.secret}`);
92
- console.log(`(setup expires in ${Math.floor((res.expiresInSeconds || 600) / 60)} minutes)`);
93
- const qr = require('qrcode-terminal');
94
- qr.generate(res.otpauthUrl, { small: true });
95
- const code = await question('Enter the 6-digit code from your authenticator app: ');
96
- await ctx.client.request('auth.totp.verify', { code });
97
- console.log('TOTP enabled.');
98
- } else if (sub === 'disable') {
99
- const pass = await readSecret('Password: ');
100
- const code = await question('TOTP code: ');
101
- await ctx.client.request('auth.totp.disable', { password: pass, code });
102
- console.log('TOTP disabled.');
103
- } else {
104
- throw new Error('Usage: totp <setup|disable>');
105
- }
106
- }
107
-
108
- module.exports = { login, logout, whoami, ping, passwd, totp, doLogin };
1
+
2
+
3
+ 'use strict';
4
+
5
+ const { question, readSecret } = require('../prompts');
6
+ const session = require('../session');
7
+ const config = require('../config');
8
+
9
+ function saveCreds(cfg, res) {
10
+ session.save({
11
+ host: cfg.host,
12
+ port: cfg.port,
13
+ token: res.token,
14
+ username: res.username,
15
+ expiresAt: Date.now() + res.expiresInMinutes * 60000,
16
+ });
17
+ config.save({ host: cfg.host, port: cfg.port });
18
+ }
19
+
20
+ async function doLogin(ctx) {
21
+ const user = await question('Username: ');
22
+ const pass = await readSecret('Password: ');
23
+ let res;
24
+ try {
25
+ res = await ctx.client.request('auth.login', { username: user, password: pass }, { timeoutMs: 30000 });
26
+ } catch (err) {
27
+ if (err.code === 'E_TOTP_REQUIRED') {
28
+ const code = await question('TOTP code: ');
29
+ res = await ctx.client.request('auth.login', { username: user, password: pass, totp: code }, { timeoutMs: 30000 });
30
+ } else {
31
+ throw err;
32
+ }
33
+ }
34
+ saveCreds(ctx.cfg, res);
35
+ ctx.username = res.username;
36
+ ctx.cwd = '/';
37
+ console.log(`Authenticated as ${res.username}. Session valid for ${res.expiresInMinutes} minutes.`);
38
+ return res;
39
+ }
40
+
41
+ async function login(ctx) {
42
+ await doLogin(ctx);
43
+ }
44
+
45
+ async function logout(ctx) {
46
+ const creds = session.load(ctx.cfg.host, ctx.cfg.port);
47
+ await ctx.client.request('auth.logout', { token: creds ? creds.token : null }).catch(() => {});
48
+ session.clear();
49
+ ctx.username = null;
50
+ console.log('Logged out. Local credentials cleared.');
51
+ }
52
+
53
+ async function whoami(ctx) {
54
+ const res = await ctx.client.request('auth.whoami');
55
+ console.log(`User ${res.username} (IP ${res.ip}), cwd ${res.cwd}, session expires in ${Math.floor(res.expiresInSeconds / 60)}m${res.totpEnabled ? ', 2FA enabled' : ', 2FA disabled'}`);
56
+ }
57
+
58
+ async function ping(ctx) {
59
+ const res = await ctx.client.request('ping', {}, { timeoutMs: 15000 });
60
+ console.log(`pong (protocol v${res.protocol}, plugin v${res.pluginVersion}, server time ${new Date(res.serverTime).toISOString()})`);
61
+ }
62
+
63
+ async function passwd(ctx) {
64
+ const oldP = await readSecret('Current password: ');
65
+ const newP = await readSecret('New password: ');
66
+ if (newP.length < 8) throw new Error('New password must be at least 8 characters');
67
+ await ctx.client.request('auth.passwd', { oldPassword: oldP, newPassword: newP });
68
+ console.log('Password changed. Other sessions were invalidated.');
69
+ }
70
+
71
+ async function totp(ctx, args) {
72
+ const sub = args[0];
73
+ if (sub === 'setup') {
74
+ const pass = await readSecret('Password: ');
75
+ const res = await ctx.client.request('auth.totp.setup', { password: pass });
76
+ console.log('Scan the QR below with your authenticator app, or enter the secret manually:');
77
+ console.log(`Secret: ${res.secret}`);
78
+ console.log(`(setup expires in ${Math.floor((res.expiresInSeconds || 600) / 60)} minutes)`);
79
+ const qr = require('qrcode-terminal');
80
+ qr.generate(res.otpauthUrl, { small: true });
81
+ const code = await question('Enter the 6-digit code from your authenticator app: ');
82
+ await ctx.client.request('auth.totp.verify', { code });
83
+ console.log('TOTP enabled.');
84
+ } else if (sub === 'disable') {
85
+ const pass = await readSecret('Password: ');
86
+ const code = await question('TOTP code: ');
87
+ await ctx.client.request('auth.totp.disable', { password: pass, code });
88
+ console.log('TOTP disabled.');
89
+ } else {
90
+ throw new Error('Usage: totp <setup|disable>');
91
+ }
92
+ }
93
+
94
+ module.exports = { login, logout, whoami, ping, passwd, totp, doLogin };
@@ -1,37 +1,23 @@
1
- /*
2
- * Copyright 2026 DemonZDevelopment
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
-
17
- 'use strict';
18
-
19
- async function exec(ctx, args) {
20
- if (!args.length) throw new Error('Usage: exec <minecraft command>');
21
- const command = args.join(' ');
22
- const res = await ctx.client.request('exec.run', { command }, { timeoutMs: 150000 });
23
- if (res.lines && res.lines.length) console.log(res.lines.join('\n'));
24
- if (!res.success) console.error('Command reported failure');
25
- if (res.elapsedMs != null) console.error(`(completed in ${res.elapsedMs}ms)`);
26
- return res.success ? 0 : 1;
27
- }
28
-
29
- function say(ctx, args) {
30
- return exec(ctx, ['say', ...args]);
31
- }
32
-
33
- function restart(ctx, args) {
34
- return exec(ctx, ['restart', ...args]);
35
- }
36
-
37
- module.exports = { exec, say, restart };
1
+
2
+
3
+ 'use strict';
4
+
5
+ async function exec(ctx, args) {
6
+ if (!args.length) throw new Error('Usage: exec <minecraft command>');
7
+ const command = args.join(' ');
8
+ const res = await ctx.client.request('exec.run', { command }, { timeoutMs: 150000 });
9
+ if (res.lines && res.lines.length) console.log(res.lines.join('\n'));
10
+ if (!res.success) console.error('Command reported failure');
11
+ if (res.elapsedMs != null) console.error(`(completed in ${res.elapsedMs}ms)`);
12
+ return res.success ? 0 : 1;
13
+ }
14
+
15
+ function say(ctx, args) {
16
+ return exec(ctx, ['say', ...args]);
17
+ }
18
+
19
+ function restart(ctx, args) {
20
+ return exec(ctx, ['restart', ...args]);
21
+ }
22
+
23
+ module.exports = { exec, say, restart };