# Connection Mode Documentation

This document provides detailed information about the connection modes supported by the Sofya Transcription SDK.

## Overview

The Sofya Transcription SDK supports multiple connection modes to accommodate different providers and authentication methods. New integrations should prefer `createTranscriber(connection)`, which narrows the returned type by provider. The legacy `new SofyaTranscriber(connection)` constructor remains supported.

## Connection Types

### API Key Connection

The API key connection mode is used when you want to authenticate with the Sofya API using an API key. This mode allows the SDK to automatically discover and connect to available providers.

```typescript
const transcriber = createTranscriber({
  apiKey: 'YOUR_API_KEY',
  config: {
    language: 'en-US'
  }
});
```

#### Properties

- `apiKey` (string, required): Your Sofya API key.
- `config` (BaseConfig, optional): Base configuration for the transcription service.

### Provider-Specific Connections

Provider-specific connections are used when you want to connect directly to a specific provider without using the Sofya API. This mode requires you to provide the provider-specific configuration and endpoint.

#### Sofya Compliance Provider

```typescript
const transcriber = createTranscriber({
  provider: 'sofya_compliance',
  endpoint: 'YOUR_ENDPOINT',
  config: {
    language: 'en-US',
    token: 'YOUR_TOKEN',
    compartmentId: 'YOUR_COMPARTMENT_ID',
    region: 'YOUR_REGION'
  }
});
```

#### Sofya As Service Provider

```typescript
const transcriber = createTranscriber({
  provider: 'sofya_as_service',
  endpoint: 'YOUR_ENDPOINT',
  config: {
    language: 'en-US'
  }
});
```

#### STT WVAD Provider

```typescript
const transcriber = createTranscriber({
  provider: 'stt_wvad',
  endpoint: 'YOUR_ENDPOINT',
  config: {
    language: 'en-US'
  }
});
```

#### Sofya Whisper Flow Provider

```typescript
const transcriber = createTranscriber({
  provider: 'sofya_whisper_flow',
  endpoint: 'YOUR_ENDPOINT',
  config: {
    language: 'en-US'
  }
});
```

## Configuration Types

### BaseConfig

The `BaseConfig` interface defines the common configuration properties shared by all providers.

```typescript
interface BaseConfig {
  language: string;
  external_id?: string;
  record?: boolean;
  protocols?: string | string[];
  resilience?: {
    connectTimeoutMs?: number;
    healthCheckIntervalMs?: number;
    maxBufferedAmountBytes?: number;
    drainStallTimeoutMs?: number;
    preRecoveryReplayMs?: number;
    reconnectBaseDelayMs?: number;
    reconnectMaxDelayMs?: number;
    reconnectDelayGrowFactor?: number;
    minConnectionUptimeMs?: number;
    maxReconnectAttempts?: number;
  };
  debug?: boolean | {
    enabled?: boolean;
    advancedMetrics?: boolean;
    autoDownload?: boolean;
    fileName?: string;
    label?: string;
    metadata?: Record<string, unknown>;
  };
  telemetry?: {
    enabled?: boolean;
    provider?: "dynatrace";
    dynatrace?: {
      globalKey?: string;
      actionName?: string;
    };
  };
}
```

#### Properties

- `language` (string, required): The language code for transcription (e.g., 'en-US', 'pt-BR').
- `external_id` (string, optional): Added to the websocket URL as the `x-external-id` query parameter on every connection, and copied verbatim into the recording metadata. Use an opaque value (UUID, hash, internal id; never CPF, medical record, name or e-mail), up to 512 characters. Use `[A-Za-z0-9._-]`, starting with a letter or digit, up to 64 characters, so the value is used as-is in the bucket path; otherwise the server uses a hash in the path and keeps the original only in the metadata.
- `record` (boolean, optional): Added to the websocket URL as `record=true`/`record=false` on every connection. Omitted when `undefined`, in which case the server environment default applies.
- `resilience` (object, optional): Transport recovery tuning.
- `debug` (boolean/object, optional): Debug audit controls. Essential audit data remains available even when disabled.
- `telemetry` (object, optional): Audit-first vendor connector settings. Opt-in and disabled by default.

### SofyaComplianceConfig

