diff --git a/packages/client/README.md b/packages/client/README.md index d1305317f..2d6e98215 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -15,7 +15,7 @@ const transportFactory: ByteTransportFactory = async (handlers) => { }; }; -const client = new PiClient({ token: bearerToken, transportFactory }); +const client = new PiClient({ transportFactory }); await client.connect(); const session = await client.createSession({ cwd: "/workspace" }); const unsubscribe = session.subscribe((snapshot) => render(snapshot)); @@ -23,7 +23,7 @@ await session.prompt("Inspect this project"); unsubscribe(); ``` -Call `handlers.onData(chunk)` for inbound bytes, `handlers.onClose()` for an orderly terminal close, and `handlers.onError(error)` for transport failures. A factory must create a fresh transport for every connection attempt. +Call `handlers.onData(chunk)` for inbound bytes, `handlers.onClose()` for an orderly terminal close, and `handlers.onError(error)` for transport failures. A factory must create a fresh transport for every connection attempt and complete any transport-specific authentication before resolving. For example, a WebSocket factory can provide credentials in its upgrade request. `PiClient` does not reconnect automatically. Call `reconnect()` after disconnection. One connection can attach several sessions. Requests are correlated by ID. Server snapshots and successful response snapshots are authoritative, while progress events do not mutate snapshot state optimistically. Read cached session summaries from `client.snapshot?.sessions`; call `listSessions()` to request a refreshed list from the server. @@ -37,7 +37,7 @@ Calling `dispose()` or `detach()` releases only that lease. A lease rejects comm `PiClientOptions.maxFrameLength` bounds inbound and outbound CBOR payloads. Configure matching limits on the client and server. Transports should separately bound queued outbound bytes and preserve send order. -Treat peers as untrusted. Use a secure transport where required and protect the protocol bearer token. +Treat peers as untrusted. Use a secure transport with appropriate access controls and authenticate during transport establishment. Subscriber exceptions are isolated from protocol state. Set `onListenerError` in `PiClientOptions` to report them to application logging or diagnostics. @@ -50,7 +50,6 @@ import { PiClient } from "@earendil-works/pi-client"; import { createUnixTransportFactory } from "@earendil-works/pi-client/unix"; const client = new PiClient({ - token: bearerToken, transportFactory: createUnixTransportFactory({ path: "/tmp/pi.sock", }), diff --git a/packages/client/src/client.ts b/packages/client/src/client.ts index fa65229c5..9ad2afbf5 100644 --- a/packages/client/src/client.ts +++ b/packages/client/src/client.ts @@ -69,7 +69,6 @@ export class PiClient { this.#options = options; this.#state = new ClientState(options.onListenerError); this.#connection = new Connection({ - token: options.token, transportFactory: options.transportFactory, maxFrameLength: options.maxFrameLength, onHandshake: (snapshot) => this.#state.applyServerSnapshot(snapshot), diff --git a/packages/client/src/connection.ts b/packages/client/src/connection.ts index 7ad2eeaa3..cfc707869 100644 --- a/packages/client/src/connection.ts +++ b/packages/client/src/connection.ts @@ -30,7 +30,6 @@ type ConnectionLifecycle = } & ActiveConnection); interface ConnectionOptions { - token: string; transportFactory: ByteTransportFactory; maxFrameLength?: number; onHandshake(snapshot: ServerSnapshot): void; @@ -133,10 +132,7 @@ export class Connection { this.#lifecycle = { ...lifecycle, transport }; try { await transport.send( - encodeClientMessage( - { type: "hello", version: PROTOCOL_VERSION, token: this.#options.token }, - { maxFrameLength: this.#maxFrameLength }, - ), + encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION }, { maxFrameLength: this.#maxFrameLength }), ); } catch (error) { if (this.#isCurrent(id)) this.#failAndClose(toDisconnectedError(error)); diff --git a/packages/client/src/transport.ts b/packages/client/src/transport.ts index 71b7489c3..0916f9e62 100644 --- a/packages/client/src/transport.ts +++ b/packages/client/src/transport.ts @@ -14,5 +14,5 @@ export interface ByteTransportHandlers { onError(error: Error): void; } -/** Creates a fresh connected transport for each PiClient connection attempt. Exactly one terminal handler is expected. */ +/** Creates a fresh connected, authenticated transport. Exactly one terminal handler is expected. */ export type ByteTransportFactory = (handlers: ByteTransportHandlers) => ByteTransport | Promise; diff --git a/packages/client/src/types.ts b/packages/client/src/types.ts index 0a5cf6662..bd65b42db 100644 --- a/packages/client/src/types.ts +++ b/packages/client/src/types.ts @@ -12,7 +12,6 @@ export type Unsubscribe = () => void; export type ListenerErrorHandler = (error: Error) => void; export interface PiClientOptions { - token: string; transportFactory: ByteTransportFactory; maxFrameLength?: number; /** Reports subscriber failures without allowing them to corrupt client state. */ diff --git a/packages/client/test/connection.test.ts b/packages/client/test/connection.test.ts index 55cb07954..4c7dc1515 100644 --- a/packages/client/test/connection.test.ts +++ b/packages/client/test/connection.test.ts @@ -19,7 +19,7 @@ import { } from "./support.ts"; describe("PiClient", () => { - test("sends a framed version and bearer token before accepting a fragmented server hello", async () => { + test("sends a framed version before accepting a fragmented server hello", async () => { const server = new MemoryByteServer(); const received: ClientMessage[] = []; server.onMessage((message) => { @@ -39,8 +39,7 @@ describe("PiClient", () => { const client = createClient(server); await expect(client.connect()).resolves.toEqual(baseServerSnapshot); - expect(received[0]).toEqual({ type: "hello", version: PROTOCOL_VERSION, token: "bearer-secret" }); - expect(server.sentByClient[0]).toBeInstanceOf(Uint8Array); + expect(received[0]).toEqual({ type: "hello", version: PROTOCOL_VERSION }); expect(client.connectionState).toBe("connected"); }); @@ -48,7 +47,6 @@ describe("PiClient", () => { let closeCount = 0; let sendCount = 0; const client = new PiClient({ - token: "bearer-secret", transportFactory: (handlers) => { handlers.onData( encodeServerMessage({ @@ -113,7 +111,6 @@ describe("PiClient", () => { } }); const client = new PiClient({ - token: "bearer-secret", transportFactory: (handlers) => server.connect(handlers), onListenerError: (error) => listenerErrors.push(error), }); @@ -161,7 +158,6 @@ describe("PiClient", () => { }); } const client = new PiClient({ - token: "bearer-secret", transportFactory: (handlers) => (connection++ === 0 ? first : second).connect(handlers), }); let reconnect: Promise | undefined; @@ -180,20 +176,20 @@ describe("PiClient", () => { expect(first.clientCloseCount).toBe(1); }); - test("rejects a typed handshake authentication error", async () => { + test("rejects a typed handshake version error", async () => { const server = new MemoryByteServer(); server.onMessage(() => { server.send({ type: "hello_error", - error: { code: "auth", message: "Invalid token" }, + error: { code: "version", message: "Unsupported protocol version" }, }); }); - const client = createClient(server, "wrong"); + const client = createClient(server); await expect(client.connect()).rejects.toMatchObject({ name: "PiServerError", - code: "auth", - message: "Invalid token", + code: "version", + message: "Unsupported protocol version", }); expect(client.connectionState).toBe("disconnected"); expect(server.clientCloseCount).toBe(1); @@ -217,7 +213,7 @@ describe("PiClient", () => { } const transportFactory: ByteTransportFactory = (handlers) => (connection++ === 0 ? first : second).connect(handlers); - const client = new PiClient({ token: "bearer-secret", transportFactory }); + const client = new PiClient({ transportFactory }); const states: string[] = []; client.onConnectionStateChange(({ state }) => states.push(state)); await client.connect(); @@ -247,7 +243,6 @@ describe("PiClient", () => { }); } const client = new PiClient({ - token: "bearer-secret", transportFactory: (handlers) => (connection++ === 0 ? first : second).connect(handlers), }); await client.connect(); @@ -285,7 +280,6 @@ describe("PiClient", () => { } }); const client = new PiClient({ - token: "bearer-secret", maxFrameLength: 512, transportFactory: (handlers) => server.connect(handlers), }); @@ -325,7 +319,6 @@ describe("PiClient", () => { expect( () => new PiClient({ - token: "secret", maxFrameLength: 0x1_0000_0000, transportFactory: (handlers) => server.connect(handlers), }), diff --git a/packages/client/test/disposal.test.ts b/packages/client/test/disposal.test.ts index 0be7b18f5..7688734f0 100644 --- a/packages/client/test/disposal.test.ts +++ b/packages/client/test/disposal.test.ts @@ -1,3 +1,4 @@ +import { PROTOCOL_VERSION } from "@earendil-works/pi-protocol"; import { describe, expect, test } from "vitest"; import { PiClient, PiClientDisposedError } from "../src/index.ts"; import { attachSession, baseServerSnapshot, connectClient, MemoryByteServer, sessionSnapshot } from "./support.ts"; @@ -9,14 +10,13 @@ describe("PiClient disposal", () => { if (message.type !== "hello") return; server.send({ type: "hello", - version: 2, + version: PROTOCOL_VERSION, connectionId: "connection-1", snapshot: baseServerSnapshot, }); }); const client = await PiClient.connect({ - token: "secret", transportFactory: (handlers) => server.connect(handlers), }); diff --git a/packages/client/test/support.ts b/packages/client/test/support.ts index fdd3e8a29..933d803c0 100644 --- a/packages/client/test/support.ts +++ b/packages/client/test/support.ts @@ -104,15 +104,14 @@ export function sessionSnapshot(id: string, overrides: Partial }; } -export function createClient(server: MemoryByteServer, token = "bearer-secret"): PiClient { +export function createClient(server: MemoryByteServer): PiClient { return new PiClient({ - token, transportFactory: (handlers) => server.connect(handlers), }); } -export async function connectClient(server: MemoryByteServer, token = "bearer-secret"): Promise { - const client = createClient(server, token); +export async function connectClient(server: MemoryByteServer): Promise { + const client = createClient(server); server.onMessage((message) => { if (message.type === "hello") { server.send({ diff --git a/packages/client/test/unix.test.ts b/packages/client/test/unix.test.ts index 47f1425ed..98f3e7d7e 100644 --- a/packages/client/test/unix.test.ts +++ b/packages/client/test/unix.test.ts @@ -78,7 +78,6 @@ describe.runIf(process.platform !== "win32")("Unix-domain sockets", () => { }); await listen(server, socketPath); const client = new PiClient({ - token: "unix-secret", transportFactory: createUnixTransportFactory({ path: socketPath }), }); @@ -192,7 +191,6 @@ describe.runIf(process.platform !== "win32")("Unix-domain sockets", () => { }); await listen(server, socketPath); const client = new PiClient({ - token: "unix-secret", transportFactory: createUnixTransportFactory({ path: socketPath }), }); diff --git a/packages/coding-agent/test/client/support.ts b/packages/coding-agent/test/client/support.ts index ec6076587..d51b3cab7 100644 --- a/packages/coding-agent/test/client/support.ts +++ b/packages/coding-agent/test/client/support.ts @@ -74,7 +74,7 @@ export function sessionSnapshot(id: string, overrides: Partial } export async function connectClient(server: MemoryServer): Promise { - const client = new PiClient({ token: "secret", transportFactory: (handlers) => server.connect(handlers) }); + const client = new PiClient({ transportFactory: (handlers) => server.connect(handlers) }); server.onMessage((message) => { if (message.type !== "hello") return; server.send({ diff --git a/packages/protocol/README.md b/packages/protocol/README.md index 428f75203..7f216023e 100644 --- a/packages/protocol/README.md +++ b/packages/protocol/README.md @@ -2,12 +2,12 @@ Runtime-neutral schemas, types, CBOR encoding, and byte-stream framing for the experimental pi protocol. -Protocol version `2` uses binary messages with this wire layout: +Protocol version `1` uses binary messages with this wire layout: 1. A four-byte unsigned big-endian payload length. 2. One definite-length CBOR item containing the message. -The first client message is always `hello`, containing `PROTOCOL_VERSION` and a bearer token. Subsequent messages use correlated request/response envelopes and server event envelopes. Session and server snapshots are authoritative. Progress events are transient UI hints and must not be reduced into authoritative state. +The first client message is always `hello`, containing `PROTOCOL_VERSION`. Subsequent messages use correlated request/response envelopes and server event envelopes. Session and server snapshots are authoritative. Progress events are transient UI hints and must not be reduced into authoritative state. Transports complete authentication before protocol bytes are exchanged. ## Validated message API @@ -24,7 +24,6 @@ import { const hello: ClientHello = { type: "hello", version: PROTOCOL_VERSION, - token: bearerToken, }; transport.send(encodeClientMessage(hello)); @@ -46,7 +45,7 @@ Every transport carries the same complete bytes: `[uint32-be CBOR length][CBOR p This package does not bundle a transport. Consumers provide a byte-stream transport that preserves byte order and reports stream closure. Custom transports must handle arbitrary frame fragmentation and coalescing. -All transports are untrusted. Configure matching frame limits and enforce the authentication and access controls appropriate for the transport. +All transports are untrusted. Configure matching frame limits and enforce access controls appropriate for the transport before exposing a connection to the protocol. Unix sockets can use filesystem permissions, while network transports can authenticate during connection establishment. ## Encoding and framing diff --git a/packages/protocol/src/schemas.ts b/packages/protocol/src/schemas.ts index 1a36216d9..afd61e91b 100644 --- a/packages/protocol/src/schemas.ts +++ b/packages/protocol/src/schemas.ts @@ -1,6 +1,6 @@ import Type, { type Static } from "typebox"; -export const PROTOCOL_VERSION = 2 as const; +export const PROTOCOL_VERSION = 1 as const; const IdSchema = Type.String({ minLength: 1 }); const TimestampSchema = Type.Integer({ minimum: 0 }); @@ -264,7 +264,6 @@ export const ServerSnapshotSchema = StrictObject({ export type ServerSnapshot = Static; export const ProtocolErrorCodeSchema = Type.Union([ - Type.Literal("auth"), Type.Literal("version"), Type.Literal("busy"), Type.Literal("session_locked"), @@ -381,7 +380,6 @@ export type ResultForCommand = TCommand["command"] ext export const ClientHelloSchema = StrictObject({ type: Type.Literal("hello"), version: Type.Integer({ minimum: 0 }), - token: Type.String({ minLength: 1 }), }); export type ClientHello = Static; diff --git a/packages/protocol/test/protocol.test.ts b/packages/protocol/test/protocol.test.ts index 45559315a..30e328ed3 100644 --- a/packages/protocol/test/protocol.test.ts +++ b/packages/protocol/test/protocol.test.ts @@ -31,7 +31,6 @@ const emptyServerSnapshot: ServerSnapshot = { const clientHello: ClientHello = { type: "hello", version: PROTOCOL_VERSION, - token: "secret", }; const serverHello: ServerHello = { @@ -53,24 +52,27 @@ function itemMessage(item: unknown, type: "item_updated" | "item_finished" = "it } describe("protocol validation", () => { - test("uses protocol version 2", () => { - expect(PROTOCOL_VERSION).toBe(2); - expect(isSupportedProtocolVersion(2)).toBe(true); - expect(isSupportedProtocolVersion(1)).toBe(false); + test("uses protocol version 1", () => { + expect(PROTOCOL_VERSION).toBe(1); + expect(isSupportedProtocolVersion(1)).toBe(true); + expect(isSupportedProtocolVersion(2)).toBe(false); expect(isSupportedProtocolVersion(2.5)).toBe(false); }); - test.each([0, 1, PROTOCOL_VERSION])("accepts integer client hello version %s for negotiation", (version) => { - const message = { ...clientHello, version }; - expect(parseClientMessage(message)).toEqual(message); - }); + test.each([0, PROTOCOL_VERSION, PROTOCOL_VERSION + 1])( + "accepts integer client hello version %s for negotiation", + (version) => { + const message = { ...clientHello, version }; + expect(parseClientMessage(message)).toEqual(message); + }, + ); test.each([ - { type: "hello", version: "2", token: "secret" }, - { type: "hello", version: 2.5, token: "secret" }, - { type: "hello", version: 2, token: "" }, - { type: "hello", version: 2, token: "secret", extra: true }, - ])("rejects an invalid strict handshake: $version", (message) => { + ["string version", { type: "hello", version: String(PROTOCOL_VERSION) }], + ["fractional version", { type: "hello", version: PROTOCOL_VERSION + 0.5 }], + ["credential field", { type: "hello", version: PROTOCOL_VERSION, token: "secret" }], + ["unknown field", { type: "hello", version: PROTOCOL_VERSION, extra: true }], + ] as const)("rejects a handshake with %s", (_label, message) => { expect(() => parseClientMessage(message)).toThrow(ProtocolValidationError); }); @@ -99,7 +101,13 @@ describe("protocol validation", () => { }); test.each([ - { type: "hello", version: 1, connectionId: "connection-1", snapshot: emptyServerSnapshot }, + { + type: "hello", + version: PROTOCOL_VERSION + 1, + connectionId: "connection-1", + snapshot: emptyServerSnapshot, + }, + { type: "hello_error", error: { code: "auth", message: "Authentication failed" } }, { type: "response", id: "request-1", ok: true, result: { command: "unknown" } }, { type: "event", event: { type: "session_removed", sessionId: 42 } }, ])("rejects invalid server messages", (wire) => { @@ -257,7 +265,11 @@ describe("protocol validation", () => { test("validation errors do not retain rejected payloads", () => { let thrown: unknown; try { - parseClientMessage({ type: "hello", version: "2", token: "x".repeat(2_000_000) }); + parseClientMessage({ + type: "hello", + version: String(PROTOCOL_VERSION), + extra: "x".repeat(2_000_000), + }); } catch (error) { thrown = error; } @@ -284,7 +296,9 @@ describe("validated framed protocol APIs", () => { }); test("validates messages before encoding", () => { - expect(() => encodeClientMessage({ type: "hello", version: 2, token: "" })).toThrow(ProtocolValidationError); + expect(() => encodeClientMessage({ type: "hello", version: PROTOCOL_VERSION + 0.5 })).toThrow( + ProtocolValidationError, + ); }); test("omits explicit undefined optional properties on the wire", () => { @@ -324,7 +338,7 @@ describe("validated framed protocol APIs", () => { test("incrementally decodes server messages", () => { const errorMessage: ServerMessage = { type: "hello_error", - error: { code: "auth", message: "Invalid token" }, + error: { code: "version", message: "Unsupported protocol version" }, }; const decoder = new ServerMessageDecoder(); expect(decoder.push(encodeServerMessage(errorMessage))).toEqual([errorMessage]); @@ -334,7 +348,7 @@ describe("validated framed protocol APIs", () => { test.each([ ["empty CBOR payload", encodeFrame(new Uint8Array())], ["malformed CBOR", encodeFrame(new Uint8Array([0xff]))], - ["schema-invalid CBOR", encodeFrame(encodeCbor({ type: "hello", version: 2, token: "", extra: true }))], + ["schema-invalid CBOR", encodeFrame(encodeCbor({ type: "hello", version: PROTOCOL_VERSION, extra: true }))], ] as const)("rejects invalid framed client input: %s", (_label, wire) => { const decoder = new ClientMessageDecoder(); expect(() => decoder.push(wire)).toThrow(ProtocolValidationError); diff --git a/packages/server/README.md b/packages/server/README.md index 5d4799eae..3fa91e395 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -34,13 +34,12 @@ const backend: PiSessionBackend = { }; const server = createUnixServer(backend, { - token: process.env.PI_SERVER_TOKEN!, path: "/tmp/pi/server.sock", }); await server.start(); ``` -`PiServer` composes transport listeners through the `PiServerListener` interface. The Unix submodule exports the `createUnixListener()` building block and `createUnixServer()` preset, keeping the common case concise without coupling the primary server to Unix sockets. The listener uses authenticated, length-prefixed CBOR messages from `@earendil-works/pi-protocol`. It does not yet replace the legacy JSONL IPC control plane, child-process supervisor, standalone `server` CLI, or Radius presence integration. +`PiServer` composes transport listeners through the `PiServerListener` interface. Each listener must complete any transport-specific authentication and authorization before passing a connection to `PiServer`. For example, a WebSocket listener can validate credentials during the HTTP upgrade, while the Unix listener relies on socket filesystem permissions. The Unix submodule exports the `createUnixListener()` building block and `createUnixServer()` preset, keeping the common case concise without coupling the primary server to Unix sockets. The listener uses length-prefixed CBOR messages from `@earendil-works/pi-protocol`. It does not yet replace the legacy JSONL IPC control plane, child-process supervisor, standalone `server` CLI, or Radius presence integration. ## Transport testing diff --git a/packages/server/src/connection.ts b/packages/server/src/connection.ts index 5e9116184..a7bb0769a 100644 --- a/packages/server/src/connection.ts +++ b/packages/server/src/connection.ts @@ -2,7 +2,7 @@ import type { ClientMessageDecoder } from "@earendil-works/pi-protocol"; import type { MaybePromise } from "./types.ts"; -/** A connected, ordered byte sink used by PiServer's transport-neutral domain core. */ +/** An established, authorized ordered byte connection. */ export interface ByteConnection { readonly closed: boolean; send(chunk: Uint8Array): Promise; diff --git a/packages/server/src/listener.ts b/packages/server/src/listener.ts index 026e10734..6c69dcb05 100644 --- a/packages/server/src/listener.ts +++ b/packages/server/src/listener.ts @@ -1,9 +1,10 @@ import type { ByteConnectionAcceptor } from "./connection.ts"; -/** A transport listener that supplies ordered byte connections to PiServer. */ +/** Supplies established byte connections after any required transport authentication. */ export interface PiServerListener { /** Human-readable bound address after startup, when the transport has one. */ readonly address?: string; + /** Starts listening and passes authorized connections to accept. */ start(accept: ByteConnectionAcceptor): Promise; close(): Promise; } diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index 20c3fcdc5..95e7c859b 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,4 +1,4 @@ -import { createHash, randomUUID, timingSafeEqual } from "node:crypto"; +import { randomUUID } from "node:crypto"; import { type ClientHello, type ClientMessage, @@ -31,15 +31,10 @@ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 5_000; const MAX_UINT32 = 0xffff_ffff; const MAX_TIMER_DELAY_MS = 2_147_483_647; -function tokenDigest(token: string): Buffer { - return createHash("sha256").update(token, "utf8").digest(); -} - export class PiServer { readonly id: string; private readonly listeners: readonly PiServerListener[]; - private readonly expectedTokenDigest: Buffer; private readonly maxFrameLength: number; private readonly handshakeTimeoutMs: number; private readonly onError: ((error: Error) => void) | undefined; @@ -55,7 +50,6 @@ export class PiServer { const resolved = resolveOptions(options); this.listeners = options.listeners; this.id = options.serverId ?? randomUUID(); - this.expectedTokenDigest = tokenDigest(options.token); this.maxFrameLength = resolved.maxFrameLength; this.handshakeTimeoutMs = resolved.handshakeTimeoutMs; this.onError = options.onError; @@ -220,10 +214,6 @@ export class PiServer { } private async finishHandshake(state: ConnectionState, hello: ClientHello): Promise { - if (!this.authenticate(hello)) { - await this.failProtocol(state, { code: "auth", message: "Authentication failed" }); - return; - } if (!isSupportedProtocolVersion(hello.version)) { await this.failProtocol(state, { code: "version", @@ -254,10 +244,6 @@ export class PiServer { } } - private authenticate(hello: ClientHello): boolean { - return timingSafeEqual(tokenDigest(hello.token), this.expectedTokenDigest); - } - private async handleRequest(state: ConnectionState, envelope: RequestEnvelope): Promise { try { const result = await this.sessions.executeCommand(state, envelope.request); @@ -381,7 +367,6 @@ export class PiServer { function resolveOptions(options: PiServerOptions): { maxFrameLength: number; handshakeTimeoutMs: number } { if (!Array.isArray(options.listeners)) throw new TypeError("PiServer listeners must be an array"); - if (!options.token) throw new TypeError("PiServer token must not be empty"); if (options.serverId === "") throw new TypeError("PiServer serverId must not be empty"); const maxFrameLength = options.maxFrameLength ?? DEFAULT_MAX_FRAME_LENGTH; if (!Number.isSafeInteger(maxFrameLength) || maxFrameLength <= 0 || maxFrameLength > MAX_UINT32) { diff --git a/packages/server/src/testing/backend.ts b/packages/server/src/testing/backend.ts index 115c5fabd..2b8cb8341 100644 --- a/packages/server/src/testing/backend.ts +++ b/packages/server/src/testing/backend.ts @@ -16,7 +16,6 @@ import type { PromptInput, } from "../types.ts"; -export const TEST_TOKEN = "server-conformance-token"; export const TEST_MODEL: ModelMetadata = { provider: "test", id: "small", diff --git a/packages/server/src/testing/client.ts b/packages/server/src/testing/client.ts index ba2b6dc43..788f75254 100644 --- a/packages/server/src/testing/client.ts +++ b/packages/server/src/testing/client.ts @@ -9,7 +9,7 @@ import { type ServerMessage, ServerMessageDecoder, } from "@earendil-works/pi-protocol"; -import { Deferred, TEST_TOKEN } from "./backend.ts"; +import { Deferred } from "./backend.ts"; interface MessageWaiter { predicate: (message: ServerMessage) => boolean; @@ -40,9 +40,9 @@ export class ProtocolTestClient { return this.closedValue; } - hello(token = TEST_TOKEN, version: number = PROTOCOL_VERSION): Promise { + hello(version: number = PROTOCOL_VERSION): Promise { const response = this.next((message) => message.type === "hello" || message.type === "hello_error"); - void this.sendMessage({ type: "hello", token, version }); + void this.sendMessage({ type: "hello", version }); return response; } diff --git a/packages/server/src/testing/index.ts b/packages/server/src/testing/index.ts index ae70fc1b6..4f32c6f79 100644 --- a/packages/server/src/testing/index.ts +++ b/packages/server/src/testing/index.ts @@ -1,4 +1,4 @@ -export { Deferred, TEST_MODEL, TEST_TOKEN, TestSessionBackend, TestSessionRuntime } from "./backend.ts"; +export { Deferred, TEST_MODEL, TestSessionBackend, TestSessionRuntime } from "./backend.ts"; export type { WireChannel } from "./client.ts"; export { connectUnixTestClient, ProtocolTestClient } from "./client.ts"; export type { TestServer, TestServerOptions } from "./server.ts"; diff --git a/packages/server/src/testing/server.ts b/packages/server/src/testing/server.ts index e9f275940..b101953d2 100644 --- a/packages/server/src/testing/server.ts +++ b/packages/server/src/testing/server.ts @@ -1,9 +1,8 @@ import { PiServer } from "../server.ts"; import type { PiServerOptions, PiSessionBackend } from "../types.ts"; -import { TEST_TOKEN, TestSessionBackend } from "./backend.ts"; +import { TestSessionBackend } from "./backend.ts"; -export interface TestServerOptions extends Omit { - token?: string; +export interface TestServerOptions extends PiServerOptions { backend?: PiSessionBackend; } @@ -17,7 +16,6 @@ export function createTestServer(options: TestServerOptions): TestServer { const backend = options.backend ?? new TestSessionBackend(); return { server: new PiServer(backend, { - token: options.token ?? TEST_TOKEN, listeners: options.listeners, maxFrameLength: options.maxFrameLength, handshakeTimeoutMs: options.handshakeTimeoutMs, diff --git a/packages/server/src/transports/unix/preset.ts b/packages/server/src/transports/unix/preset.ts index b94c68a6a..c1d24c8b6 100644 --- a/packages/server/src/transports/unix/preset.ts +++ b/packages/server/src/transports/unix/preset.ts @@ -14,7 +14,6 @@ export function createUnixServer(backend: PiSessionBackend, options: UnixServerO onError: options.onError, }); return new PiServer(backend, { - token: options.token, listeners: [listener], maxFrameLength: options.maxFrameLength, handshakeTimeoutMs: options.handshakeTimeoutMs, diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 1cda2bb80..7e1911e4a 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -12,7 +12,6 @@ import type { PiServerError } from "./errors.ts"; import type { PiServerListener } from "./listener.ts"; export interface PiServerOptions { - token: string; listeners: readonly PiServerListener[]; maxFrameLength?: number; handshakeTimeoutMs?: number; diff --git a/packages/server/test/conformance.test.ts b/packages/server/test/conformance.test.ts index 2959a1c69..5ebba2192 100644 --- a/packages/server/test/conformance.test.ts +++ b/packages/server/test/conformance.test.ts @@ -4,13 +4,7 @@ import { join } from "node:path"; import { encodeClientMessage, encodeFrame, PROTOCOL_VERSION } from "@earendil-works/pi-protocol"; import { afterEach, describe, expect, test } from "vitest"; import { type PiServer, PiServerError } from "../src/index.ts"; -import { - connectUnixTestClient, - Deferred, - type ProtocolTestClient, - TEST_TOKEN, - TestSessionBackend, -} from "../src/testing/index.ts"; +import { connectUnixTestClient, Deferred, type ProtocolTestClient, TestSessionBackend } from "../src/testing/index.ts"; import { createUnixServer, type UnixServerOptions } from "../src/transports/unix/index.ts"; const servers = new Set(); @@ -24,7 +18,6 @@ async function startServer( const directory = await mkdtemp(join(tmpdir(), "pis-")); tempDirectories.add(directory); const server = createUnixServer(backend, { - token: TEST_TOKEN, path: join(directory, "server.sock"), ...overrides, }); @@ -53,22 +46,15 @@ describe("Unix transport conformance", () => { const { server } = await startServer(); const client = await connect(server); const response = client.next((message) => message.type === "hello"); - await client.sendFragmentedMessage({ type: "hello", version: PROTOCOL_VERSION, token: TEST_TOKEN }, 2); + await client.sendFragmentedMessage({ type: "hello", version: PROTOCOL_VERSION }, 2); expect(await response).toMatchObject({ type: "hello", version: PROTOCOL_VERSION }); }); - test("enforces authentication, version, and exactly one first-message hello", async () => { + test("enforces version and exactly one first-message hello", async () => { const { server } = await startServer(); - const badToken = await connect(server); - expect(await badToken.hello("wrong-token")).toMatchObject({ - type: "hello_error", - error: { code: "auth" }, - }); - await badToken.waitForClose(); - const badVersion = await connect(server); - expect(await badVersion.hello(TEST_TOKEN, PROTOCOL_VERSION + 1)).toMatchObject({ + expect(await badVersion.hello(PROTOCOL_VERSION + 1)).toMatchObject({ type: "hello_error", error: { code: "version" }, }); @@ -83,7 +69,7 @@ describe("Unix transport conformance", () => { const duplicate = await connect(server); expect(await duplicate.hello()).toMatchObject({ type: "hello" }); const duplicateError = duplicate.next((message) => message.type === "hello_error"); - await duplicate.sendMessage({ type: "hello", version: PROTOCOL_VERSION, token: TEST_TOKEN }); + await duplicate.sendMessage({ type: "hello", version: PROTOCOL_VERSION }); expect(await duplicateError).toMatchObject({ type: "hello_error", error: { code: "invalid_request" } }); await duplicate.waitForClose(); }); @@ -102,7 +88,7 @@ describe("Unix transport conformance", () => { const delay = backend.delayNextList(); const { server } = await startServer(backend, { handshakeTimeoutMs: 20 }); const client = await connect(server); - await client.sendMessage({ type: "hello", version: PROTOCOL_VERSION, token: TEST_TOKEN }); + await client.sendMessage({ type: "hello", version: PROTOCOL_VERSION }); await delay.entered.promise; await client.waitForClose(); delay.release.resolve(undefined); @@ -132,7 +118,7 @@ describe("Unix transport conformance", () => { const outboundServer = await startServer(new TestSessionBackend(), { maxFrameLength: 128 }); const outbound = await connect(outboundServer.server); - await outbound.sendMessage({ type: "hello", version: PROTOCOL_VERSION, token: TEST_TOKEN }); + await outbound.sendMessage({ type: "hello", version: PROTOCOL_VERSION }); await outbound.waitForClose(); expect(outbound.messages).toEqual([]); }); diff --git a/packages/server/test/listener.test.ts b/packages/server/test/listener.test.ts index f1262ad26..7f15d9bdd 100644 --- a/packages/server/test/listener.test.ts +++ b/packages/server/test/listener.test.ts @@ -31,7 +31,7 @@ describe("PiServer listener composition", () => { test("starts and closes every configured listener", async () => { const first = new TestListener("first"); const second = new TestListener("second"); - const { server } = createTestServer({ token: "secret", listeners: [first, second] }); + const { server } = createTestServer({ listeners: [first, second] }); await server.start(); expect(server.addresses).toEqual(["first", "second"]); @@ -48,7 +48,7 @@ describe("PiServer listener composition", () => { const first = new TestListener("first"); const failure = new Error("listener failed"); const second = new TestListener("second", failure); - const { server } = createTestServer({ token: "secret", listeners: [first, second] }); + const { server } = createTestServer({ listeners: [first, second] }); await expect(server.start()).rejects.toBe(failure); expect(first.closeCount).toBe(1); diff --git a/packages/server/test/server.test.ts b/packages/server/test/server.test.ts index 81e39cf2f..69304c6ad 100644 --- a/packages/server/test/server.test.ts +++ b/packages/server/test/server.test.ts @@ -8,8 +8,6 @@ import { PiServer } from "../src/index.ts"; import { TestSessionBackend } from "../src/testing/index.ts"; import { createUnixServer } from "../src/transports/unix/index.ts"; -const TOKEN = "transport-smoke-token"; - const backend = new TestSessionBackend(); let server: PiServer | undefined; @@ -28,25 +26,25 @@ afterEach(async () => { }); test("requires explicit listeners", () => { - expect(() => Reflect.construct(PiServer, [backend, { token: TOKEN }])).toThrow(/listeners/); + expect(() => Reflect.construct(PiServer, [backend, {}])).toThrow(/listeners/); }); test("rejects Unix socket paths that cannot fit in sockaddr_un", () => { - expect(() => createUnixServer(backend, { token: TOKEN, path: `/tmp/${"x".repeat(512)}` })).toThrow(/too long/); + expect(() => createUnixServer(backend, { path: `/tmp/${"x".repeat(512)}` })).toThrow(/too long/); }); test("rejects an overlong derived private Unix bind path", async () => { const maxLength = process.platform === "linux" ? 107 : 103; const suffixLength = Buffer.byteLength("/tmp//s"); const path = `/tmp/${"x".repeat(maxLength - suffixLength)}/s`; - server = createUnixServer(backend, { token: TOKEN, path }); + server = createUnixServer(backend, { path }); await expect(server.start()).rejects.toThrow(/private Unix bind path.*too long/); }); test("rejects concurrent start calls without leaking the Unix listener", async () => { const path = await makeSocketPath(); - server = createUnixServer(backend, { token: TOKEN, path }); + server = createUnixServer(backend, { path }); const starting = server.start(); await expect(server.start()).rejects.toThrow(/starting/); await starting; @@ -71,7 +69,6 @@ test("handshake timeout cleanup does not wait for a blocked output queue", async } const core = new PiServer(backend, { listeners: [], - token: TOKEN, maxFrameLength: 1024, handshakeTimeoutMs: 10, }); @@ -87,17 +84,17 @@ test("handshake timeout cleanup does not wait for a blocked output queue", async test("rejects timeout values above Node's maximum timer delay", () => { const unix = { path: "/tmp/pi-server-timeout-test.sock" }; - expect(() => - createUnixServer(backend, { token: TOKEN, path: unix.path, handshakeTimeoutMs: 2_147_483_648 }), - ).toThrow(/handshakeTimeoutMs/); - expect(() => - createUnixServer(backend, { token: TOKEN, path: unix.path, gracefulCloseTimeoutMs: 2_147_483_648 }), - ).toThrow(/gracefulCloseTimeoutMs/); + expect(() => createUnixServer(backend, { path: unix.path, handshakeTimeoutMs: 2_147_483_648 })).toThrow( + /handshakeTimeoutMs/, + ); + expect(() => createUnixServer(backend, { path: unix.path, gracefulCloseTimeoutMs: 2_147_483_648 })).toThrow( + /gracefulCloseTimeoutMs/, + ); }); test("rejects pending-byte limits smaller than one maximum frame", async () => { const path = await makeSocketPath(); - expect(() => createUnixServer(backend, { token: TOKEN, path, maxFrameLength: 128, maxPendingBytes: 131 })).toThrow( + expect(() => createUnixServer(backend, { path, maxFrameLength: 128, maxPendingBytes: 131 })).toThrow( /maxPendingBytes/, ); }); diff --git a/packages/server/test/sessions.test.ts b/packages/server/test/sessions.test.ts index b8dd5f6f3..1f8acfb3b 100644 --- a/packages/server/test/sessions.test.ts +++ b/packages/server/test/sessions.test.ts @@ -9,12 +9,10 @@ import { Deferred, type ProtocolTestClient, TEST_MODEL, - TEST_TOKEN, TestSessionBackend, } from "../src/testing/index.ts"; import { createUnixServer, type UnixServerOptions } from "../src/transports/unix/index.ts"; -const TOKEN = TEST_TOKEN; const MODEL = TEST_MODEL; type Client = ProtocolTestClient; class MemoryBackend extends TestSessionBackend {} @@ -49,7 +47,6 @@ async function startServer(backend = new MemoryBackend(), options: Partial(); @@ -25,7 +20,7 @@ async function makeSocketPath(nested = false): Promise { } function makeServer(path: string): PiServer { - const server = createUnixServer(new TestSessionBackend(), { token: TEST_TOKEN, path }); + const server = createUnixServer(new TestSessionBackend(), { path }); servers.add(server); return server; }