Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,17 @@
# @suaveplan/third-party-elevenlabs - Typed thin client for the ElevenLabs platform API: REST + WebSocket transport, retry, rate-limit-header parsing, OTel instrumentation, Zod-validated DTOs. Domain adapters (voice, conversational AI, music, dubbing) compose this client.
<!-- begin:auto-docs -->
## Documentation tree
- [client](./src/client/client.md) — REST + WebSocket transport for the ElevenLabs platform. `ElevenLabsClient` exposes typed `fetchJson` / `fetchBinary` /…
- [client](./src/client/client.md) — REST + WebSocket transport for the ElevenLabs platform. `ElevenLabsClient` exposes typed `fetchJson` / `fetchBinary` /…
- [headers](./src/client/headers.md) — HTTP header helpers for the ElevenLabs transport. Pure functions for parsing the `Retry-After` header, extracting per-r…
- [internals](./src/client/internals.md) — Pure helper functions used by `ElevenLabsClient`. Everything here is side-effect-free, so the helpers are unit-testable…
- [socket](./src/client/socket.md) — Wrapper around any `WebSocketLike` (the browser global, `ws` from Node, or a fake from tests). Exposes a typed frame-le…
- [url](./src/client/url.md) — Tiny URL composition helper for the ElevenLabs transport. One function: `buildUrl(baseUrl, path, query?)`.
- [errors](./src/errors/errors.md) — Stable error class hierarchy and code registry for `@suaveplan/third-party-elevenlabs`. Every non-2xx HTTP response and…
- [errors](./src/errors/errors.md) — Stable error class hierarchy and code registry for `@suaveplan/third-party-elevenlabs`. Every non-2xx HTTP response and…
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
<!-- end:auto-docs -->

View File

