From d11fa25be462f00b7b297f42a3d784fe011487c8 Mon Sep 17 00:00:00 2001 From: ZVN DEV <78920650+zvndev@users.noreply.github.com> Date: Tue, 9 Jun 2026 20:03:32 -0400 Subject: [PATCH] feat(ts-client): send username for multi-user auth; drop dead source maps (0.4.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the optional Connect-frame username (length-prefixed, appended after the password — mirrors crates/server/src/protocol.rs) so Node clients can authenticate to multi-user servers. When `user` is omitted the frame stays byte-identical to the 0.3.x legacy shape, so old servers are unaffected. - protocol.ts: Connect variant gains `username: string | null`; encoder omits the field entirely when null; decoder parses the optional trailing username exactly like the server (absent or empty → null) - index.ts: `user?: string` on ClientOptions, threaded into the handshake; PoolOptions inherits it via the existing ClientOptions spread - test/protocol.test.ts: 4 new round-trip/byte-compat tests (TDD-first) - test/auth.test.ts: new live integration suite (pnpm run test:auth) — alice(readwrite) writes OK; bob(readonly) reads OK, insert rejected with `permission denied` and the connection survives; no-user / wrong-password / unknown-user all reject with auth_failed; legacy no-auth server still accepts the username-less frame - tsconfig.json: sourceMap/declarationMap off — tarball no longer ships 12 dead .map files (verified with npm pack --dry-run) - version 0.4.0; CHANGELOG + README document the `user` option and the compat matrix (multi-user mode requires client >=0.4.0 AND server >=0.4.6), replacing the 0.3.x incompatibility caveat All suites green: 207 tests (193 existing + 14 new). Co-Authored-By: Claude Fable 5 --- clients/ts/CHANGELOG.md | 26 +++ clients/ts/README.md | 39 +++- clients/ts/package.json | 3 +- clients/ts/src/index.ts | 14 +- clients/ts/src/protocol.ts | 33 +++- clients/ts/test/auth.test.ts | 315 +++++++++++++++++++++++++++++++ clients/ts/test/protocol.test.ts | 82 ++++++++ clients/ts/tsconfig.json | 4 +- 8 files changed, 503 insertions(+), 13 deletions(-) create mode 100644 clients/ts/test/auth.test.ts diff --git a/clients/ts/CHANGELOG.md b/clients/ts/CHANGELOG.md index a74bc18..4c9090e 100644 --- a/clients/ts/CHANGELOG.md +++ b/clients/ts/CHANGELOG.md @@ -6,12 +6,38 @@ All notable changes to `@zvndev/powdb-client`. | Client version | Compatible PowDB server | Notes | |---|---|---| +| 0.4.x | 0.3.x – 0.4.x | Wire protocol v1 plus the optional Connect `username` field. **Multi-user mode requires client ≥0.4.0 AND server ≥0.4.6** (the server enforces roles as of 0.4.6; 0.4.5 accepted the username but did not enforce `readonly`). When `user` is omitted the Connect frame is byte-identical to the 0.3.x shape, so legacy shared-password and no-auth servers work unchanged. | | 0.3.x | 0.3.x – 0.4.x | Wire protocol v1. The client warns only on a major-version mismatch, so any `0.x` server connects. Minor server bumps may add new opcodes; the client tolerates unknown response codes by surfacing `PowDBError`. Pin both ends. **Caveat:** the 0.3.x client has no `user` option, so it cannot authenticate to a 0.4.5+ server running in **multi-user mode** (the server requires a username once any named user is defined). Shared-password mode works fine. | The client warns on major-version mismatch with the server during the handshake. Within `0.x`, treat any minor-version skew between client and server as best-effort and pin both ends. +## [0.4.0] — 2026-06-09 + +### Added +- `user` option on `ClientOptions` (and therefore `PoolOptions`) for + multi-user authentication. The username is sent as a length-prefixed + string appended after the password in the Connect frame, mirroring + `crates/server/src/protocol.rs`. When omitted, the frame stays + byte-identical to the 0.3.x legacy shape, so older servers are unaffected. +- Live integration suite (`pnpm run test:auth`) covering readwrite/readonly + roles, `permission denied` on readonly writes, and `auth_failed` on + missing user / wrong password / unknown user. + +### Changed +- Version jumps to **0.4.0** (semver-minor for a feature in 0.x), which also + intentionally aligns the client's minor with the server's: multi-user mode + end-to-end requires client ≥0.4.0 and server ≥0.4.6. +- The exported `Message` Connect variant now carries `username: string | null`. + Callers constructing Connect frames directly via `encode(...)` must add the + field (pass `null` for legacy behaviour). + +### Fixed +- `sourceMap`/`declarationMap` disabled in `tsconfig.json` — the tarball no + longer ships 12 dead `.map` files (they pointed at `src/`, which the 0.3.4 + `files` allowlist already excluded, so they were broken references). + ## [0.3.5] — 2026-06-05 ### Fixed diff --git a/clients/ts/README.md b/clients/ts/README.md index 0a49576..ea93ddf 100644 --- a/clients/ts/README.md +++ b/clients/ts/README.md @@ -60,6 +60,36 @@ escapeIdent("User"); // → "User" (throws on invalid) `escapeLiteral` accepts `string | number | bigint | boolean | null`. It rejects `NaN`/`Infinity`, `undefined`, objects, arrays, symbols, and `Date` — convert those yourself before passing them in. +## Authentication + +For servers using the legacy shared password (`POWDB_PASSWORD`), pass +`password` alone. For servers with named users (multi-user mode), pass +`user` + `password`: + +```typescript +const client = await Client.connect({ + host: "localhost", + port: 5433, + user: "alice", // named user (multi-user mode) + password: "s3cret", +}); +``` + +Roles are enforced server-side: a `readonly` user's writes are rejected with +a `PowDBError` (`code: "query_failed"`, message containing `permission +denied`) — the connection stays usable for reads. A missing or wrong +`user`/`password` rejects the handshake with `code: "auth_failed"`. + +Version matrix for multi-user mode: + +| | Requirement | +|---|---| +| Client | ≥0.4.0 (adds the `user` option) | +| Server | ≥0.4.6 (enforces roles; 0.4.5 accepted usernames but did not enforce `readonly`) | + +When `user` is omitted the Connect frame is byte-identical to the 0.3.x +client's, so legacy shared-password and no-auth servers work unchanged. + ## Connection pooling For multi-query workloads (web servers, batch jobs), use `Pool`: @@ -239,14 +269,13 @@ Returns a `Promise`. Options: | `host` | `string` | *(required)* | Server hostname or IP | | `port` | `number` | *(required)* | Server port | | `dbName` | `string` | `"default"` | Database name | -| `password` | `string \| null` | `null` | Server password (if auth is enabled) | +| `password` | `string \| null` | `null` | Password — shared (`POWDB_PASSWORD`) or the named user's | +| `user` | `string` | *(omitted)* | User name for multi-user servers (see Authentication above). Omit for shared-password / no-auth servers | | `connectTimeoutMs` | `number` | `5000` | Connection timeout in milliseconds | | `tls` | `boolean \| tls.ConnectionOptions` | `false` | Enable TLS; `true` uses system defaults, or pass a `tls.connect` options object | -> **Multi-user servers:** PowDB server 0.4.5 added named users with roles. The -> 0.3.x client has no `user` option, so it **cannot authenticate to a server -> running in multi-user mode** (the server requires a username once any user -> is defined). Shared-password mode (`POWDB_PASSWORD`) works as before. +> **Multi-user servers:** requires client ≥0.4.0 (`user` option) and server +> ≥0.4.6 (enforced roles). See the version matrix under Authentication. ### `client.query(query, opts?)` diff --git a/clients/ts/package.json b/clients/ts/package.json index 80e82e5..64c4c81 100644 --- a/clients/ts/package.json +++ b/clients/ts/package.json @@ -1,6 +1,6 @@ { "name": "@zvndev/powdb-client", - "version": "0.3.5", + "version": "0.4.0", "description": "TypeScript client for PowDB (PowQL wire protocol)", "type": "module", "packageManager": "pnpm@10.29.3", @@ -51,6 +51,7 @@ "test": "tsx test/run-with-server.ts test/client.test.ts", "test:escape": "tsx test/escape.test.ts", "test:protocol": "tsx test/protocol.test.ts", + "test:auth": "tsx test/auth.test.ts", "test:pool": "tsx test/run-with-server.ts test/pool.test.ts", "test:typed": "tsx test/typed.test.ts", "test:errors": "tsx test/errors.test.ts", diff --git a/clients/ts/src/index.ts b/clients/ts/src/index.ts index 1d766cf..470ffff 100644 --- a/clients/ts/src/index.ts +++ b/clients/ts/src/index.ts @@ -27,7 +27,7 @@ import { } from "./typed.js"; /** Client library version. Compared to the server's reported version. */ -export const CLIENT_VERSION = "0.3.5"; +export const CLIENT_VERSION = "0.4.0"; export type QueryResult = | { kind: "rows"; columns: string[]; rows: string[][] } @@ -40,6 +40,13 @@ export interface ClientOptions { port: number; dbName?: string; password?: string | null; + /** + * User name for multi-user authentication. Servers ≥0.4.5 with named users + * defined require a `(user, password)` pair; role enforcement (readonly vs + * readwrite) requires server ≥0.4.6. Omit for legacy shared-password or + * no-auth servers — the Connect frame is then byte-identical to 0.3.x. + */ + user?: string; /** Connection timeout in ms. Defaults to 5000. */ connectTimeoutMs?: number; /** @@ -133,6 +140,7 @@ export class Client extends EventEmitter { port, dbName = "default", password = null, + user, connectTimeoutMs = 5000, tls: tlsOpt = false, } = opts; @@ -184,7 +192,9 @@ export class Client extends EventEmitter { socket.on("close", onClose); }); - socket.write(encode({ type: "Connect", dbName, password })); + socket.write( + encode({ type: "Connect", dbName, password, username: user ?? null }), + ); const reply = await handshake; if (reply.type === "Error") { diff --git a/clients/ts/src/protocol.ts b/clients/ts/src/protocol.ts index fc578c3..a63a70d 100644 --- a/clients/ts/src/protocol.ts +++ b/clients/ts/src/protocol.ts @@ -31,7 +31,19 @@ export const MAX_ROWS = 10_000_000; export const MAX_COLUMNS = 4096; export type Message = - | { type: "Connect"; dbName: string; password: string | null } + | { + type: "Connect"; + dbName: string; + password: string | null; + /** + * Optional user name for multi-user authentication (server ≥0.4.6 + * enforces roles). Encoded as a length-prefixed string appended AFTER + * the password field. When `null` the field is omitted entirely, so the + * frame is byte-identical to the pre-username (0.3.x) shape and old + * servers accept it unchanged. + */ + username: string | null; + } | { type: "ConnectOk"; version: string } | { type: "Query"; query: string } | { type: "ResultRows"; columns: string[]; rows: string[][] } @@ -54,7 +66,15 @@ export function encode(msg: Message): Buffer { const dbBuf = encodeString(msg.dbName); const pwBuf = msg.password === null ? u32LE(0) : encodeString(msg.password); - payload = Buffer.concat([dbBuf, pwBuf]); + // Username rides after the password (append-only extension, mirrors + // crates/server/src/protocol.rs). The server reads it only when bytes + // remain after the password, so omitting it when null keeps the frame + // byte-identical to the legacy 0.3.x shape. + const parts = [dbBuf, pwBuf]; + if (msg.username !== null) { + parts.push(encodeString(msg.username)); + } + payload = Buffer.concat(parts); msgType = MSG_CONNECT; break; } @@ -155,7 +175,14 @@ function decodePayload(msgType: number, payload: Buffer): Message { const p = decodeString(payload, cursor); password = p.length === 0 ? null : p; } - return { type: "Connect", dbName, password }; + // Optional trailing username (mirror the server: present only when + // bytes remain after the password; empty string means null). + let username: string | null = null; + if (cursor.pos < payload.length) { + const u = decodeString(payload, cursor); + username = u.length === 0 ? null : u; + } + return { type: "Connect", dbName, password, username }; } case MSG_CONNECT_OK: return { type: "ConnectOk", version: decodeString(payload, cursor) }; diff --git a/clients/ts/test/auth.test.ts b/clients/ts/test/auth.test.ts new file mode 100644 index 0000000..6f569d4 --- /dev/null +++ b/clients/ts/test/auth.test.ts @@ -0,0 +1,315 @@ +/** + * Live multi-user authentication tests for the PowDB TypeScript client. + * + * Spawns its own powdb-server instances (this suite needs a data dir seeded + * with named users via `powdb-cli useradd`, so it cannot reuse the plain + * run-with-server harness): + * + * - port 7771: multi-user server (alice=readwrite, bob=readonly) + * - port 7772: legacy server (no users, no password) for back-compat + * + * Uses the prebuilt binaries in ../../target/release when present, falling + * back to `cargo run --release` otherwise. Data dirs live under + * /tmp/powdb-sweep/ts-auth/ and are wiped on entry and exit. + * + * Run with: + * pnpm run test:auth + * + * Role enforcement (the readonly rejection assertions) requires a server + * with RBAC enforcement (≥0.4.6 / the ecosystem-sweep build). + */ + +import * as fs from "node:fs"; +import * as fsp from "node:fs/promises"; +import * as net from "node:net"; +import * as path from "node:path"; +import { spawn, spawnSync, type ChildProcess } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import { strict as assert } from "node:assert"; +import { Client } from "../src/index.js"; +import { isPowDBError } from "../src/errors.js"; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(here, "..", "..", ".."); +const HOST = "127.0.0.1"; +const MULTI_PORT = 7771; +const LEGACY_PORT = 7772; +const BASE_DIR = "/tmp/powdb-sweep/ts-auth"; +const MULTI_DIR = path.join(BASE_DIR, "multi"); +const LEGACY_DIR = path.join(BASE_DIR, "legacy"); + +let passed = 0; +let failed = 0; +const failures: string[] = []; + +async function test(name: string, fn: () => Promise | void) { + try { + await fn(); + passed++; + console.log(` ✓ ${name}`); + } catch (err) { + failed++; + const msg = err instanceof Error ? err.message : String(err); + failures.push(`${name}: ${msg}`); + console.log(` ✗ ${name}`); + console.log(` ${msg}`); + } +} + +// ───── server / cli plumbing ──────────────────────────────────────────────── + +function bin(name: string): { cmd: string; prefix: string[] } { + const release = path.join(repoRoot, "target", "release", name); + if (fs.existsSync(release)) { + return { cmd: release, prefix: [] }; + } + // Fall back to cargo (slower, but works on a fresh checkout). + return { + cmd: "cargo", + prefix: ["run", "--release", "-p", name, "--"], + }; +} + +function runCli(args: string[]): void { + const { cmd, prefix } = bin("powdb-cli"); + const r = spawnSync(cmd, [...prefix, ...args], { + cwd: repoRoot, + encoding: "utf8", + }); + if (r.status !== 0) { + throw new Error( + `powdb-cli ${args.join(" ")} failed (${r.status}): ${r.stderr}`, + ); + } +} + +function canConnect(port: number): Promise { + return new Promise((resolve) => { + const socket = new net.Socket(); + let done = false; + const finish = (ok: boolean) => { + if (done) return; + done = true; + socket.destroy(); + resolve(ok); + }; + socket.setTimeout(250); + socket.once("connect", () => finish(true)); + socket.once("timeout", () => finish(false)); + socket.once("error", () => finish(false)); + socket.connect(port, HOST); + }); +} + +async function startServer( + port: number, + dataDir: string, +): Promise { + const { cmd, prefix } = bin("powdb-server"); + const child = spawn( + cmd, + [...prefix, "--bind", HOST, "--port", String(port), "--data-dir", dataDir], + { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] }, + ); + const deadline = Date.now() + 120_000; + while (Date.now() < deadline) { + if (child.exitCode !== null) { + throw new Error(`powdb-server exited early with code ${child.exitCode}`); + } + if (await canConnect(port)) return child; + await new Promise((r) => setTimeout(r, 200)); + } + child.kill("SIGKILL"); + throw new Error(`timed out waiting for powdb-server on ${HOST}:${port}`); +} + +async function stopServer(child: ChildProcess): Promise { + if (child.exitCode !== null) return; + child.kill("SIGINT"); + await new Promise((resolve) => { + const timer = setTimeout(() => { + child.kill("SIGKILL"); + resolve(); + }, 5_000); + child.once("exit", () => { + clearTimeout(timer); + resolve(); + }); + }); +} + +// ───── suite ──────────────────────────────────────────────────────────────── + +async function main() { + await fsp.rm(BASE_DIR, { recursive: true, force: true }); + await fsp.mkdir(MULTI_DIR, { recursive: true }); + await fsp.mkdir(LEGACY_DIR, { recursive: true }); + + // Seed the multi-user store offline, exactly as documented in + // docs/getting-started.md ("Multi-user authentication"). + runCli(["--data-dir", MULTI_DIR, "useradd", "alice", "--role", "readwrite", "--password", "s3cret"]); + runCli(["--data-dir", MULTI_DIR, "useradd", "bob", "--role", "readonly", "--password", "hunter2"]); + + console.log(`\nStarting multi-user server on ${HOST}:${MULTI_PORT}...`); + const multiServer = await startServer(MULTI_PORT, MULTI_DIR); + console.log(`Starting legacy (no-users) server on ${HOST}:${LEGACY_PORT}...`); + const legacyServer = await startServer(LEGACY_PORT, LEGACY_DIR); + + try { + console.log("\nMulti-user server — readwrite (alice)"); + + let alice: Client | null = null; + + await test("alice (readwrite) authenticates", async () => { + alice = await Client.connect({ + host: HOST, + port: MULTI_PORT, + user: "alice", + password: "s3cret", + }); + assert.ok(alice.serverVersion, "should report a server version"); + }); + + await test("alice can create a type and insert", async () => { + assert.ok(alice, "alice must be connected"); + const ddl = await alice.query( + "type AuthUsers { required name: str, age: int }", + ); + assert.equal(ddl.kind, "message"); + const ins = await alice.query( + 'insert AuthUsers { name := "Zed", age := 41 }', + ); + assert.equal(ins.kind, "ok"); + if (ins.kind === "ok") assert.equal(Number(ins.affected), 1); + }); + + console.log("\nMulti-user server — readonly (bob)"); + + let bob: Client | null = null; + + await test("bob (readonly) authenticates", async () => { + bob = await Client.connect({ + host: HOST, + port: MULTI_PORT, + user: "bob", + password: "hunter2", + }); + assert.ok(bob.serverVersion); + }); + + await test("bob can read", async () => { + assert.ok(bob, "bob must be connected"); + const r = await bob.query("AuthUsers { .name, .age }"); + assert.equal(r.kind, "rows"); + if (r.kind === "rows") { + assert.equal(r.rows.length, 1); + assert.equal(r.rows[0]![0], "Zed"); + } + }); + + await test("bob's insert is rejected with permission denied (no crash)", async () => { + assert.ok(bob, "bob must be connected"); + try { + await bob.query('insert AuthUsers { name := "Mallory", age := 99 }'); + assert.fail("readonly insert should have been rejected"); + } catch (err) { + assert.ok(isPowDBError(err), `expected PowDBError, got ${err}`); + assert.equal(err.code, "query_failed"); + assert.ok( + /permission denied/i.test(err.message), + `expected 'permission denied', got: ${err.message}`, + ); + console.log(` server said: ${err.message}`); + } + // The connection must survive the rejection — reads still work. + const r = await bob.query("count(AuthUsers)"); + assert.equal(r.kind, "scalar"); + if (r.kind === "scalar") assert.equal(r.value, "1"); + }); + + await test("bob's write never landed (count still 1 via alice)", async () => { + assert.ok(alice, "alice must be connected"); + const r = await alice.query("count(AuthUsers)"); + assert.equal(r.kind, "scalar"); + if (r.kind === "scalar") assert.equal(r.value, "1"); + }); + + console.log("\nMulti-user server — rejected connects"); + + await test("connect with no user fails with auth_failed", async () => { + try { + await Client.connect({ host: HOST, port: MULTI_PORT }); + assert.fail("connect without a username should have been rejected"); + } catch (err) { + assert.ok(isPowDBError(err), `expected PowDBError, got ${err}`); + assert.equal(err.code, "auth_failed"); + console.log(` server said: ${err.message}`); + } + }); + + await test("connect with wrong password fails with auth_failed", async () => { + try { + await Client.connect({ + host: HOST, + port: MULTI_PORT, + user: "alice", + password: "wrong", + }); + assert.fail("wrong password should have been rejected"); + } catch (err) { + assert.ok(isPowDBError(err), `expected PowDBError, got ${err}`); + assert.equal(err.code, "auth_failed"); + } + }); + + await test("connect as unknown user fails with auth_failed", async () => { + try { + await Client.connect({ + host: HOST, + port: MULTI_PORT, + user: "mallory", + password: "s3cret", + }); + assert.fail("unknown user should have been rejected"); + } catch (err) { + assert.ok(isPowDBError(err), `expected PowDBError, got ${err}`); + assert.equal(err.code, "auth_failed"); + } + }); + + console.log("\nLegacy (no-users) server — back-compat"); + + await test("connect without user works against a no-auth server", async () => { + const c = await Client.connect({ host: HOST, port: LEGACY_PORT }); + const r = await c.query("type LegacyT { required name: str }"); + assert.equal(r.kind, "message"); + const ins = await c.query('insert LegacyT { name := "ok" }'); + assert.equal(ins.kind, "ok"); + await c.close(); + }); + + if (alice !== null) await (alice as Client).close(); + if (bob !== null) await (bob as Client).close(); + } finally { + await stopServer(multiServer); + await stopServer(legacyServer); + await fsp.rm(BASE_DIR, { recursive: true, force: true }); + } + + console.log("\n" + "═".repeat(50)); + console.log(`Results: ${passed} passed, ${failed} failed`); + if (failures.length > 0) { + console.log("\nFailures:"); + for (const f of failures) { + console.log(` - ${f}`); + } + } + console.log("═".repeat(50)); + + process.exit(failed > 0 ? 1 : 0); +} + +main().catch((err) => { + console.error("Test suite crashed:", err); + process.exit(1); +}); diff --git a/clients/ts/test/protocol.test.ts b/clients/ts/test/protocol.test.ts index d88a366..93787ed 100644 --- a/clients/ts/test/protocol.test.ts +++ b/clients/ts/test/protocol.test.ts @@ -119,6 +119,88 @@ async function main() { assert.throws(() => tryDecode(frame), /truncated affected count/); }); + console.log("\nConnect frame — optional username (multi-user auth)"); + + // Helper: length-prefixed string, identical to the wire encoding. + const lpString = (s: string): Buffer => { + const bytes = Buffer.from(s, "utf8"); + const out = Buffer.alloc(4 + bytes.length); + out.writeUInt32LE(bytes.length, 0); + bytes.copy(out, 4); + return out; + }; + + await test("encodes Connect with username after password (round-trip)", () => { + const buf = encode({ + type: "Connect", + dbName: "main", + password: "pw", + username: "alice", + }); + const decoded = tryDecode(buf); + assert.ok(decoded, "frame should decode"); + assert.equal(decoded.msg.type, "Connect"); + if (decoded.msg.type === "Connect") { + assert.equal(decoded.msg.dbName, "main"); + assert.equal(decoded.msg.password, "pw"); + assert.equal(decoded.msg.username, "alice"); + } + }); + + await test("encodes Connect with null username as byte-identical legacy frame", () => { + const buf = encode({ + type: "Connect", + dbName: "main", + password: "pw", + username: null, + }); + // Hand-build the pre-username (0.3.x) frame: header + dbName + password, + // with NO trailing username field. Old servers must see exactly this. + const payload = Buffer.concat([lpString("main"), lpString("pw")]); + const expected = Buffer.alloc(6 + payload.length); + expected.writeUInt8(0x01, 0); // MSG_CONNECT + expected.writeUInt8(0, 1); // flags + expected.writeUInt32LE(payload.length, 2); + payload.copy(expected, 6); + assert.deepStrictEqual(buf, expected); + }); + + await test("decodes legacy Connect frame (no username bytes) with username=null", () => { + // Frame as produced by a 0.3.x client: dbName + password only. + const payload = Buffer.concat([lpString("main"), lpString("pw")]); + const frame = Buffer.alloc(6 + payload.length); + frame.writeUInt8(0x01, 0); + frame.writeUInt8(0, 1); + frame.writeUInt32LE(payload.length, 2); + payload.copy(frame, 6); + const decoded = tryDecode(frame); + assert.ok(decoded, "frame should decode"); + assert.equal(decoded.msg.type, "Connect"); + if (decoded.msg.type === "Connect") { + assert.equal(decoded.msg.username, null); + } + }); + + await test("decodes empty (len=0) username as null, mirroring the server", () => { + // Server treats a zero-length username string as None. + const payload = Buffer.concat([ + lpString("main"), + lpString("pw"), + lpString(""), + ]); + const frame = Buffer.alloc(6 + payload.length); + frame.writeUInt8(0x01, 0); + frame.writeUInt8(0, 1); + frame.writeUInt32LE(payload.length, 2); + payload.copy(frame, 6); + const decoded = tryDecode(frame); + assert.ok(decoded, "frame should decode"); + assert.equal(decoded.msg.type, "Connect"); + if (decoded.msg.type === "Connect") { + assert.equal(decoded.msg.username, null); + } + }); + console.log("\nCancellation — abort during in-flight query"); await test("AbortSignal rejects the pending query without destroying the socket", async () => { diff --git a/clients/ts/tsconfig.json b/clients/ts/tsconfig.json index 163083c..319bc54 100644 --- a/clients/ts/tsconfig.json +++ b/clients/ts/tsconfig.json @@ -9,8 +9,8 @@ "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "declaration": true, - "declarationMap": true, - "sourceMap": true, + "declarationMap": false, + "sourceMap": false, "outDir": "dist", "rootDir": "src", "esModuleInterop": true,