The `SofyaComplianceConfig` interface extends `BaseConfig` and adds Sofya Compliance-specific configuration properties.

```typescript
interface SofyaComplianceConfig extends BaseConfig {
  token: string;
  compartmentId: string;
  region: string;
}
```

#### Properties

- `language` (string, required): The language code for transcription (e.g., 'en-US', 'pt-BR').
- `token` (string, required): The authentication token.
- `compartmentId` (string, required): The compartment ID.
- `region` (string, required): The region (e.g., 'us-ashburn-1').

### SofyaSpeechConfig

The `SofyaSpeechConfig` interface extends `BaseConfig` and can be extended with Sofya As Service-specific configuration properties.

```typescript
interface SofyaSpeechConfig<TBatchPayload = unknown> extends BaseConfig {
  auth?: DirectModeAuth;
  translation_lang?: SupportedLanguage;
  token?: string;
  headers?: Record<string, string>;
  finalUpload?: FinalUploadConfig;
  batchReprocess?: {
    endpoint?: string;
    parseResponse?: (response: Response) => Promise<TBatchPayload>;
  };
}
```

#### Properties

- `language` (string, required): The language code for transcription (e.g., 'en-US', 'pt-BR').
- `auth` (object, optional): Direct-mode authentication. `{ type: "none" }` by default; `{ type: "api_key", key, transport }` sends the STT API key; `{ type: "jwt", token }` sends a JWT instead. See [Authentication in Direct Mode](#authentication-in-direct-mode).
- `translation_lang` (string, optional): Appended to the realtime URL as `translation_language` and inherited by batch reprocess.
- `headers` (object, optional): Extra HTTP headers. Inherited by the batch request, except `Content-Type`, which is managed by the SDK for multipart uploads.
- `batchReprocess` (object, optional): Whisper-only batch reprocess overrides. When `endpoint` is omitted, the SDK derives the batch URL from the realtime host and uses `/api/transcriber`.

## Authentication in Direct Mode

Direct-endpoint connections (`{ provider, endpoint, config }`) can authenticate
with an STT API key through the optional `config.auth` option:

```typescript
type DirectModeAuth =
  | { type: "none" }
  | {
      type: "api_key";
      key: string;
      transport?: "subprotocol" | "query"; // default: "subprotocol"
    }
  | { type: "jwt"; token: string }; // raw JWT, the SDK adds "Bearer "
```

`auth` lives on `SofyaSpeechConfig` and `SofyaBatchConfig`, the configs used by
the Whisper providers in direct mode. It is not available in `apiKey` mode,
where the reasoner `/providers` flow already authenticates the session, nor for
`sofya_compliance` / `oracle`.

Browsers cannot set request headers on a WebSocket handshake, so the SDK offers
the two browser-compatible transports accepted by the STT middleware.

### `transport: "subprotocol"` (default, recommended)

The key is sent as an extra `Sec-WebSocket-Protocol` token, `x-api-key.<key>`.
The server strips the token before the application sees it and echoes back the
first requested subprotocol, so the SDK always keeps the integrator protocols in
front and appends the key token last.

```typescript
const transcriber = createTranscriber({
  provider: "sofya_as_service",
  endpoint: "wss://scribe.sofya.health/api/realtime",
  config: {
    language: "pt-BR",
    auth: { type: "api_key", key: "YOUR_STT_API_KEY" },
  },
});
```

Requested subprotocols:

| `config.protocols` | Subprotocols sent on the handshake |
| --- | --- |
| omitted | `["sofya-stt.v1", "x-api-key.<key>"]` |
| `"bearer.jwt"` | `["bearer.jwt", "x-api-key.<key>"]` |
| `["bearer.jwt", "sofya-stt.v1"]` | `["bearer.jwt", "sofya-stt.v1", "x-api-key.<key>"]` |

Caveats:

- The server rejects a handshake whose only subprotocol is the key token
  (`subprotocol_companion_required`). That is why the SDK falls back to
  `sofya-stt.v1` as the companion token when no protocol is configured.
- RFC 6455 subprotocol tokens accept only letters, digits and
  ``!#$%&'*+-.^_`|~``; `=`, `/`, `,`, spaces and quotes are not allowed. The SDK
  never transforms the key: when it does not fit, the constructor throws
  `SofyaAuthError` with the code `INVALID_API_KEY_CHARACTERS` before any
  connection attempt. Issue a token-safe key or use `transport: "query"`.

### `transport: "query"`

The key is appended to the realtime URL as the `x-api-key` query parameter and
`config.protocols` are forwarded untouched.

```typescript
const transcriber = createTranscriber({
  provider: "sofya_as_service",
  endpoint: "wss://scribe.sofya.health/api/realtime",
  config: {
    language: "pt-BR",
    auth: {
      type: "api_key",
      key: "YOUR_STT_API_KEY",
      transport: "query",
    },
  },
});
```

URLs are routinely recorded by proxies, gateways, browser history and access
logs, so this transport is the less safe choice. Prefer it only when the key
cannot be expressed as an RFC 6455 token.

### Behavior notes

- Reconnections reuse the same authentication: the URL and the subprotocol list
  are rebuilt from the same connection configuration on every attempt.
- The key is also sent as the `x-api-key` header on the HTTP calls the SDK makes
  (batch reprocess and audit ingestion), unless `config.headers` already
  contains that header.
- The SDK never logs the key: debug output redacts `auth.key`, the
  `x-api-key.*` subprotocol token, the `x-api-key` header and the `x-api-key`
  query parameter as `[redacted]`.
- When the server rejects the key, it denies the handshake with HTTP 401 or
  closes with code `4401`. On `4401` the SDK emits `error` with a
  `SofyaAuthError` (code `AUTH_REJECTED`) plus `disconnected`, and stops
  reconnecting instead of retrying the same key. A handshake denied with HTTP
  401 is reported by the browser as a generic connection failure, so it is
  retried until `resilience.maxReconnectAttempts` is reached.
- `{ type: "none" }` (or omitting `auth`) keeps the previous behavior: the URL
  and the protocols are left exactly as configured.

### `type: "jwt"`

A JWT replaces the API key; the two cannot be combined
([ADR 0002](./adr/0002-exclusive-auth-strategy.md)). Every request the SDK makes
follows the chosen strategy:

| request | `api_key` | `jwt` |
|---|---|---|
| WebSocket | `x-api-key.<key>` subprotocol (last) or `x-api-key` query | the JWT as the **first** subprotocol, then `protocols` |
| primary batch POST | `x-api-key` header | `Authorization: Bearer <token>` |
| batch reprocess | `x-api-key` header | `Authorization: Bearer <token>` |
| audit ingestion | `x-api-key` header | `Authorization: Bearer <token>` (skipped with `missing_api_key` unless `headers` has `x-api-key`) |
| final upload | nothing | nothing |

> **Pass the raw JWT, without `Bearer `.** The SDK adds the prefix to the HTTP
> requests and throws `SofyaAuthError("INVALID_JWT")` when the token already
> starts with `Bearer `. It never strips the prefix silently.

- Realtime sessions require an RFC 6455 token. A standard JWT (base64url, no
  padding) always is; anything else throws
  `SofyaAuthError("INVALID_JWT_CHARACTERS")` before connecting. Batch sessions
  accept any character.
- Do not also put the JWT in `protocols`: it would be sent twice.
- An `Authorization` header in `config.headers` wins over the JWT.
- The Sofya STT does not validate JWTs: use `jwt` with STTs that have
  authentication off or sit behind a gateway that validates the JWT.
- Debug output shows `auth.token` as `[redacted]`.
- `config.token` is deprecated in direct mode (removal in 1.0.0). It still
  becomes `Authorization: Bearer <token>` with any `auth`; `auth.token` wins
  when both are set.

## Batch Reprocess Capability

Batch reprocess is supported only for the durable Whisper providers:

- `sofya_as_service`
- `stt_wvad`
- `sofya_whisper_flow`

The capability becomes available only after `stopTranscription()` and while the retained IndexedDB audio still exists inside the current 30 minute TTL window.

The batch request automatically inherits the realtime connection context:

- base URL / host
- `Authorization`
- extra configured headers
- `x-external-id`
- `transcription_language`
- `translation_language`

The SDK sends the request as `POST multipart/form-data` with a `file` part containing the exported consultation `.wav`.

Successful batch reprocessing does not clear the retained IndexedDB consultation storage. The database remains available until the stop-triggered TTL expires.

Use `getBatchReprocessStatus()` for UI state:

- `waiting_for_stop`
- `unconfigured`
- `ready`
- `running`
- `expired`
- `error`

Use `getBatchReprocessRemainingTime()` when you only need the remaining TTL in milliseconds without reading the full batch status snapshot.

If the batch endpoint is on another origin, browser consumers still need a valid CORS policy or a same-origin proxy.

## Connection Flow

### API Key Connection Flow

1. The SDK authenticates with the Sofya API using the provided API key.
2. The API returns a list of available providers and their configurations.
3. The SDK attempts to connect to each provider in order until a successful connection is established.
4. Once connected, the SDK emits a 'ready' event.

### Provider-Specific Connection Flow

1. The SDK attempts to connect directly to the specified provider using the provided endpoint and configuration.
2. Once connected, the SDK emits a 'ready' event.

## Error Handling

The SDK provides comprehensive error handling for connection issues:

- If the API key is invalid or missing, the SDK will throw an error.
- If `config.auth.key` cannot be sent as a WebSocket subprotocol, the SDK throws `SofyaAuthError` before connecting.
- If the server rejects the API key with close code `4401`, the SDK emits `error` with a `SofyaAuthError` and stops reconnecting.
- If no providers are available for the API key, the SDK will throw an error.
- If the connection to a provider fails, the SDK will attempt to connect to the next provider.
- If all providers fail to connect, the SDK will throw an error.

## Events

The SDK emits the following events during the connection process:

- `ready`: Emitted when the transcription service is ready to start.
- `error`: Emitted when an error occurs during the connection process.
- `connected`: Emitted when the transcription service is connected to the provider.
- `recognizing`: Emitted when transcription is in progress.
- `recognized`: Emitted when transcription is complete.
- `stopped`: Emitted when the transcription process is stopped.
- `reconnecting`: Emitted while websocket recovery is in progress.
- `reconnected`: Emitted when websocket recovery succeeds.
- `disconnected`: Emitted when websocket recovery reaches terminal disconnected state.
- `resilience_status`: Emitted whenever resilience snapshot changes.
- `telemetry`: Emitted whenever telemetry snapshot changes.
- `telemetry_row`: Emitted whenever a telemetry row is recorded.
- `telemetry_integration_warning`: Emitted for non-fatal vendor connector warnings.

## Examples

### API Key Connection

```typescript
import { SofyaTranscriber } from 'sofya.transcription';

// Create a transcriber with API key connection
const transcriber = new SofyaTranscriber({
  apiKey: 'YOUR_API_KEY',
  config: {
    language: 'en-US'
  }
});

// Listen for ready event
transcriber.on('ready', () => {
  console.log('Transcription service is ready');
  
  // Get media stream and start transcription
  navigator.mediaDevices.getUserMedia({ audio: true })
    .then(mediaStream => {
      transcriber.startTranscription(mediaStream);
    })
    .catch(error => {
      console.error('Error accessing microphone:', error);
    });
});

// Listen for error event
transcriber.on('error', (error) => {
  console.error('Transcription error:', error);
});
```

### Sofya Compliance Provider Connection

```typescript
import { SofyaTranscriber } from 'sofya.transcription';

// Create a transcriber with Sofya Compliance provider connection
const transcriber = new SofyaTranscriber({
  provider: 'sofya_compliance',
  endpoint: 'YOUR_ENDPOINT',
  config: {
    language: 'en-US',
    token: 'YOUR_TOKEN',
    compartmentId: 'YOUR_COMPARTMENT_ID',
    region: 'YOUR_REGION'
  }
});

// Listen for ready event
transcriber.on('ready', () => {
  console.log('Transcription service is ready');
  
  // Get media stream and start transcription
  navigator.mediaDevices.getUserMedia({ audio: true })
    .then(mediaStream => {
      transcriber.startTranscription(mediaStream);
    })
    .catch(error => {
      console.error('Error accessing microphone:', error);
    });
});

// Listen for error event
transcriber.on('error', (error) => {
  console.error('Transcription error:', error);
});
```
