@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/LICENSE +21 -0
- package/README.md +861 -0
- package/SECURITY.md +130 -0
- package/dist/assertions.d.ts +67 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +142 -0
- package/dist/assertions.js.map +1 -0
- package/dist/client.d.ts +91 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +157 -0
- package/dist/client.js.map +1 -0
- package/dist/connection.d.ts +27 -0
- package/dist/connection.d.ts.map +1 -0
- package/dist/connection.js +149 -0
- package/dist/connection.js.map +1 -0
- package/dist/container.d.ts +38 -0
- package/dist/container.d.ts.map +1 -0
- package/dist/container.js +116 -0
- package/dist/container.js.map +1 -0
- package/dist/expect.d.ts +74 -0
- package/dist/expect.d.ts.map +1 -0
- package/dist/expect.js +164 -0
- package/dist/expect.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/reporter.d.ts +11 -0
- package/dist/reporter.d.ts.map +1 -0
- package/dist/reporter.js +67 -0
- package/dist/reporter.js.map +1 -0
- package/dist/runner.d.ts +90 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +257 -0
- package/dist/runner.js.map +1 -0
- package/dist/world.d.ts +62 -0
- package/dist/world.d.ts.map +1 -0
- package/dist/world.js +130 -0
- package/dist/world.js.map +1 -0
- package/package.json +76 -0
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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"}
|
package/dist/client.d.ts
ADDED
|
@@ -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"}
|