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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ DEEPGRAM_LANGUAGE=en
#DEEPGRAM_GREETING=Hello! You've reached Komaa. How can I help?

# --- optional ---
PORT=8080
PORT=9442
BIND=0.0.0.0

# TLS: with BOTH set the bridge serves wss:// itself. Without them it is plain WS
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
COPY package.json ./
EXPOSE 8080
EXPOSE 9442
# healthcheck against the bridge's /healthz endpoint
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:${PORT:-8080}/healthz || exit 1
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:${PORT:-9442}/healthz || exit 1
USER node
CMD ["node", "dist/cli.js"]
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,11 @@ startServer(loadConfig(), undefined, undefined, { handleSignals: true });

### 3. Connect it to StandIn

StandIn dials in **from the internet**, so expose port 8080 (tunnel or public host), then register the URL on your identity in the [StandIn dashboard](https://standin.komaa.com/dashboard):
StandIn dials in **from the internet**, so expose port 9442 (tunnel or public host), then register the URL on your identity in the [StandIn dashboard](https://standin.komaa.com/dashboard):

```bash
tailscale funnel --bg --https=8080 8080
# Agent voice URL: wss://<machine>.<tailnet>.ts.net:8080/voice/msteams/stream
tailscale funnel --bg --set-path /voice/msteams/stream http://127.0.0.1:9442/voice/msteams/stream
# Agent voice URL: wss://<machine>.<tailnet>.ts.net/voice/msteams/stream
```

Place a Teams call to your bot (or join the [sandbox](https://standin.komaa.com/sandbox) meeting). StandIn joins, connects to the bridge, and your Deepgram agent answers.
Expand Down
2 changes: 1 addition & 1 deletion examples/basic-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ cp ../../.env.example .env # fill in DEEPGRAM_API_KEY and WORKER_SHARED_SECRET
npm start
```

The bridge listens on `PORT` (default 8080). Point your StandIn identity's agent
The bridge listens on `PORT` (default 9442). Point your StandIn identity's agent
WebSocket URL at `wss://<your-host>/voice/msteams/stream` (front it with TLS, or set
`TLS_CERT_PATH`/`TLS_KEY_PATH`), call your bot in Microsoft Teams, and talk to the agent.

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@komaa/deepgram-msteams-bridge",
"version": "0.1.2",
"version": "0.1.3",
"description": "Put a Deepgram Voice Agent on Microsoft Teams calls. Terminates the StandIn media bridge wire protocol on one side and the Deepgram Voice Agent WebSocket on the other. No transcoding: PCM 16k verbatim both ways, barge-in, extensible client-side tools, vision on demand, call governors.",
"keywords": [
"deepgram",
Expand Down
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { logger } from "./log.js";
const log = logger("config");

export interface BridgeConfig {
/** TCP port the bridge listens on for worker WebSocket upgrades. */
/** TCP port the bridge listens on for worker WebSocket upgrades (default 9442). */
port: number;
/** Bind address. */
host: string;
Expand Down Expand Up @@ -229,7 +229,7 @@ function numFromEnv(name: string, fallback: number): number {

export function loadConfig(): BridgeConfig {
return {
port: numFromEnv("PORT", 8080),
port: numFromEnv("PORT", 9442),
host: process.env.BIND?.trim() || "0.0.0.0",
workerSharedSecret: required("WORKER_SHARED_SECRET"),
deepgramApiKey: required("DEEPGRAM_API_KEY"),
Expand Down
2 changes: 1 addition & 1 deletion website/src/content/docs/configuration-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The Voice Agent API is audio-only, so the bridge answers `look` itself - and onl

| Env | Default | Meaning |
|---|---|---|
| `PORT` | `8080` | TCP port the bridge listens on. |
| `PORT` | `9442` | TCP port the bridge listens on. |
| `BIND` | `0.0.0.0` | Bind address. |
| `TLS_CERT_PATH` / `TLS_KEY_PATH` | unset | PEM cert/key for native TLS (`wss://`). Without both, the bridge serves plain WS and MUST be fronted by a TLS terminator. |
| `HMAC_FRESHNESS_MS` | `60000` | Allowed clock skew for the HMAC timestamp. |
Expand Down
6 changes: 3 additions & 3 deletions website/src/content/docs/connecting-to-standin.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ This bridge does not join Teams calls itself. That is the job of **StandIn** (th

## The connection model

- **The bridge is a WebSocket server.** It listens on `BIND:PORT` (default `0.0.0.0:8080`) and waits.
- **The bridge is a WebSocket server.** It listens on `BIND:PORT` (default `0.0.0.0:9442`) and waits.
- **The StandIn media bridge is the client.** For each Teams call it opens **one WebSocket** to your bridge, appending the call id to the URL path (the call id is the last path segment).
- **Authentication is HMAC over a shared secret.** Both sides hold the same secret. On the WebSocket upgrade, StandIn sends two headers whose signature is `HMAC-SHA256(secret, "{timestampMs}.{callId}")`. The bridge verifies it (constant-time, inside a 60 s freshness window, with a single-use replay guard) before accepting the connection. A mismatch is rejected with `401`.

```mermaid
flowchart LR
Teams["Teams call"]
StandIn["StandIn media bridge<br/>(hosted)<br/>dials {url}/{callId} with<br/>X-StandIn-Timestamp / -Signature"]
Bridge["your-host:8080<br/>(this bridge)"]
Bridge["your-host:9442<br/>(this bridge)"]
DG["Deepgram Voice Agent"]
Teams <--> StandIn
StandIn -- "HMAC WS" --> Bridge
Expand Down Expand Up @@ -62,7 +62,7 @@ The value in your env **must equal** the value StandIn holds, or the HMAC handsh
In the StandIn dashboard, set your identity's **agent WebSocket URL** to where this bridge listens, for example:

```text
wss://dg-bridge.example.com:8080/voice/msteams/stream
wss://dg-bridge.example.com:9442/voice/msteams/stream
```

StandIn appends `/{callId}` per call. Any base path works - the bridge takes the **last path segment** as the call id and verifies it against the HMAC signature and the `session.start` body.
Expand Down
15 changes: 8 additions & 7 deletions website/src/content/docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { loadConfig, startServer } from "@komaa/deepgram-msteams-bridge";
startServer(loadConfig()); // same env variables as the CLI
```

Every option is an environment variable; the package ships a fully commented [`.env.example`](https://github.com/komaa-com/deepgram-msteams-bridge/blob/main/.env.example), and the [Configuration Reference](/deepgram-msteams-bridge/configuration-reference/) documents each one. The bridge listens on `0.0.0.0:8080` by default and exposes `GET /healthz` for liveness checks.
Every option is an environment variable; the package ships a fully commented [`.env.example`](https://github.com/komaa-com/deepgram-msteams-bridge/blob/main/.env.example), and the [Configuration Reference](/deepgram-msteams-bridge/configuration-reference/) documents each one. The bridge listens on `0.0.0.0:9442` by default and exposes `GET /healthz` for liveness checks.

`WORKER_SHARED_SECRET` comes from StandIn in the next step.

Expand All @@ -51,33 +51,34 @@ The bridge appends per-call caller context (name, tenant, direction) to your pro
StandIn is the hosted service that joins the Teams call and dials into your bridge. Pick a tier at [standin.komaa.com](https://standin.komaa.com) (sandbox for an instant trial), pair, and you get a **shared secret**.

1. Put the secret in `WORKER_SHARED_SECRET` (both sides must match exactly).
2. Point the identity's **agent WebSocket URL** at your bridge, for example `wss://dg-bridge.example.com:8080/voice/msteams/stream`. StandIn appends `/{callId}` per call.
2. Point the identity's **agent WebSocket URL** at your bridge, for example `wss://dg-bridge.example.com:9442/voice/msteams/stream`. StandIn appends `/{callId}` per call.
3. Restart the bridge if you changed the env.

StandIn dials in **from the internet**, so a laptop or private host needs a public URL. A tunnel gives you one and terminates TLS (so you get `wss://` for free). Run one pointing at port `8080`, then use the `wss://…/voice/msteams/stream` form of the printed host:
StandIn dials in **from the internet**, so a laptop or private host needs a public URL. A tunnel gives you one and terminates TLS (so you get `wss://` for free). Run one pointing at port `9442`, then use the `wss://…/voice/msteams/stream` form of the printed host (Tailscale Funnel only serves 443/8443/10000, so mount the bridge path on 443 and use a portless `wss://` URL):

Tailscale Funnel:

```bash
tailscale funnel --bg --https=8080 8080
tailscale funnel --bg --set-path /voice/msteams/stream http://127.0.0.1:9442/voice/msteams/stream
# Agent voice URL: wss://<machine>.<tailnet>.ts.net/voice/msteams/stream
```

Cloudflare Tunnel:

```bash
cloudflared tunnel --url http://localhost:8080
cloudflared tunnel --url http://localhost:9442
```

ngrok:

```bash
ngrok http 8080
ngrok http 9442
```

VS Code dev tunnels:

```bash
devtunnel host -p 8080 --allow-anonymous
devtunnel host -p 9442 --allow-anonymous
```

For a fixed production host use an ingress/load balancer, or serve TLS natively with `TLS_CERT_PATH` + `TLS_KEY_PATH`. Never give StandIn a plain `ws://` URL outside local testing.
Expand Down