Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions clients/ts/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 34 additions & 5 deletions clients/ts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -239,14 +269,13 @@ Returns a `Promise<Client>`. 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?)`

Expand Down
3 changes: 2 additions & 1 deletion clients/ts/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 12 additions & 2 deletions clients/ts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] }
Expand All @@ -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;
/**
Expand Down Expand Up @@ -133,6 +140,7 @@ export class Client extends EventEmitter<ClientEvents> {
port,
dbName = "default",
password = null,
user,
connectTimeoutMs = 5000,
tls: tlsOpt = false,
} = opts;
Expand Down Expand Up @@ -184,7 +192,9 @@ export class Client extends EventEmitter<ClientEvents> {
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") {
Expand Down
33 changes: 30 additions & 3 deletions clients/ts/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[][] }
Expand All @@ -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;
}
Expand Down Expand Up @@ -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) };
Expand Down
Loading
Loading