@@ -0,0 +1,109 @@
# `client`
REST + WebSocket transport for the ElevenLabs platform. `ElevenLabsClient`
exposes typed `fetchJson` / `fetchBinary` / `fetchStream` /
`fetchMultipartJson` / `fetchMultipartBinary` / `openSocket` methods, plus
internal retry-with-exponential-backoff that honours the `Retry-After`
header. Every adapter in the package (`tts`, `stt`, `voices`, …) composes
this single client so authentication, timeouts, retries, and event
emissions live in exactly one place.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
ElevenLabs offers a sprawling REST + WebSocket API surface. Without a
shared transport, every adapter would duplicate the same error mapping,
header injection, retry policy, and timeout handling. `ElevenLabsClient`
solves that: it's the only piece of code in the package that calls
`fetch`, and the only piece that knows how to map HTTP status codes onto
typed errors. Adapters become thin domain remappers.
## Features
- Five typed request shapes — JSON in/out, JSON in / binary out, JSON in /
streaming out, multipart in / JSON out, multipart in / binary out.
- WebSocket support via `openSocket()` for ElevenLabs's streaming-input
TTS endpoints.
- Built-in retry: exponential backoff (`initialDelay * 2^attempt`) with
`Retry-After` honouring on 429 / 503 / 504 / network errors.
- Per-request `AbortSignal` plus a per-call `timeoutMs` race.
- Optional `LoggerLike` and `EventBusLike` for observability — emits
`request-start`, `request-end`, `retry-attempt`, `rate-limit-hit`.
- Caller-supplied `fetchImpl` and `webSocketImpl` for tests / edge runtimes.
## Usage: Basic Example
```ts
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
const client = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_KEY! });
const res = await client.fetchJson<{ voice_id: string }>({
method: "GET",
path: "/v1/voices/abc",
feature: "voices",
operation: "get",
});
console.log(res.body.voice_id, res.metadata.requestId);
```
## Usage: Advanced Example
```ts
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
const client = new ElevenLabsClient({
apiKey: process.env.ELEVENLABS_KEY!,
baseUrl: "https://eu.api.elevenlabs.io",
timeoutMs: 60_000,
maxRetries: 5,
initialRetryDelayMs: 250,
fetchImpl: customFetch,
events: myBus,
logger: myLogger,
});
const ctrl = new AbortController();
const stream = await client.fetchStream({
method: "POST",
path: "/v1/text-to-speech/voice-id/stream",
feature: "tts",
operation: "synthesize_stream",
body: { text: "hello" },
accept: "audio/mpeg",
signal: ctrl.signal,
});
for await (const chunk of stream.body) {
/* … */
}
```
## API Reference
- `new ElevenLabsClient(options: ElevenLabsClientOptions)` — constructor.
- `fetchJson<TBody>(req: JsonRequest): Promise<JsonResponse<TBody>>`
- `fetchBinary(req: BinaryRequest): Promise<BinaryResponse>`
- `fetchStream(req: StreamRequest): Promise<StreamResponse>`
- `fetchMultipartJson<TBody>(req: MultipartRequest): Promise<JsonResponse<TBody>>`
- `fetchMultipartBinary(req: MultipartRequest): Promise<BinaryResponse>`
- `openSocket(req: SocketRequest): ElevenLabsSocket` — wss connection with
`xi-api-key` injected as a query parameter.
Throws typed `ElevenLabsApiError` subclasses on non-2xx responses.
## Implementation Notes
- `xi-api-key` is sent as a request header for HTTP, but as a query
parameter for WebSocket (browsers can't set headers on the WS upgrade).
- Multipart values may be `Uint8Array | Blob | ReadableStream | URL`;
streams are buffered and URLs are fetched before upload — ElevenLabs
multipart endpoints require complete bodies.
- Retries fire only for `RATE_LIMIT.EXCEEDED`, `SERVER.TRANSIENT`,
`NETWORK.TIMEOUT`, and `NETWORK.UNREACHABLE` codes. Auth and validation
failures are not retried.
- The class delegates error mapping, multipart materialisation, retry
decision, and backoff math to `internals.ts` so the class itself stays
focused on transport orchestration.

View File

@@ -0,0 +1,89 @@
# `headers`
HTTP header helpers for the ElevenLabs transport. Pure functions for
parsing the `Retry-After` header, extracting per-response metadata, and
building the canonical outbound header set.
## Purpose
ElevenLabs's REST API uses a small but specific set of conventions:
authentication via `xi-api-key`, request correlation via `x-request-id`,
rate-limit observability via `x-ratelimit-remaining`, and billing
observability via `character-cost`. The functions here normalise those
conventions into typed values that the rest of the client can safely
work with — so no other module has to remember header names or worry
about case sensitivity, missing headers, or malformed `Retry-After`
values.
## Features
- `parseRetryAfter()` — accepts the two RFC 7231 forms (delta-seconds or
HTTP-date) and returns milliseconds, with `undefined` on malformed
input rather than throwing.
- `extractMetadata()` — pulls every observability header into a typed
`RequestMetadata` shape, returning a partial object with only the
fields that were actually present.
- `buildHeaders()` — composes API key + user-agent + caller-supplied
defaults + per-request overrides + content-type into the final outbound
`Record<string, string>`.
## Usage: Basic Example
```ts
import { buildHeaders, extractMetadata, parseRetryAfter } from "./headers.js";
const headers = buildHeaders(
"secret-key",
"my-app/1.0",
undefined,
undefined,
"application/json",
"application/json"
);
const ms = parseRetryAfter("5", () => Date.now()); // 5000
const meta = extractMetadata(response.headers);
```
## Usage: Advanced Example
```ts
import { buildHeaders, parseRetryAfter } from "./headers.js";
// Tenant-tagged outbound headers with a per-request override.
const headers = buildHeaders(
apiKey,
"tenant-A/2.3",
{ "x-tenant": "acme", "x-feature-flag": "stream" },
{ "x-trace-id": traceId },
undefined,
"audio/mpeg"
);
// Honour an HTTP-date Retry-After against an injectable clock.
const fakeClock = () => 1_700_000_000_000;
const wait = parseRetryAfter("Wed, 14 Nov 2023 22:13:25 GMT", fakeClock);
```
## API Reference
- `parseRetryAfter(header: string | null | undefined, clock: Clock): number | undefined`
— milliseconds until retry, or `undefined` if the header is missing /
unparseable.
- `extractMetadata(headers: Headers): RequestMetadata` — populates
`requestId`, `rateLimitRemaining`, and `characterCost` only when the
underlying header is present.
- `buildHeaders(apiKey, userAgent, defaults?, perRequest?, contentType?, accept): Record<string, string>`
— final header bag. Per-request entries override defaults.
## Implementation Notes
- `parseRetryAfter` strictly rejects fractional or negative numerics —
V8's `Date.parse` is permissive enough that `"1.5"` becomes a real
timestamp; that footgun lives here, not in the caller.
- `extractMetadata` checks both `x-request-id` and `request-id` because
ElevenLabs has used both spellings historically.
- `buildHeaders` is order-sensitive: caller-supplied defaults are layered
on top of the canonical headers, then per-request overrides win on top
of defaults — but `xi-api-key` and `accept` always reflect the
caller-provided values because they're set last in the canonical set.

View File

@@ -0,0 +1,96 @@
# `internals`
Pure helper functions used by `ElevenLabsClient`. Everything here is
side-effect-free, so the helpers are unit-testable without spinning up
the full client machinery, and the `ElevenLabsClient` class can stay
focused on transport orchestration.
## Purpose
The transport layer has many concerns that are independent of any
particular request: mapping HTTP status codes to typed errors, deciding
whether an error is retryable, computing the next backoff delay,
materialising multipart bodies, and parsing response bodies safely. Each
of these is a pure function of its inputs. Exposing them here keeps
`client.ts` short, lets each helper be tested in isolation, and makes the
HTTP-status-to-error mapping easy to audit at a glance.
## Features
- `mapHttpStatus()` — total mapping over `[400, 600)` to typed
`ElevenLabsApiError` subclasses, preserving body on 422 and
`retryAfterMs` on 429.
- `mapNetworkError()` — translates DOMException AbortError / generic
thrown errors into typed network errors.
- `shouldRetry()` — checks code-and-budget for retryability.
- `backoffDelay()` — exponential backoff that honours `Retry-After` when
present.
- `readJsonBody()` — safe response-body parser that returns `undefined`
for empty bodies and a `string` when JSON parsing fails.
- `buildMultipartBody()` — turns typed `MultipartField[]` into `FormData`.
- `materializeMultipart()` — drains `ReadableStream` and fetches URL
values into in-memory `Uint8Array` before upload.
- `endpointFor()` / `buildCtx()` — context-builder helpers used in error
payloads.
## Usage: Basic Example
```ts
import { mapHttpStatus, shouldRetry } from "./internals.js";
const err = mapHttpStatus(429, "", "GET /v1/x", "rid", 5000, undefined);
if (shouldRetry(err, 0, 3)) {
/* retry */
}
```
## Usage: Advanced Example
```ts
import {
backoffDelay,
buildMultipartBody,
materializeMultipart,
readJsonBody,
} from "./internals.js";
// 503 retry with no Retry-After → exponential.
const wait = backoffDelay(2, 250, undefined); // 1000
// Multipart upload from a streamed file.
const fields = await materializeMultipart([
{ kind: "file", name: "audio", filename: "a.wav", value: incomingStream },
{ kind: "scalar", name: "model_id", value: "scribe_v1" },
]);
const form = buildMultipartBody(fields);
// Tolerant JSON parse.
const body = await readJsonBody(response);
```
## API Reference
- `mapHttpStatus(status, statusText, endpoint, requestId?, retryAfterMs?, body?): ElevenLabsApiError`
- `mapNetworkError(err: unknown, endpoint: string): ElevenLabsApiError`
- `shouldRetry(err, attempt, max): boolean`
- `backoffDelay(attempt, initialMs, retryAfterMs?): number`
- `readJsonBody(response: Response): Promise<unknown>`
- `buildMultipartBody(fields: MultipartField[]): FormData`
- `materializeMultipart(fields): Promise<MultipartField[]>`
- `endpointFor(env: RequestEnvelope): string`
- `buildCtx(endpoint, status, requestId?): { endpoint, statusCode, requestId? }`
## Implementation Notes
- The HTTP-status mapping is **total** over `[400, 600)`: every code maps
to exactly one `ElevenLabsApiError` subclass. 5xx codes 502 / 503 /
504 map to `ServerTransientError` (retryable); other 5xx map to
`ServerUnavailableError` (terminal).
- 429 uses a heuristic on `statusText` to distinguish quota exhaustion
from transient throttling — quota-exhausted is non-retryable.
- `materializeMultipart` is the only place in the package that calls the
global `fetch` directly (to resolve URL-typed multipart values); every
other call goes through `client.#fetchOnce` which adds auth, timeouts,
and retries.
- `readJsonBody` never throws: empty body → `undefined`, JSON-parse
failure → raw string. Callers don't need a try/catch.

View File

@@ -0,0 +1,108 @@
# `socket`
Wrapper around any `WebSocketLike` (the browser global, `ws` from Node,
or a fake from tests). Exposes a typed frame-level API and a
`Promise<void>` for the open handshake.
## Purpose
Raw WebSocket APIs are awkward — open / message / close / error are
delivered as untyped events, binary frames arrive as `Blob` /
`ArrayBuffer` / typed-array views depending on platform, and there's no
built-in way to await the open handshake. `ElevenLabsSocket` papers over
all of that so adapters that need WebSocket transport (e.g. the
streaming-input TTS endpoint) can `await sock.waitOpen()` and then send
typed `SocketFrame` values without touching the raw event surface.
## Features
- `waitOpen()` — promise that resolves on `open` and rejects on `error`
or caller-signal abort.
- `send(frame: SocketFrame)` — typed text or binary send; binary frames
are copied into a fresh `ArrayBuffer` so the underlying WS impl never
sees a SharedArrayBuffer-backed view by accident.
- `onMessage` / `onClose` / `onError` — listeners returning unsubscribe
functions.
- Accepts `Uint8Array`, `ArrayBuffer`, or any `ArrayBufferView` on the
wire and normalises them to `Uint8Array` before dispatching.
- Sends on a closed socket throw `WebSocketProtocolError` instead of
silently dropping the frame.
- Pluggable `WebSocketConstructor` so tests inject a fake.
## Usage: Basic Example
```ts
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
const client = new ElevenLabsClient({ apiKey: process.env.KEY! });
const sock = client.openSocket({
path: "/v1/text-to-speech/voice-id/stream-input",
feature: "tts",
operation: "stream_input",
query: { model_id: "eleven_flash_v2" },
});
await sock.waitOpen();
sock.onMessage((f) => process.stdout.write(f.data));
sock.send({ kind: "text", data: JSON.stringify({ text: "hello" }) });
```
## Usage: Advanced Example
```ts
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
const ctrl = new AbortController();
const client = new ElevenLabsClient({
apiKey: KEY,
webSocketImpl: WS, // 'ws' on Node
});
const sock = client.openSocket({
path: "/v1/text-to-speech/abc/stream-input",
feature: "tts",
operation: "stream_input",
});
const unsubMsg = sock.onMessage((f) => sink.write(f.data));
const unsubClose = sock.onClose((e) =>
log.info("closed", { code: e.code, clean: e.wasClean })
);
sock.onError((err) => log.warn("ws error", { err }));
try {
await sock.waitOpen();
for (const chunk of corpus) {
sock.send({ kind: "text", data: JSON.stringify({ text: chunk }) });
}
} finally {
unsubMsg();
unsubClose();
sock.close(1000, "done");
}
ctrl.abort();
```
## API Reference
- `new ElevenLabsSocket(ws: WebSocketLike, endpoint: string, signal?: AbortSignal)`
- `waitOpen(): Promise<void>`
- `readyState: number` — 0 connecting / 1 open / 2 closing / 3 closed.
- `send(frame: SocketFrame): void` — `{ kind: "text" | "binary", data }`
- `close(code?: number, reason?: string): void`
- `onMessage(listener): () => void`
- `onClose(listener): () => void`
- `onError(listener): () => void`
## Implementation Notes
- The `xi-api-key` is sent as a query parameter (browsers can't set
headers on the WS upgrade) — that's done in `ElevenLabsClient.openSocket`,
not here.
- Binary `send` always copies into a fresh `ArrayBuffer` to defend
against SharedArrayBuffer leakage; the cost is one heap allocation per
outbound binary frame.
- The internal `#wireOpenPromise` removes its `error` listener once the
promise settles, so post-open transport errors flow only into the
user-installed `onError` listeners — they don't reject `waitOpen()`
twice.
- `bytesFrom` rejects unsupported binary payload types with
`WebSocketProtocolError`. The error is dispatched to `onError` rather
than thrown synchronously, since it originates inside an event handler
callback.

View File

@@ -0,0 +1,74 @@
# `url`
Tiny URL composition helper for the ElevenLabs transport. One function:
`buildUrl(baseUrl, path, query?)`.
## Purpose
The platform exposes around fifty REST endpoints, all rooted at the same
base URL, all reached by joining a slash-prefixed path and (sometimes) a
query string. Doing that with template literals at every call site
inevitably accumulates subtle bugs — double slashes, missing leading
slashes, `undefined` values serialised as the literal string
`"undefined"`. `buildUrl` normalises the construction so adapters never
have to think about it.
## Features
- Strips a trailing slash from the base URL, preserves a leading slash on
the path, and inserts one if missing.
- Accepts `string | number | boolean | undefined` query values and
silently drops `undefined` entries before serialisation.
- Builds the query string with `URLSearchParams` so values are
percent-encoded according to the WHATWG URL spec.
- Pure and synchronous — no I/O, no allocations beyond the returned
string.
## Usage: Basic Example
```ts
import { buildUrl } from "./url.js";
buildUrl("https://api.elevenlabs.io", "/v1/voices");
// → "https://api.elevenlabs.io/v1/voices"
buildUrl("https://api.elevenlabs.io/", "v1/voices/abc");
// → "https://api.elevenlabs.io/v1/voices/abc"
```
## Usage: Advanced Example
```ts
import { buildUrl } from "./url.js";
const url = buildUrl(
"https://api.elevenlabs.io",
"/v1/text-to-speech/voice-123/stream",
{
output_format: "mp3_44100_128",
optimize_streaming_latency: 2,
enable_logging: false,
next_text: undefined, // dropped
}
);
// → "https://api.elevenlabs.io/v1/text-to-speech/voice-123/stream?output_format=mp3_44100_128&optimize_streaming_latency=2&enable_logging=false"
```
## API Reference
- `buildUrl(baseUrl: string, path: string, query?: Readonly<Record<string, string | number | boolean | undefined>>): string`
- `baseUrl` — typically `https://api.elevenlabs.io`; trailing slash
is normalised away.
- `path` — leading slash is preserved or added.
- `query` — optional record. `undefined` values are dropped; non-string
values are stringified via `String(value)`.
## Implementation Notes
- The function returns the input as-is if `query` is omitted — no
`URLSearchParams` allocation when there are no parameters.
- All parameters become string-typed entries on the wire because that's
what `URLSearchParams.append` does, regardless of the input type.
- `buildUrl` is also reused by `ElevenLabsClient.openSocket` to build
WSS URLs after rewriting the scheme — the path / query semantics are
the same.

View File

@@ -0,0 +1,148 @@
# `errors`
Stable error class hierarchy and code registry for
`@suaveplan/third-party-elevenlabs`. Every non-2xx HTTP response and every
transport-level failure maps to one of 17 typed `ElevenLabsApiError`
subclasses under namespace `THIRD_PARTY_ELEVENLABS.*`.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The ElevenLabs API surface is large and surfaces failures across HTTP status
codes, malformed JSON bodies, transport timeouts, and WebSocket protocol
errors. Without a stable error hierarchy, every consumer of this client
would have to learn the upstream's quirks. This module defines the
canonical 17-code surface so callers can branch on `error.code` or
`error instanceof RateLimitExceededError` without touching the wire.
The mapping is total and disjoint over the `[400, 600)` HTTP range, every
documented network failure mode, every JSON / Zod parse failure, and the
two WebSocket failure modes (handshake / protocol). Every error preserves
the original cause via the cause-chain on `SuaveplanError`.
## Features
- 17 error codes registered via `defineCodes('THIRD_PARTY_ELEVENLABS', ...)`.
- 17 typed subclasses, one per code, with sensible default messages.
- Convenience options surface common transport context:
- `RateLimitExceededError({ retryAfterMs })` — retries planning.
- `RequestValidationFailedError({ body })` — preserved 422 body.
- `WebSocketConnectionFailedError({ code })` — close code.
- `NetworkTimeoutError({ timeoutMs })` — observed limit.
- Every HTTP error accepts `{ endpoint, statusCode, requestId }`.
- Caller-supplied `context` is shallow-merged on top of derived context so
adapter packages can layer their own attribution (e.g. `tenantId`).
## Usage: Basic Example
```ts
import {
ElevenLabsApiError,
RateLimitExceededError,
} from "@suaveplan/third-party-elevenlabs/errors/errors";
try {
await client.fetchJson(req, schema);
} catch (err) {
if (err instanceof RateLimitExceededError) {
console.log(`retry after ${err.context?.retryAfterMs ?? "unknown"} ms`);
}
}
```
## Usage: Advanced Example
Branch on every documented failure mode and re-throw with adapter context:
```ts
import {
ElevenLabsApiError,
ELEVENLABS_CODES,
} from "@suaveplan/third-party-elevenlabs/errors/errors";
function handleFailure(err: unknown): never {
if (!(err instanceof ElevenLabsApiError)) throw err;
switch (err.code) {
case ELEVENLABS_CODES["AUTH.UNAUTHORIZED"]:
case ELEVENLABS_CODES["AUTH.FORBIDDEN"]:
throw new ProviderNotConfiguredError({ cause: err });
case ELEVENLABS_CODES["RATE_LIMIT.EXCEEDED"]:
case ELEVENLABS_CODES["RATE_LIMIT.QUOTA_EXHAUSTED"]:
throw new ProviderRateLimitedError({
retryAfterMs: err.context?.retryAfterMs as number | undefined,
cause: err,
});
default:
throw new TtsSynthesisFailedError({ cause: err });
}
}
```
## API Reference
- `ELEVENLABS_CODES` — frozen record `{ "<reason>":
"THIRD_PARTY_ELEVENLABS.<reason>" }`.
- `ElevenLabsErrorCode` — TypeScript union of every code value.
- `ElevenLabsApiError` — abstract base extending `SuaveplanError`.
Auth + request errors:
- `UnauthorizedError({ endpoint?, statusCode?, requestId?, ...base })` —
`AUTH.UNAUTHORIZED` (HTTP 401).
- `ForbiddenError(...)` — `AUTH.FORBIDDEN` (HTTP 403).
- `RequestInvalidError(...)` — `REQUEST.INVALID` (other 4xx).
- `RequestValidationFailedError({ body?, ...request })` —
`REQUEST.VALIDATION_FAILED` (HTTP 422); body preserved verbatim.
- `RequestNotFoundError(...)` — `REQUEST.NOT_FOUND` (HTTP 404).
Rate limit:
- `RateLimitExceededError({ retryAfterMs?, ...request })` —
`RATE_LIMIT.EXCEEDED` (HTTP 429).
- `RateLimitQuotaExhaustedError(...)` — `RATE_LIMIT.QUOTA_EXHAUSTED`.
Server:
- `ServerTransientError(...)` — `SERVER.TRANSIENT` (retryable 5xx).
- `ServerUnavailableError(...)` — `SERVER.UNAVAILABLE` (non-retryable / circuit-open).
Network:
- `NetworkTimeoutError({ endpoint?, timeoutMs?, ...base })` —
`NETWORK.TIMEOUT`.
- `NetworkAbortedError({ endpoint?, ...base })` — `NETWORK.ABORTED`.
- `NetworkUnreachableError({ endpoint?, ...base })` — `NETWORK.UNREACHABLE`.
Response / stream:
- `ResponseMalformedError({ endpoint?, bodyPreview?, ...base })` —
`RESPONSE.MALFORMED`.
- `ResponseUnsupportedFormatError({ contentType?, ...base })` —
`RESPONSE.UNSUPPORTED_FORMAT`.
- `StreamInterruptedError(...)` — `STREAM.INTERRUPTED`.
WebSocket:
- `WebSocketConnectionFailedError({ code?, ...base })` —
`WEBSOCKET.CONNECTION_FAILED`.
- `WebSocketProtocolError({ code?, ...base })` — `WEBSOCKET.PROTOCOL`.
Every constructor accepts `{ message?, cause?, context?, traceId? }`.
## Implementation Notes
- The 17 codes partition `[400, 600)` HTTP statuses plus three networking
modes plus three response / stream modes plus two WebSocket modes. The
client never throws a generic `ElevenLabsApiError` — every thrown error
is one of the typed subclasses.
- `RequestValidationFailedError.body` should be the **parsed** JSON body
(truncated to a reasonable bound by the client — currently 4 KB; see
`client.ts`). Never put raw network buffers here.
- `bodyPreview` on `ResponseMalformedError` is a short string (≤ 256
bytes) suitable for logs. Adapters should not re-serialise the full body
into context.
- All subclasses default to a stable English message. Localisation flows
through `MessageResolver` on `@suaveplan/error`.

View File

@@ -0,0 +1,153 @@
# `types`
Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request /
response envelope shapes consumed by the transport layer (`ElevenLabsClient`)
and the feature modules built on top of it.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The transport layer needs a small, stable surface that every feature module
agrees on: how to describe a JSON request, how to describe a multipart
upload with mixed scalar / file fields, what the response metadata envelope
looks like, what events the client emits over an optional event bus. This
module locks those types in one place so any new feature module slots into
the existing transport without a new envelope shape.
## Features
- `RequestEnvelope` shared by every transport call (`method`, `path`,
`query`, `headers`, `signal`, `feature`, `operation`).
- `JsonRequest`, `BinaryRequest`, `StreamRequest`, `MultipartRequest`,
`SocketRequest` — one per transport mode, all derived from the shared
envelope.
- `MultipartFile` union of `Uint8Array | Blob | ReadableStream | string`
(URL).
- `MultipartField` discriminated union over `'scalar'` and `'file'` parts.
- `JsonResponse<TBody>`, `BinaryResponse`, `StreamResponse` — each carrying
the body and a `RequestMetadata` envelope (`requestId`,
`rateLimitRemaining`, `characterCost`).
- `ElevenLabsClientEvents` — typed event-bus map covering
`request-start`, `request-end`, `retry-attempt`, `rate-limit-hit`.
- `ElevenLabsClientOptions` — full constructor options including auth, base
URL, timeout, retry config, `fetchImpl`, `webSocketImpl`, `clock`,
`logger`, `events`, default headers.
- `WebSocketConstructor` / `WebSocketLike` — minimal duck types so
consumers can inject a fake WebSocket in tests.
- `SocketFrame` — `'text' | 'binary'` discriminated union.
- `SocketCloseEvent` — close-event surface.
## Usage: Basic Example
```ts
import type { JsonRequest } from "@suaveplan/third-party-elevenlabs/types/types";
const req: JsonRequest = {
method: "GET",
path: "/v1/voices",
feature: "voices",
operation: "list",
};
```
## Usage: Advanced Example
Build a multipart upload from a URL, raw bytes, and scalar fields:
```ts
import type {
MultipartField,
MultipartRequest,
} from "@suaveplan/third-party-elevenlabs/types/types";
const fields: MultipartField[] = [
{ kind: "scalar", name: "model_id", value: "scribe_v2" },
{
kind: "file",
name: "file",
filename: "audio.wav",
contentType: "audio/wav",
value: new Uint8Array(audioBytes),
},
{
kind: "file",
name: "additional",
filename: "extra.wav",
contentType: "audio/wav",
value: "https://cdn.example.com/extra.wav",
},
];
const req: MultipartRequest = {
method: "POST",
path: "/v1/speech-to-text",
fields,
feature: "stt",
operation: "transcribe",
};
```
## API Reference
Request envelopes:
- `RequestEnvelope` — `{ method, path, query?, headers?, signal?, feature,
operation }`.
- `JsonRequest extends RequestEnvelope` — `body?: unknown`.
- `BinaryRequest extends RequestEnvelope` — `body?: unknown; accept?: string`.
- `StreamRequest extends RequestEnvelope` — `body?: unknown; accept?: string`.
- `MultipartRequest extends RequestEnvelope` —
`fields: readonly MultipartField[]; accept?: string`.
- `SocketRequest` — `{ path, query?, headers?, feature, operation }`.
Multipart:
- `MultipartFile = Uint8Array | Blob | ReadableStream<Uint8Array> | string`.
- `MultipartFieldFile { kind: 'file'; name, filename, contentType?, value }`.
- `MultipartFieldScalar { kind: 'scalar'; name, value }`.
- `MultipartField = MultipartFieldFile | MultipartFieldScalar`.
Response envelopes:
- `RequestMetadata` — `{ requestId?, rateLimitRemaining?, characterCost? }`.
- `JsonResponse<TBody>` — `{ body: TBody, metadata: RequestMetadata }`.
- `BinaryResponse` — `{ body: Uint8Array, contentType, metadata }`.
- `StreamResponse` —
`{ body: ReadableStream<Uint8Array>, contentType, metadata }`.
Events:
- `ElevenLabsClientEvents` — typed event-bus map. See source for the four
event payload shapes.
Options:
- `ElevenLabsClientOptions` — see source for the full set of constructor
options.
WebSocket:
- `WebSocketConstructor` — `new (url, protocols?) => WebSocketLike`.
- `WebSocketLike` — minimal duck type covering `readyState`, `url`, `send`,
`close`, `addEventListener`, `removeEventListener`.
- `SocketFrame` — `'text' | 'binary'` discriminated union.
- `SocketCloseEvent` — `{ code, reason, wasClean }`.
## Implementation Notes
- `feature` and `operation` are intentionally part of every request
envelope so the transport layer can label OTel spans and counters
consistently without per-call wiring.
- `MultipartFile` accepts URLs as strings; the client fetches them via
the injected `fetchImpl` before uploading. This is the only place in
the transport that issues a *secondary* network call from a single
caller invocation.
- `RequestMetadata` keeps every field optional because some endpoints
do not surface `X-RateLimit-Remaining` or `X-Character-Cost`.
- `WebSocketConstructor` mirrors the WHATWG `WebSocket` constructor
signature so the global `WebSocket` is a valid value at runtime.
- The two-mode `SocketFrame` discriminated union keeps the wire shape
explicit; feature modules layer their own typed schemas on top.