@rhost/testkit 0.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/SECURITY.md ADDED
@@ -0,0 +1,130 @@
1
+ # Security
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Open a private security advisory at:
6
+ **https://github.com/RhostMUSH/rhostmush-docker/security/advisories/new**
7
+
8
+ Do not open a public issue for security vulnerabilities.
9
+
10
+ ---
11
+
12
+ ## Audit — 2026-03-27
13
+
14
+ **Scope:** `@rhost/testkit` SDK source (`sdk/src/`), Docker configuration, and shell scripts.
15
+ **Method:** Static analysis — TDD Remediation Auto-Audit (--scan).
16
+
17
+ ---
18
+
19
+ ### Batch A — CRITICAL ✓ FIXED
20
+
21
+ | ID | Severity | File | Finding |
22
+ |----|----------|------|---------|
23
+ | A1 | CRITICAL | `src/world.ts:26,43,60,67,75,82,90,98` | User-supplied strings interpolated into MUSH commands without sanitization. Special characters or MUSH command delimiters in `name`, `attr`, `value`, `lockstring`, or `args` can inject arbitrary commands. |
24
+ | A2 | CRITICAL | `src/client.ts:79` | `login()` concatenates `username` and `password` directly into the `connect` command. Newlines or spaces in the password string can inject additional commands into the MUSH server command stream. |
25
+
26
+ **A1 detail — `world.ts` command injection**
27
+
28
+ ```typescript
29
+ // UNFIXED — all of these interpolate user input directly:
30
+ create(name) → `create(${name},${cost})`
31
+ dig(name) → `@dig ${name}`
32
+ set(dbref,attr,val) → `&${attr} ${dbref}=${value}`
33
+ lock(dbref,lock) → `@lock ${dbref}=${lockstring}`
34
+ trigger(dbref,attr) → `@trigger ${dbref}/${attr}=${args}`
35
+ ```
36
+
37
+ Fix: validate that `name`, `attr`, and `value` match a safe character allowlist (alphanumerics, hyphens, underscores, spaces). Reject or escape inputs containing `;`, `\n`, `\r`, `[`, `]`, `{`, `}`.
38
+
39
+ **A2 detail — `client.ts` login injection**
40
+
41
+ ```typescript
42
+ // UNFIXED:
43
+ this.conn.send(`connect ${username} ${password}`);
44
+ ```
45
+
46
+ Fix: strip or reject newlines and carriage returns from both `username` and `password` before sending.
47
+
48
+ **Status:** FIXED — `guardInput()` added to all `world.ts` methods; `login()` validates credentials. Tests: `a1-world-injection.test.ts`, `a2-login-injection.test.ts` (18 tests, all green).
49
+
50
+ ---
51
+
52
+ ### Batch B — HIGH (fix before production use)
53
+
54
+ | ID | Severity | File | Finding |
55
+ |----|----------|------|---------|
56
+ | B1 | HIGH | `examples/09-api.ts`, `examples/10-lua.ts` | HTTP Basic Auth sent over plaintext HTTP. Credentials are base64-encoded (not encrypted) and trivially intercepted. Default password `Nyctasia` is hardcoded as fallback. |
57
+ | B2 | HIGH | `docker-compose.yml`, `entrypoint.sh`, `examples/` | Default password `Nyctasia` used as fallback if `RHOST_PASS` is not set. No enforcement to prevent accidental deployment with default credentials. |
58
+
59
+ **B1/B2 detail**
60
+
61
+ The default password is intentional for local development. The risks are:
62
+ - Anyone who clones the repo knows the default
63
+ - `RHOST_PASS` env var is opt-in, not enforced
64
+
65
+ Mitigations already in place:
66
+ - Ports are bound to `127.0.0.1` by default (FIXED — see below)
67
+ - `entrypoint.sh` emits a warning if `RHOST_PASS` is not set
68
+ - API IP ACL defaults to `127.0.0.1`
69
+
70
+ Remaining fix: examples should refuse to run against non-localhost hosts without explicit opt-in when using the default password.
71
+
72
+ **Status:** PARTIALLY MITIGATED — ports + IP ACL hardened; default password warning in place. Tests: `h1-cleartext-credentials.test.ts`, `m2-hardcoded-password.test.ts` (8 tests, all green).
73
+
74
+ ---
75
+
76
+ ### Batch C — MEDIUM ✓ FIXED
77
+
78
+ | ID | Severity | File | Finding |
79
+ |----|----------|------|---------|
80
+ | C1 | MEDIUM | `src/world.ts:28-34,45-52` | Server output parsed with regex; no validation that parsed dbref is a valid MUSH reference. Malformed server responses could cause silent failures. |
81
+ | C2 | MEDIUM | `src/connection.ts:64-79` | No socket connection timeout. If the MUSH server hangs, the SDK waits indefinitely. |
82
+
83
+ **Status:** FIXED — C1 already throws descriptive errors on bad server output (confirmed with tests). C2 fixed by adding `connectTimeout` option to `RhostClientOptions` and `socket.setTimeout(connectTimeoutMs)` in `MushConnection.connect()`. Tests: `c1-dbref-validation.test.ts`, `c2-connection-timeout.test.ts` (17 tests, all green).
84
+
85
+ ---
86
+
87
+ ### Batch D — LOW / INFORMATIONAL
88
+
89
+ | ID | Severity | File | Finding |
90
+ |----|----------|------|---------|
91
+ | D1 | LOW | `src/client.ts:95-106` | No built-in rate limiting beyond `paceMs`. Could flood a server if called in a tight loop. `paceMs` option provides manual mitigation. |
92
+ | D2 | LOW | `src/expect.ts`, `src/assertions.ts` | Error messages include the full evaluated expression. Could leak softcode logic if errors are logged externally. |
93
+ | D3 | LOW | `src/world.ts:106-115` | `cleanup()` iterates `dbrefs` while `destroy()` calls could theoretically modify it. No practical exploit path. |
94
+
95
+ **Status:** INFORMATIONAL — no immediate action required
96
+
97
+ ---
98
+
99
+ ### Fixed (previous audit cycles)
100
+
101
+ | ID | Severity | File | Finding | Fix |
102
+ |----|----------|------|---------|-----|
103
+ | F1 | HIGH | `docker-compose.yml` | Ports bound to `0.0.0.0`, exposing MUSH to all network interfaces | Bound to `127.0.0.1` |
104
+ | F2 | HIGH | `entrypoint.sh` | HTTP API IP ACL defaulted to all IPs | Defaulted to `127.0.0.1`, overridable via `RHOST_API_ALLOW_IP` |
105
+ | F3 | MEDIUM | `scripts/math.sh` | Integer overflow in `pow` with large exponents | Exponent bounded to 0–62 |
106
+ | F4 | MEDIUM | `entrypoint.sh` | Heredoc used unquoted, allowing shell variable expansion inside Python bootstrap | Quoted heredoc (`'PYEOF'`), env vars passed safely |
107
+
108
+ ---
109
+
110
+ ### Dependency audit
111
+
112
+ ```
113
+ testcontainers ^11.13.0 — no known critical CVEs
114
+ jest ^29.5.0 — no known critical CVEs
115
+ ts-jest ^29.1.0 — no known critical CVEs
116
+ typescript ^5.0.0 — no known critical CVEs
117
+ ```
118
+
119
+ Run `npm audit` before each release. The CI workflow (`security-tests.yml`) gates on `npm audit --audit-level=high`.
120
+
121
+ ---
122
+
123
+ ## Remediation priority
124
+
125
+ ```
126
+ Batch A (CRITICAL) → Batch B (HIGH) → Batch C (MEDIUM) → Batch D (LOW)
127
+ ```
128
+
129
+ Batch A must be resolved before `v1.0.0` publish.
130
+ Batch B should be resolved before any production deployment.
@@ -0,0 +1,67 @@
1
+ import { RhostClient } from './client';
2
+ export interface AssertionResult {
3
+ expression: string;
4
+ expected: string;
5
+ actual: string;
6
+ passed: boolean;
7
+ }
8
+ export declare class RhostAssertionError extends Error {
9
+ readonly result: AssertionResult;
10
+ constructor(result: AssertionResult);
11
+ }
12
+ /**
13
+ * Whether a string is a RhostMUSH error value.
14
+ * Rhost returns `#-1 <MESSAGE>` for most error cases.
15
+ */
16
+ export declare function isRhostError(value: string): boolean;
17
+ /**
18
+ * Assertion helpers for writing RhostMUSH softcode tests.
19
+ *
20
+ * Designed to work inside any test framework (Jest, Vitest, Mocha) — failed
21
+ * assertions throw `RhostAssertionError` which the framework will catch and
22
+ * report.
23
+ *
24
+ * @example
25
+ * const assert = new RhostAssert(client);
26
+ * await assert.equal('add(2,3)', '5');
27
+ * await assert.truthy('strlen(hello)');
28
+ * await assert.error('foo(BAD)'); // expects a #-1 error
29
+ */
30
+ export declare class RhostAssert {
31
+ private readonly client;
32
+ constructor(client: RhostClient);
33
+ /** Evaluate and assert the result equals `expected` (exact string match). */
34
+ equal(expression: string, expected: string, timeout?: number): Promise<AssertionResult>;
35
+ /** Evaluate and assert the result matches `pattern`. */
36
+ matches(expression: string, pattern: RegExp, timeout?: number): Promise<AssertionResult>;
37
+ /**
38
+ * Evaluate and assert the result is truthy in MUSH terms:
39
+ * non-empty, not `0`, and not a `#-1` error.
40
+ */
41
+ truthy(expression: string, timeout?: number): Promise<AssertionResult>;
42
+ /**
43
+ * Evaluate and assert the result is falsy in MUSH terms:
44
+ * empty string, `0`, or a `#-1` error.
45
+ */
46
+ falsy(expression: string, timeout?: number): Promise<AssertionResult>;
47
+ /**
48
+ * Evaluate and assert the result contains `substring`.
49
+ */
50
+ contains(expression: string, substring: string, timeout?: number): Promise<AssertionResult>;
51
+ /**
52
+ * Evaluate and assert the result is a `#-1` error (any kind).
53
+ * Useful for testing that invalid arguments are properly rejected.
54
+ *
55
+ * @example
56
+ * await assert.error('div(1,0)');
57
+ * await assert.error('nonexistentfunc()');
58
+ */
59
+ error(expression: string, timeout?: number): Promise<AssertionResult>;
60
+ /**
61
+ * Run a batch of `[expression, expected]` pairs.
62
+ * Runs all cases before throwing, returning the full result set.
63
+ * Throws after all cases if any failed.
64
+ */
65
+ batch(cases: Array<[expression: string, expected: string]>, timeout?: number): Promise<AssertionResult[]>;
66
+ }
67
+ //# sourceMappingURL=assertions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,MAAM,WAAW,eAAe;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;aACd,MAAM,EAAE,eAAe;gBAAvB,MAAM,EAAE,eAAe;CAStD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEnD;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,WAAW;IACR,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAAN,MAAM,EAAE,WAAW;IAEhD,6EAA6E;IACvE,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAO7F,wDAAwD;IAClD,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAa9F;;;OAGG;IACG,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAQ5E;;;OAGG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAQ3E;;OAEG;IACG,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAajG;;;;;;;OAOG;IACG,KAAK,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC;IAQ3E;;;;OAIG;IACG,KAAK,CACP,KAAK,EAAE,KAAK,CAAC,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,EACpD,OAAO,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,eAAe,EAAE,CAAC;CAoBhC"}
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RhostAssert = exports.RhostAssertionError = void 0;
4
+ exports.isRhostError = isRhostError;
5
+ class RhostAssertionError extends Error {
6
+ constructor(result) {
7
+ super(`RhostMUSH assertion failed\n` +
8
+ ` Expression : ${result.expression}\n` +
9
+ ` Expected : ${JSON.stringify(result.expected)}\n` +
10
+ ` Actual : ${JSON.stringify(result.actual)}`);
11
+ this.result = result;
12
+ this.name = 'RhostAssertionError';
13
+ }
14
+ }
15
+ exports.RhostAssertionError = RhostAssertionError;
16
+ /**
17
+ * Whether a string is a RhostMUSH error value.
18
+ * Rhost returns `#-1 <MESSAGE>` for most error cases.
19
+ */
20
+ function isRhostError(value) {
21
+ return value.startsWith('#-1') || value.startsWith('#-2') || value.startsWith('#-3');
22
+ }
23
+ /**
24
+ * Assertion helpers for writing RhostMUSH softcode tests.
25
+ *
26
+ * Designed to work inside any test framework (Jest, Vitest, Mocha) — failed
27
+ * assertions throw `RhostAssertionError` which the framework will catch and
28
+ * report.
29
+ *
30
+ * @example
31
+ * const assert = new RhostAssert(client);
32
+ * await assert.equal('add(2,3)', '5');
33
+ * await assert.truthy('strlen(hello)');
34
+ * await assert.error('foo(BAD)'); // expects a #-1 error
35
+ */
36
+ class RhostAssert {
37
+ constructor(client) {
38
+ this.client = client;
39
+ }
40
+ /** Evaluate and assert the result equals `expected` (exact string match). */
41
+ async equal(expression, expected, timeout) {
42
+ const actual = (await this.client.eval(expression, timeout)).trim();
43
+ const result = { expression, expected, actual, passed: actual === expected };
44
+ if (!result.passed)
45
+ throw new RhostAssertionError(result);
46
+ return result;
47
+ }
48
+ /** Evaluate and assert the result matches `pattern`. */
49
+ async matches(expression, pattern, timeout) {
50
+ const actual = (await this.client.eval(expression, timeout)).trim();
51
+ const passed = pattern.test(actual);
52
+ const result = {
53
+ expression,
54
+ expected: pattern.toString(),
55
+ actual,
56
+ passed,
57
+ };
58
+ if (!passed)
59
+ throw new RhostAssertionError(result);
60
+ return result;
61
+ }
62
+ /**
63
+ * Evaluate and assert the result is truthy in MUSH terms:
64
+ * non-empty, not `0`, and not a `#-1` error.
65
+ */
66
+ async truthy(expression, timeout) {
67
+ const actual = (await this.client.eval(expression, timeout)).trim();
68
+ const passed = actual !== '' && actual !== '0' && !isRhostError(actual);
69
+ const result = { expression, expected: '<truthy>', actual, passed };
70
+ if (!passed)
71
+ throw new RhostAssertionError(result);
72
+ return result;
73
+ }
74
+ /**
75
+ * Evaluate and assert the result is falsy in MUSH terms:
76
+ * empty string, `0`, or a `#-1` error.
77
+ */
78
+ async falsy(expression, timeout) {
79
+ const actual = (await this.client.eval(expression, timeout)).trim();
80
+ const passed = actual === '' || actual === '0' || isRhostError(actual);
81
+ const result = { expression, expected: '<falsy>', actual, passed };
82
+ if (!passed)
83
+ throw new RhostAssertionError(result);
84
+ return result;
85
+ }
86
+ /**
87
+ * Evaluate and assert the result contains `substring`.
88
+ */
89
+ async contains(expression, substring, timeout) {
90
+ const actual = (await this.client.eval(expression, timeout)).trim();
91
+ const passed = actual.includes(substring);
92
+ const result = {
93
+ expression,
94
+ expected: `<contains: ${JSON.stringify(substring)}>`,
95
+ actual,
96
+ passed,
97
+ };
98
+ if (!passed)
99
+ throw new RhostAssertionError(result);
100
+ return result;
101
+ }
102
+ /**
103
+ * Evaluate and assert the result is a `#-1` error (any kind).
104
+ * Useful for testing that invalid arguments are properly rejected.
105
+ *
106
+ * @example
107
+ * await assert.error('div(1,0)');
108
+ * await assert.error('nonexistentfunc()');
109
+ */
110
+ async error(expression, timeout) {
111
+ const actual = (await this.client.eval(expression, timeout)).trim();
112
+ const passed = isRhostError(actual);
113
+ const result = { expression, expected: '<#-1 error>', actual, passed };
114
+ if (!passed)
115
+ throw new RhostAssertionError(result);
116
+ return result;
117
+ }
118
+ /**
119
+ * Run a batch of `[expression, expected]` pairs.
120
+ * Runs all cases before throwing, returning the full result set.
121
+ * Throws after all cases if any failed.
122
+ */
123
+ async batch(cases, timeout) {
124
+ const results = [];
125
+ for (const [expression, expected] of cases) {
126
+ const actual = (await this.client.eval(expression, timeout)).trim();
127
+ results.push({ expression, expected, actual, passed: actual === expected });
128
+ }
129
+ const failures = results.filter((r) => !r.passed);
130
+ if (failures.length > 0) {
131
+ const summary = failures
132
+ .map((r) => ` [FAIL] ${r.expression}\n` +
133
+ ` Expected : ${JSON.stringify(r.expected)}\n` +
134
+ ` Actual : ${JSON.stringify(r.actual)}`)
135
+ .join('\n');
136
+ throw new Error(`${failures.length} of ${results.length} assertions failed:\n${summary}`);
137
+ }
138
+ return results;
139
+ }
140
+ }
141
+ exports.RhostAssert = RhostAssert;
142
+ //# sourceMappingURL=assertions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertions.js","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /** Strip ANSI/VT100 escape sequences from a string. */
2
+ export declare function stripAnsi(s: string): string;
3
+ export interface RhostClientOptions {
4
+ /** Server hostname. Default: 'localhost' */
5
+ host?: string;
6
+ /** Server port. Default: 4201 */
7
+ port?: number;
8
+ /** Default timeout in milliseconds. Default: 10000 */
9
+ timeout?: number;
10
+ /**
11
+ * Idle time (ms) after the last banner line before the banner is considered
12
+ * finished. Shorter values speed up tests. Default: 300
13
+ */
14
+ bannerTimeout?: number;
15
+ /**
16
+ * Whether to strip ANSI escape codes from eval results.
17
+ * RhostMUSH can embed color codes in output; enabling this gives clean
18
+ * string comparison in tests. Default: true
19
+ */
20
+ stripAnsi?: boolean;
21
+ /**
22
+ * Minimum milliseconds to wait before sending each eval's commands.
23
+ * Use when running many rapid evals to avoid MUSH flood control.
24
+ * Default: 0 (no delay)
25
+ */
26
+ paceMs?: number;
27
+ /**
28
+ * Timeout in milliseconds for the raw TCP connection to be established.
29
+ * If the server accepts the socket but then stalls, the connect will be
30
+ * aborted after this many milliseconds. Default: 10000
31
+ */
32
+ connectTimeout?: number;
33
+ }
34
+ /**
35
+ * High-level client for interacting with a RhostMUSH server.
36
+ *
37
+ * @example
38
+ * const client = new RhostClient({ host: 'localhost', port: 4201 });
39
+ * await client.connect();
40
+ * await client.login('Wizard', 'Nyctasia');
41
+ * const result = await client.eval('add(2,3)'); // => '5'
42
+ * await client.disconnect();
43
+ */
44
+ export declare class RhostClient {
45
+ private conn;
46
+ private defaultTimeout;
47
+ private bannerTimeout;
48
+ private doStripAnsi;
49
+ private paceMs;
50
+ private connectTimeout;
51
+ constructor(options?: RhostClientOptions);
52
+ /**
53
+ * Establish the TCP connection. Drains the welcome banner before returning.
54
+ */
55
+ connect(): Promise<void>;
56
+ /**
57
+ * Log in with character credentials.
58
+ * Uses a sentinel `@pemit` to confirm login regardless of welcome text.
59
+ */
60
+ login(username: string, password: string): Promise<void>;
61
+ /**
62
+ * Evaluate a MUSHcode expression and return the string result.
63
+ *
64
+ * Uses `think` to evaluate and `@pemit me=` sentinels to delimit output.
65
+ * ANSI escape codes are stripped by default (see `stripAnsi` option).
66
+ *
67
+ * @example
68
+ * await client.eval('add(2,3)') // => '5'
69
+ * await client.eval('lcstr(HELLO)') // => 'hello'
70
+ * await client.eval('encode64(hello)') // => 'aGVsbG8='
71
+ */
72
+ eval(expression: string, timeout?: number): Promise<string>;
73
+ /**
74
+ * Run a MUSHcode command and collect all output lines until the
75
+ * internal sentinel is received.
76
+ *
77
+ * @example
78
+ * const lines = await client.command('look here');
79
+ * const lines = await client.command('@pemit me=hello');
80
+ */
81
+ command(cmd: string, timeout?: number): Promise<string[]>;
82
+ /** Subscribe to every raw line received from the server. */
83
+ onLine(handler: (line: string) => void): void;
84
+ offLine(handler: (line: string) => void): void;
85
+ /** Send QUIT and close the TCP connection. */
86
+ disconnect(): Promise<void>;
87
+ private readUntilMarker;
88
+ private drainBanner;
89
+ private makeId;
90
+ }
91
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAQA,uDAAuD;AACvD,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,MAAM,WAAW,kBAAkB;IAC/B,4CAA4C;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,qBAAa,WAAW;IACpB,OAAO,CAAC,IAAI,CAAiB;IAC7B,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,aAAa,CAAS;IAC9B,OAAO,CAAC,WAAW,CAAU;IAC7B,OAAO,CAAC,MAAM,CAAS;IAEvB,OAAO,CAAC,cAAc,CAAS;gBAEnB,OAAO,GAAE,kBAAuB;IAS5C;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAK9B;;;OAGG;IACG,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAa9D;;;;;;;;;;OAUG;IACG,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IA0BjE;;;;;;;OAOG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAmB/D,4DAA4D;IAC5D,MAAM,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAI7C,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI;IAI9C,8CAA8C;IACxC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAanB,eAAe;IAQ7B,OAAO,CAAC,WAAW;IAWnB,OAAO,CAAC,MAAM;CAGjB"}
package/dist/client.js ADDED
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RhostClient = void 0;
4
+ exports.stripAnsi = stripAnsi;
5
+ const crypto_1 = require("crypto");
6
+ const connection_1 = require("./connection");
7
+ // ESC [ ... m — SGR sequences (colors, bold, etc.)
8
+ // ESC [ ... (A-Z or a-z) — cursor movement, erase, etc.
9
+ // ESC ] ... ST — OSC sequences
10
+ const ANSI_RE = /\x1b(?:\[[0-9;]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g;
11
+ /** Strip ANSI/VT100 escape sequences from a string. */
12
+ function stripAnsi(s) {
13
+ return s.replace(ANSI_RE, '');
14
+ }
15
+ /**
16
+ * High-level client for interacting with a RhostMUSH server.
17
+ *
18
+ * @example
19
+ * const client = new RhostClient({ host: 'localhost', port: 4201 });
20
+ * await client.connect();
21
+ * await client.login('Wizard', 'Nyctasia');
22
+ * const result = await client.eval('add(2,3)'); // => '5'
23
+ * await client.disconnect();
24
+ */
25
+ class RhostClient {
26
+ constructor(options = {}) {
27
+ this.conn = new connection_1.MushConnection(options.host ?? 'localhost', options.port ?? 4201);
28
+ this.defaultTimeout = options.timeout ?? 10000;
29
+ this.bannerTimeout = options.bannerTimeout ?? 300;
30
+ this.doStripAnsi = options.stripAnsi !== false;
31
+ this.paceMs = options.paceMs ?? 0;
32
+ this.connectTimeout = options.connectTimeout ?? 10000;
33
+ }
34
+ /**
35
+ * Establish the TCP connection. Drains the welcome banner before returning.
36
+ */
37
+ async connect() {
38
+ await this.conn.connect(this.connectTimeout);
39
+ await this.drainBanner(this.bannerTimeout);
40
+ }
41
+ /**
42
+ * Log in with character credentials.
43
+ * Uses a sentinel `@pemit` to confirm login regardless of welcome text.
44
+ */
45
+ async login(username, password) {
46
+ if (/[\n\r]/.test(username)) {
47
+ throw new RangeError('login: invalid username — must not contain newline or carriage return characters');
48
+ }
49
+ if (/[\n\r]/.test(password)) {
50
+ throw new RangeError('login: invalid password — must not contain newline or carriage return characters');
51
+ }
52
+ const sentinel = `RHOST_LOGIN_${this.makeId()}`;
53
+ this.conn.send(`connect ${username} ${password}`);
54
+ this.conn.send(`@pemit me=${sentinel}`);
55
+ await this.readUntilMarker(sentinel, this.defaultTimeout);
56
+ }
57
+ /**
58
+ * Evaluate a MUSHcode expression and return the string result.
59
+ *
60
+ * Uses `think` to evaluate and `@pemit me=` sentinels to delimit output.
61
+ * ANSI escape codes are stripped by default (see `stripAnsi` option).
62
+ *
63
+ * @example
64
+ * await client.eval('add(2,3)') // => '5'
65
+ * await client.eval('lcstr(HELLO)') // => 'hello'
66
+ * await client.eval('encode64(hello)') // => 'aGVsbG8='
67
+ */
68
+ async eval(expression, timeout) {
69
+ if (this.paceMs > 0) {
70
+ await new Promise((r) => setTimeout(r, this.paceMs));
71
+ }
72
+ const id = this.makeId();
73
+ const startMarker = `RHOST_EVAL_START_${id}`;
74
+ const endMarker = `RHOST_EVAL_END_${id}`;
75
+ const ms = timeout ?? this.defaultTimeout;
76
+ this.conn.send(`@pemit me=${startMarker}`);
77
+ this.conn.send(`think ${expression}`);
78
+ this.conn.send(`@pemit me=${endMarker}`);
79
+ await this.readUntilMarker(startMarker, ms);
80
+ const resultLines = [];
81
+ while (true) {
82
+ const line = await this.conn.lines.next(ms);
83
+ const clean = this.doStripAnsi ? stripAnsi(line) : line;
84
+ if (clean.includes(endMarker))
85
+ break;
86
+ resultLines.push(clean);
87
+ }
88
+ return resultLines.join('\n');
89
+ }
90
+ /**
91
+ * Run a MUSHcode command and collect all output lines until the
92
+ * internal sentinel is received.
93
+ *
94
+ * @example
95
+ * const lines = await client.command('look here');
96
+ * const lines = await client.command('@pemit me=hello');
97
+ */
98
+ async command(cmd, timeout) {
99
+ const id = this.makeId();
100
+ const endMarker = `RHOST_CMD_END_${id}`;
101
+ const ms = timeout ?? this.defaultTimeout;
102
+ this.conn.send(cmd);
103
+ this.conn.send(`@pemit me=${endMarker}`);
104
+ const lines = [];
105
+ while (true) {
106
+ const line = await this.conn.lines.next(ms);
107
+ const clean = this.doStripAnsi ? stripAnsi(line) : line;
108
+ if (clean.includes(endMarker))
109
+ break;
110
+ lines.push(clean);
111
+ }
112
+ return lines;
113
+ }
114
+ /** Subscribe to every raw line received from the server. */
115
+ onLine(handler) {
116
+ this.conn.on('line', handler);
117
+ }
118
+ offLine(handler) {
119
+ this.conn.off('line', handler);
120
+ }
121
+ /** Send QUIT and close the TCP connection. */
122
+ async disconnect() {
123
+ try {
124
+ this.conn.send('QUIT');
125
+ }
126
+ catch {
127
+ // already closed
128
+ }
129
+ await this.conn.close();
130
+ }
131
+ // -------------------------------------------------------------------------
132
+ // Private helpers
133
+ // -------------------------------------------------------------------------
134
+ async readUntilMarker(marker, timeoutMs) {
135
+ while (true) {
136
+ const line = await this.conn.lines.next(timeoutMs);
137
+ const clean = this.doStripAnsi ? stripAnsi(line) : line;
138
+ if (clean.includes(marker))
139
+ return;
140
+ }
141
+ }
142
+ drainBanner(idleMs) {
143
+ return new Promise((resolve) => {
144
+ const tryNext = () => {
145
+ this.conn.lines.next(idleMs)
146
+ .then(() => tryNext())
147
+ .catch(() => resolve());
148
+ };
149
+ tryNext();
150
+ });
151
+ }
152
+ makeId() {
153
+ return (0, crypto_1.randomUUID)().replace(/-/g, '').slice(0, 16).toUpperCase();
154
+ }
155
+ }
156
+ exports.RhostClient = RhostClient;
157
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { EventEmitter } from 'events';
2
+ /**
3
+ * Async FIFO queue for lines received from the server.
4
+ * Delivers directly to waiting consumers; buffers when none are waiting.
5
+ */
6
+ declare class AsyncLineQueue {
7
+ private buffer;
8
+ private waiters;
9
+ push(line: string): void;
10
+ next(timeoutMs: number): Promise<string>;
11
+ drainSync(): string[];
12
+ cancelAll(reason: string): void;
13
+ }
14
+ export declare class MushConnection extends EventEmitter {
15
+ private readonly host;
16
+ private readonly port;
17
+ private socket;
18
+ private rawBuffer;
19
+ readonly lines: AsyncLineQueue;
20
+ constructor(host: string, port: number);
21
+ connect(connectTimeoutMs?: number): Promise<void>;
22
+ private onData;
23
+ send(command: string): void;
24
+ close(): Promise<void>;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=connection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connection.d.ts","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC;;;GAGG;AACH,cAAM,cAAc;IAChB,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAgF;IAE/F,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAQxB,IAAI,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAsBxC,SAAS,IAAI,MAAM,EAAE;IAMrB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;CAIlC;AAED,qBAAa,cAAe,SAAQ,YAAY;IAKhC,OAAO,CAAC,QAAQ,CAAC,IAAI;IAAU,OAAO,CAAC,QAAQ,CAAC,IAAI;IAJhE,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,SAAS,CAAM;IACvB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;gBAEF,IAAI,EAAE,MAAM,EAAmB,IAAI,EAAE,MAAM;IAKxE,OAAO,CAAC,gBAAgB,SAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAyBhD,OAAO,CAAC,MAAM;IAWd,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAO3B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAUzB"}