From f8e68dda96732947ee537000cc923a3e0e4980c6 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Tue, 29 Apr 2025 21:16:15 -0500
Subject: [PATCH 01/14] node-server docker container
---
node-server/Dockerfile | 17 +++++++++++++
node-server/src/index.ts | 14 +++++++++++
.../src/shared/config/config_schema.ts | 24 +++++++++----------
node-server/src/shared/config/load_config.ts | 2 +-
node-server/template.env | 1 +
5 files changed, 45 insertions(+), 13 deletions(-)
create mode 100644 node-server/Dockerfile
diff --git a/node-server/Dockerfile b/node-server/Dockerfile
new file mode 100644
index 0000000..6dfc50f
--- /dev/null
+++ b/node-server/Dockerfile
@@ -0,0 +1,17 @@
+FROM node:20
+
+WORKDIR /app
+
+COPY . .
+
+RUN npm install
+RUN npm run build
+
+WORKDIR /app/build/src
+
+ENV NODE_ENV="production"
+ENV LOG_DIR="/app/logs"
+ENV KEY_FILEPATH="/app/cert/key.pem"
+ENV CERTIFICATE_FILEPATH="/app/cert/cert.pem"
+
+CMD ["node", "index.js"]
\ No newline at end of file
diff --git a/node-server/src/index.ts b/node-server/src/index.ts
index 03a7656..174a502 100644
--- a/node-server/src/index.ts
+++ b/node-server/src/index.ts
@@ -24,6 +24,20 @@ async function init() {
logger.fatal({msg: 'Failed to start webserver', err});
throw err; // terminate if fails to start
}
+
+ // Close connection and server on SIGTERM/SIGINT for a graceful exit
+ process.on('SIGTERM', async () => {
+ logger.info('SIGTERM received, exiting gracefully.');
+ await server.close();
+ // eslint-disable-next-line n/no-process-exit
+ process.exit();
+ });
+ process.on('SIGINT', async () => {
+ logger.info('SIGINT received, exiting gracefully.');
+ await server.close();
+ // eslint-disable-next-line n/no-process-exit
+ process.exit();
+ });
}
await init();
diff --git a/node-server/src/shared/config/config_schema.ts b/node-server/src/shared/config/config_schema.ts
index ee51999..bd04c6c 100644
--- a/node-server/src/shared/config/config_schema.ts
+++ b/node-server/src/shared/config/config_schema.ts
@@ -17,27 +17,27 @@ export enum LogLevel {
// Define environment schema
export const SCHEMA = Type.Object({
- NODE_ENV: Type.Enum(NodeEnv),
+ NODE_ENV: Type.Enum(NodeEnv, {default: NodeEnv.Production}),
- LOG_LEVEL: Type.Enum(LogLevel),
+ LOG_LEVEL: Type.Enum(LogLevel, {default: LogLevel.Info}),
HOST: Type.String({default: 'localhost'}),
- PORT: Type.Number({default: 8000}),
+ PORT: Type.Number({default: 8080}),
USE_HTTPS: Type.Boolean({default: false}),
KEY_FILEPATH: Type.String({default: ''}),
CERTIFICATE_FILEPATH: Type.String({default: ''}),
- CORS_ORIGIN: Type.String(),
- SERVER_ADDRESS: Type.String(),
+ CORS_ORIGIN: Type.String({default: '*'}),
+ SERVER_ADDRESS: Type.String({default: '127.0.0.1:8080'}),
WHISPER_SERVICE_ENDPOINT: Type.String(),
- WHISPER_RECONNECT_INTERVAL: Type.Number({default: 1000}),
+ WHISPER_RECONNECT_INTERVAL_SEC: Type.Number({default: 1}),
- REQUIRE_AUTH: Type.Boolean(),
- ACCESS_TOKEN_BYTES: Type.Number(),
- ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Number(),
- ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Number(),
- SESSION_TOKEN_BYTES: Type.Number(),
- SESSION_LENGTH_SEC: Type.Number(),
+ REQUIRE_AUTH: Type.Boolean({default: true}),
+ ACCESS_TOKEN_BYTES: Type.Number({default: 32}),
+ ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Number({default: 150}),
+ ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Number({default: 300}),
+ SESSION_TOKEN_BYTES: Type.Number({default: 8}),
+ SESSION_LENGTH_SEC: Type.Number({default: 5400}),
});
export type ConfigType = Readonly<{
diff --git a/node-server/src/shared/config/load_config.ts b/node-server/src/shared/config/load_config.ts
index fe1919e..f815009 100644
--- a/node-server/src/shared/config/load_config.ts
+++ b/node-server/src/shared/config/load_config.ts
@@ -33,7 +33,7 @@ export default function loadConfig(path?: string): ConfigType {
},
whisper: {
endpoint: env.WHISPER_SERVICE_ENDPOINT,
- reconnectInterval: env.WHISPER_RECONNECT_INTERVAL,
+ reconnectInterval: env.WHISPER_RECONNECT_INTERVAL_SEC * 1000,
},
auth: {
required: env.REQUIRE_AUTH,
diff --git a/node-server/template.env b/node-server/template.env
index 7893082..b6aa25f 100644
--- a/node-server/template.env
+++ b/node-server/template.env
@@ -13,6 +13,7 @@ SERVER_ADDRESS="127.0.0.1:8080"
#### URL to whisper service
WHISPER_SERVICE_ENDPOINT="ws://127.0.0.1:8000/whisper?api_key=CHANGEME&model_key=faster-whisper:cpu-tiny-en"
+WHISPER_RECONNECT_INTERVAL_SEC=1
#### Authentication settings
# Enable or disable authentication
From cb2bf6063bc040196b1a925b187d93d36bee69f6 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Tue, 29 Apr 2025 21:16:28 -0500
Subject: [PATCH 02/14] whisper-service docker container
---
whisper-service/Dockerfile_CPU | 9 +++++++++
.../models/faster_whisper_model_requirements.txt | 2 --
whisper-service/requirements.txt | 6 +++++-
3 files changed, 14 insertions(+), 3 deletions(-)
create mode 100644 whisper-service/Dockerfile_CPU
delete mode 100644 whisper-service/models/faster_whisper_model_requirements.txt
diff --git a/whisper-service/Dockerfile_CPU b/whisper-service/Dockerfile_CPU
new file mode 100644
index 0000000..aba2801
--- /dev/null
+++ b/whisper-service/Dockerfile_CPU
@@ -0,0 +1,9 @@
+FROM python:3.12
+
+WORKDIR /app
+
+COPY . .
+
+RUN pip install -r requirements.txt
+
+CMD ["python", "index.py"]
\ No newline at end of file
diff --git a/whisper-service/models/faster_whisper_model_requirements.txt b/whisper-service/models/faster_whisper_model_requirements.txt
deleted file mode 100644
index 6ccc366..0000000
--- a/whisper-service/models/faster_whisper_model_requirements.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-faster-whisper==1.1.1
-ctranslate2==4.4.0
diff --git a/whisper-service/requirements.txt b/whisper-service/requirements.txt
index a291c31..c3e2b9c 100644
--- a/whisper-service/requirements.txt
+++ b/whisper-service/requirements.txt
@@ -8,4 +8,8 @@ pytest==8.3.5
pytest-cov==6.1.1
pylint==3.3.6
httpx==0.28.1
-pytest-asyncio==0.25.3
\ No newline at end of file
+pytest-asyncio==0.25.3
+
+# faster-whisper models
+faster-whisper==1.1.1
+ctranslate2==4.4.0
From e28744ec80048294d7c14f3070aa5b2e1fa849b9 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Tue, 29 Apr 2025 21:25:27 -0500
Subject: [PATCH 03/14] whisper-server dockerfile for cuda
---
whisper-service/Dockerfile_CUDA | 12 ++++++++++++
1 file changed, 12 insertions(+)
create mode 100644 whisper-service/Dockerfile_CUDA
diff --git a/whisper-service/Dockerfile_CUDA b/whisper-service/Dockerfile_CUDA
new file mode 100644
index 0000000..f6feb14
--- /dev/null
+++ b/whisper-service/Dockerfile_CUDA
@@ -0,0 +1,12 @@
+FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04
+
+ENV DEBIAN_FRONTEND=noninteractive
+RUN apt-get update && apt-get install -y python3 python3-pip
+
+WORKDIR /app
+
+COPY . .
+
+RUN pip install -r requirements.txt
+
+CMD ["python3", "index.py"]
\ No newline at end of file
From 7bd6815b925e4c8f0538a80e24ae00d79b3569ba Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bwu1324@users.noreply.github.com>
Date: Tue, 29 Apr 2025 22:37:52 -0500
Subject: [PATCH 04/14] fix dockerfile for cuda
---
node-server/.dockerignore | 133 +++++++++++++++++++++++
whisper-service/.dockerignore | 174 +++++++++++++++++++++++++++++++
whisper-service/Dockerfile_CUDA | 2 +-
whisper-service/create_server.py | 4 +-
whisper-service/model_factory.py | 21 +---
5 files changed, 315 insertions(+), 19 deletions(-)
create mode 100644 node-server/.dockerignore
create mode 100644 whisper-service/.dockerignore
diff --git a/node-server/.dockerignore b/node-server/.dockerignore
new file mode 100644
index 0000000..e908e4e
--- /dev/null
+++ b/node-server/.dockerignore
@@ -0,0 +1,133 @@
+build/
+coverage/
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
\ No newline at end of file
diff --git a/whisper-service/.dockerignore b/whisper-service/.dockerignore
new file mode 100644
index 0000000..1800114
--- /dev/null
+++ b/whisper-service/.dockerignore
@@ -0,0 +1,174 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+# However, in case of collaboration, if having platform-specific dependencies or dependencies
+# having no cross-platform support, pipenv may install dependencies that don't work, or not
+# install all needed dependencies.
+#Pipfile.lock
+
+# UV
+# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+#uv.lock
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# Ruff stuff:
+.ruff_cache/
+
+# PyPI configuration file
+.pypirc
\ No newline at end of file
diff --git a/whisper-service/Dockerfile_CUDA b/whisper-service/Dockerfile_CUDA
index f6feb14..31ac24b 100644
--- a/whisper-service/Dockerfile_CUDA
+++ b/whisper-service/Dockerfile_CUDA
@@ -1,4 +1,4 @@
-FROM nvidia/cuda:12.8.1-cudnn-runtime-ubuntu24.04
+FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y python3 python3-pip
diff --git a/whisper-service/create_server.py b/whisper-service/create_server.py
index cf9be2d..444490e 100644
--- a/whisper-service/create_server.py
+++ b/whisper-service/create_server.py
@@ -10,13 +10,13 @@
from typing import Annotated, Callable
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Query
from model_bases.transcription_model_base import TranscriptionModelBase
-from model_factory import ModelKey, model_factory
+from model_factory import model_factory
from load_config import AppConfig, load_config
def create_server(
config: AppConfig,
- model_factory_func: Callable[[ModelKey, WebSocket], TranscriptionModelBase]
+ model_factory_func: Callable[[str, WebSocket], TranscriptionModelBase]
) -> FastAPI:
'''
Instanciates FastAPI webserver.
diff --git a/whisper-service/model_factory.py b/whisper-service/model_factory.py
index 7ea6063..1ac92d5 100644
--- a/whisper-service/model_factory.py
+++ b/whisper-service/model_factory.py
@@ -8,36 +8,25 @@
ModelKey
'''
# pylint: disable=import-outside-toplevel
-from enum import StrEnum
from fastapi import WebSocket
from model_bases.transcription_model_base import TranscriptionModelBase
-
-class ModelKey(StrEnum):
- '''
- Unique identifiers for supported whisper models
- '''
- MOCK_TRANSCRIPTION_DURATION = "mock-transcription-duration"
- FASTER_WHISPER_GPU_LARGE_V3 = "faster-whisper:gpu-large-v3"
- FASTER_WHISPER_CPU_TINY_EN = "faster-whisper:cpu-tiny-en"
-
-
-def model_factory(model_key: ModelKey, websocket: WebSocket) -> TranscriptionModelBase:
+def model_factory(model_key: str, websocket: WebSocket) -> TranscriptionModelBase:
'''
Instantiates model with corresponding ModelKey.
Parameters:
- model_key (ModelKey) : Unique identifier for model to instantiate
+ model_key (str) : Unique identifier for model to instantiate
websocket (WebSocket): Websocket requesting model
Returns:
A TranscriptionModelBase instance
'''
match model_key:
- case ModelKey.MOCK_TRANSCRIPTION_DURATION:
+ case 'mock-transcription-duration':
from models.mock_transcription_duration import MockTranscribeDuration
return MockTranscribeDuration(websocket)
- case ModelKey.FASTER_WHISPER_GPU_LARGE_V3:
+ case 'faster-whisper:gpu-large-v3':
from models.faster_whisper_model import FasterWhisperModel
return FasterWhisperModel(
websocket,
@@ -46,7 +35,7 @@ def model_factory(model_key: ModelKey, websocket: WebSocket) -> TranscriptionMod
local_agree_dim=2,
min_new_samples=FasterWhisperModel.SAMPLE_RATE * 3
)
- case ModelKey.FASTER_WHISPER_CPU_TINY_EN:
+ case "faster-whisper:cpu-tiny-en":
from models.faster_whisper_model import FasterWhisperModel
return FasterWhisperModel(
websocket,
From f81f57b368433981666403e5aa964343b67a9849 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 05:16:37 -0500
Subject: [PATCH 05/14] update readme
---
node-server/README.md | 116 +++++++++++++++++++++++-------------------
1 file changed, 63 insertions(+), 53 deletions(-)
diff --git a/node-server/README.md b/node-server/README.md
index 7ccb08f..2315017 100644
--- a/node-server/README.md
+++ b/node-server/README.md
@@ -1,79 +1,89 @@
# ScribeAR Node Server
-A backend service for ScribeAR to handle authenticating and rebroadcasting transcription events. Built in Node.js with Fastify.
+A backend service for ScribeAR to handle authenticating and rebroadcasting transcription events. Users connect to node-server via websocket to send audio and/or receive transcriptions. When node-server receives wav audio chunks from the frontend it sends them to whisper-service to be transcribed. When it receives transcriptions back, it forwards them to all clients that are listening for transcriptions.
-## Getting Started
+# Getting Started
-### Prerequisites
+node-server can either be run via Docker or locally. See [Running via Docker](#running-via-docker) to run with docker or [Running Locally](#running-locally) to run locally. If you'd like to develop node-server, see [Developing](#developing).
-* Node.js (tested with Node 20)
+# Usage
-### Installing dependencies
+## Running via Docker
-1. Install dependencies
- ```
- npm install
- ```
+* TODO: Instructions for pulling container image and running
-### Configuration
+## Running Locally
-* Make a copy of `template.env` and name it `.env`
-* Edit `.env` to configure server. See [Configuration Options](#configuration-options) for details.
+### Local Setup
-## Developing
+1. Make sure you have Node.js installed. node-service has been tested to work with Node 20.
+2. Install Node dependencies
+ ```
+ npm install
+ ```
+3. Make a copy of `template.env` and name it `.env`
+4. Edit `.env` to configure server. See [Configuration Options](#configuration-options) for details.
-**Running in development mode**
+### Running Server
-1. Ensure dependencies are installed
-2. Configure service in `.env`
-3. Start webserver
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
+2. If you want to run node-server
```
- npm run dev
+ npm start
```
-**Running unit tests**
+# Developing
+
+## Running in Development Mode
-1. Ensure dependencies are installed
-2. Configure service in `.env`
-3. Run tests
+You can run node-server in development mode for easy to read logs and so that the server is automatically restarted when you save changes. This uses `tsc` to build the app, `tsc-watch` to watch for changes, and `pino-pretty` to output nice looking logs.
+
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
+2. Start webserver
```
- npm run test
+ npm run dev
```
-## Usage
+## Running Unit Tests
-**Running in production mode**
+Unit tests are run via `vitest` with code coverage via istanbul. See `vitest.config.ts` for test configuration.
-1. Ensure dependencies are installed
-2. Configure service in `.env`
-3. Start webserver
+1. Ensure dependencies are installed (See [Local Setup](#local-setup))
+2. Run tests
```
- npm start
+ npm run test
```
-## Documentation
-
-### Configuration Options
-
-| Option | Options | Description |
-| - | - | - |
-| `NODE_ENV` | `development`, `production`, `test` | Indicates the environment service is running in. Currently unused. (TODO: API documentation endpoint in dev mode) |
-| `LOG_LEVEL` | `error`, `warn`, `info`, `debug`, `trace`, `silent` | Sets the verbosity of logging. |
-| `HOST` | `ip address` | The socket the whisper service will bind to. Use `0.0.0.0` to make available to local network, `127.0.0.1` to localhost only. |
-| `PORT` | `number` | Port number that whisper service will listen for connections on. Should match the port node server is trying to connect to. |
-| `USE_HTTPS` | `boolean` | Whether to use HTTPS or not |
-| `KEY_FILEPATH` | `string` | File path to the private key file for use in when `USE_HTTPS` is set to `true` |
-| `CERTIFICATE_FILEPATH` | `string` | File path to the certificate file for use in when `USE_HTTPS` is set to `true` |
-| `CORS_ORIGIN` | `string` | Cors origin configuration for node server. |
-| `SERVER_ADDRESS` | `string` | Address the node server is reachable at. Used for ScribeAR QR code to allow other device to connect. |
-| `WHISPER_SERVICE_ENDPOINT` | `websocket address` | Websocket address for whisper service endpoint. Should be in the format:
`ws://{ADDRESS}:{PORT}/whisper?api_key={API_KEY}&model_key={MODEL_KEY}`
ADDRESS is the address or ip of the whisper service. For an all-in-one deployment, this is `127.0.0.1` (localhost).
`PORT` is the port the whisper service is listening on. This should match what the whisper service is configured to use.
`API_KEY` is the api key for the whisper service. This should match what the whisper service is configured to use.
`MODEL_KEY` is the model key of the model that the whisper service should run. See [Model Implementations and Model Keys](../whisper-service/README.md#model-implementations-and-model-keys) for more information. |
-| `REQUIRE_AUTH` | `true`, `false` | If `true`, requires authentication to connect to node server api, otherwise no authentication is used. See [Authentication](#authentication) for details. |
-| `ACCESS_TOKEN_REFRESH_INTERVAL_SEC` | `number` | Number of seconds to wait before generating a new refresh token. See [Authentication](#authentication) for details. |
-| `ACCESS_TOKEN_BYTES` | `number` | The number of random bytes used to generate access tokens |
-| `ACCESS_TOKEN_VALID_PERIOD_SEC` | `number` | Number of seconds a newly generated refresh token is valid for. See [Authentication](#authentication) for details. |
-| `SESSION_TOKEN_BYTES` | `number` | The number of random bytes used to generate session tokens |
-| `SESSION_LENGTH_SEC` | `number` | Number of seconds a newly generated session token is valid for. See [Authentication](#authentication) for details. |
-
-### Authentication
+# Documentation
+
+## Configuration Options
+
+The following options can be configured via environment variable.
+
+| Option | Values | Default | Description |
+| - | - | - | - |
+|**Runtime Options**|||
+| `NODE_ENV` | `development`, `production`, `test` | `production` | Indicates the environment service is running in. |
+| `LOG_LEVEL` | `error`, `warn`, `info`, `debug`, `trace`, `silent` | `info` | Sets the verbosity of logging. |
+|**Server Options**|||
+| `HOST` | `ip address` | `127.0.0.1` | The socket the whisper service will bind to. Use `0.0.0.0` to make available to local network, `127.0.0.1` to localhost only. |
+| `PORT` | `number` | `8080` | Port number that whisper service will listen for connections on. Should match the port node server is trying to connect to. |
+| `CORS_ORIGIN` | `string` | `*` | Cors origin configuration for node server. |
+| `SERVER_ADDRESS` | `string` | `127.0.0.1:8080` | Address the node server is reachable at. Used for ScribeAR QR code to allow other device to connect. |
+| `USE_HTTPS` | `boolean` | `false` | Whether to use HTTPS or not |
+| `KEY_FILEPATH` | `string` | Empty string | File path to the private key file for use in when `USE_HTTPS` is set to `true` |
+| `CERTIFICATE_FILEPATH` | `string` | Empty string | File path to the certificate file for use in when `USE_HTTPS` is set to `true` |
+|**Whisper Service Options**|||
+| `WHISPER_SERVICE_ENDPOINT` | `websocket address` | Required, no default value | Websocket address for whisper service endpoint. Should be in the format:
`ws://{ADDRESS}:{PORT}/whisper?api_key={API_KEY}&model_key={MODEL_KEY}`
ADDRESS is the address or ip of the whisper service. For an all-in-one deployment, this is `127.0.0.1` (localhost).
`PORT` is the port the whisper service is listening on. This should match what the whisper service is configured to use.
`API_KEY` is the api key for the whisper service. This should match what the whisper service is configured to use.
`MODEL_KEY` is the model key of the model that the whisper service should run. See [Model Implementations and Model Keys](../whisper-service/README.md#model-implementations-and-model-keys) for more information. |
+| `WHISPER_RECONNECT_INTERVAL` | `number` | `1` | Number of seconds to wait before attempting to reconnect to whisper service. Server implements exponential backoff, so interval will double each time connection fails up to a maximum of 30 seconds. |
+|**Authentication Options**|||
+| `REQUIRE_AUTH` | `true`, `false` | `true` | If `true`, requires authentication to connect to node server api, otherwise no authentication is used. See [Authentication](#authentication) for details. |
+| `ACCESS_TOKEN_REFRESH_INTERVAL_SEC` | `number` | `150` | Number of seconds to wait before generating a new refresh token. See [Authentication](#authentication) for details. |
+| `ACCESS_TOKEN_BYTES` | `number` | `32` | The number of random bytes used to generate access tokens |
+| `ACCESS_TOKEN_VALID_PERIOD_SEC` | `number` | `300` | Number of seconds a newly generated refresh token is valid for. See [Authentication](#authentication) for details. |
+| `SESSION_TOKEN_BYTES` | `number` | `8` | The number of random bytes used to generate session tokens |
+| `SESSION_LENGTH_SEC` | `number` | `5400` | Number of seconds a newly generated session token is valid for. See [Authentication](#authentication) for details. |
+
+## Authentication
* TODO: Explain authentication strategy here.
\ No newline at end of file
From 39f47276055e23b0ce8db1fb16827a3aaa523ed0 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 05:17:01 -0500
Subject: [PATCH 06/14] update config loading to better check types
---
node-server/package-lock.json | 217 ++++++++----------
node-server/package.json | 2 +-
node-server/src/index.ts | 23 ++
.../services/request_authorizer.test.ts | 20 +-
.../src/server/services/request_authorizer.ts | 30 ++-
.../services/transcription_engine.test.ts | 2 +-
.../server/services/transcription_engine.ts | 9 +-
node-server/src/server/start_server.ts | 2 +-
.../src/shared/config/config_schema.ts | 88 ++++---
node-server/src/shared/config/load_config.ts | 32 ++-
10 files changed, 246 insertions(+), 179 deletions(-)
diff --git a/node-server/package-lock.json b/node-server/package-lock.json
index e7816ab..c06c476 100644
--- a/node-server/package-lock.json
+++ b/node-server/package-lock.json
@@ -14,9 +14,9 @@
"@fastify/sensible": "6.0.3",
"@fastify/websocket": "11.0.2",
"@sinclair/typebox": "0.34.15",
+ "ajv": "8.17.1",
"axios": "1.7.9",
"dotenv": "16.4.7",
- "env-schema": "6.0.1",
"fastify": "5.2.1",
"http": "0.0.1-security",
"pino": "9.6.0",
@@ -819,6 +819,30 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@eslint/js": {
"version": "9.19.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
@@ -860,26 +884,6 @@
"fast-uri": "^3.0.0"
}
},
- "node_modules/@fastify/ajv-compiler/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
"node_modules/@fastify/cors": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.0.1.tgz",
@@ -1968,15 +1972,15 @@
}
},
"node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
+ "version": "8.17.1",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+ "license": "MIT",
"dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
@@ -1999,26 +2003,6 @@
}
}
},
- "node_modules/ajv-formats/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/ajv-formats/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -2520,6 +2504,7 @@
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
+ "license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
@@ -2527,14 +2512,6 @@
"url": "https://dotenvx.com"
}
},
- "node_modules/dotenv-expand": {
- "version": "10.0.0",
- "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
- "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
- "engines": {
- "node": ">=12"
- }
- },
"node_modules/duplexer": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
@@ -2578,46 +2555,6 @@
"once": "^1.4.0"
}
},
- "node_modules/env-schema": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/env-schema/-/env-schema-6.0.1.tgz",
- "integrity": "sha512-WRD40Q25pP4NUbI3g3CNU5PPzcaiX7YYcPwiCZlfR4qGsKmTlckRixgHww0/fOXiXSNKA87pwshzq0ULTK/48A==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/fastify"
- },
- {
- "type": "opencollective",
- "url": "https://opencollective.com/fastify"
- }
- ],
- "dependencies": {
- "ajv": "^8.12.0",
- "dotenv": "^16.4.5",
- "dotenv-expand": "10.0.0"
- }
- },
- "node_modules/env-schema/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/env-schema/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
"node_modules/error-ex": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
@@ -2919,6 +2856,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz",
@@ -2931,6 +2885,13 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/espree": {
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz",
@@ -3126,7 +3087,8 @@
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true
+ "dev": true,
+ "license": "MIT"
},
"node_modules/fast-json-stringify": {
"version": "6.0.0",
@@ -3142,36 +3104,11 @@
"rfdc": "^1.2.0"
}
},
- "node_modules/fast-json-stringify/node_modules/ajv": {
- "version": "8.17.1",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
- "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
- "dependencies": {
- "fast-deep-equal": "^3.1.3",
- "fast-uri": "^3.0.1",
- "json-schema-traverse": "^1.0.0",
- "require-from-string": "^2.0.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
- "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw=="
- },
"node_modules/fast-json-stringify/node_modules/fast-uri": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-2.4.0.tgz",
"integrity": "sha512-ypuAmmMKInk5q7XcepxlnUWDLWv4GFtaJqAzWKqn62IpQ3pejtr5dTVbt3vwqVaMKmkNR55sTT+CqUKIaT21BA=="
},
- "node_modules/fast-json-stringify/node_modules/json-schema-traverse": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
- },
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
@@ -3852,6 +3789,23 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
+ "node_modules/gts/node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/gts/node_modules/eslint": {
"version": "8.57.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz",
@@ -4004,6 +3958,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/gts/node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/gts/node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
@@ -4474,10 +4435,10 @@
}
},
"node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
@@ -5295,6 +5256,7 @@
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=6"
}
@@ -6469,6 +6431,7 @@
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
+ "license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
diff --git a/node-server/package.json b/node-server/package.json
index ce8d234..6c20ff4 100644
--- a/node-server/package.json
+++ b/node-server/package.json
@@ -46,9 +46,9 @@
"@fastify/sensible": "6.0.3",
"@fastify/websocket": "11.0.2",
"@sinclair/typebox": "0.34.15",
+ "ajv": "8.17.1",
"axios": "1.7.9",
"dotenv": "16.4.7",
- "env-schema": "6.0.1",
"fastify": "5.2.1",
"http": "0.0.1-security",
"pino": "9.6.0",
diff --git a/node-server/src/index.ts b/node-server/src/index.ts
index 174a502..aafdd92 100644
--- a/node-server/src/index.ts
+++ b/node-server/src/index.ts
@@ -5,6 +5,7 @@ import createLogger from './shared/logger/logger.js';
async function init() {
const config = loadConfig();
const logger = createLogger(config);
+ if (config.isDevelopment) logger.debug({msg: 'App configuration', config});
const server = createServer(config, logger);
process.on('uncaughtException', err => {
@@ -41,3 +42,25 @@ async function init() {
}
await init();
+
+// import {Type} from '@sinclair/typebox';
+// import {Value} from '@sinclair/typebox/value';
+
+// const TEST = Type.Intersect([
+// Type.Union([
+// Type.Object({
+// flag: Type.Literal(false),
+// }),
+// Type.Object({
+// flag: Type.Literal(true),
+// key: Type.String({minLength: 1}),
+// }),
+// ]),
+// Type.Object({
+// other: Type.String(),
+// }),
+// ]);
+
+// const env = Value.Convert(TEST, {flag: false, key: '90', other: 'asdf'});
+
+// console.log(env);
diff --git a/node-server/src/server/services/request_authorizer.test.ts b/node-server/src/server/services/request_authorizer.test.ts
index 719b360..95bfb08 100644
--- a/node-server/src/server/services/request_authorizer.test.ts
+++ b/node-server/src/server/services/request_authorizer.test.ts
@@ -13,10 +13,10 @@ describe('Request authorizer', () => {
auth: {
required: true,
accessTokenBytes: 8,
- accessTokenRefreshIntervalMS: 1_000,
- accessTokenValidPeriodMS: 10_000,
+ accessTokenRefreshIntervalSec: 1,
+ accessTokenValidPeriodSec: 10,
sessionTokenBytes: 32,
- sessionLengthMS: 30_000,
+ sessionLengthSec: 30,
},
} as ConfigType,
fakeLogger(),
@@ -200,7 +200,10 @@ describe('Request authorizer', () => {
describe('Authorization override', it => {
it('always accepts access tokens', () => {
- const ra = new RequestAuthorizer({auth: {required: false, accessTokenBytes: 8}} as ConfigType, fakeLogger());
+ const ra = new RequestAuthorizer(
+ {auth: {required: false, accessTokenBytes: 8}} as unknown as ConfigType,
+ fakeLogger(),
+ );
const {accessToken} = ra.getAccessToken();
@@ -210,7 +213,7 @@ describe('Request authorizer', () => {
it('always accepts session tokens', () => {
const ra = new RequestAuthorizer(
- {auth: {required: false, accessTokenBytes: 8, sessionTokenBytes: 32}} as ConfigType,
+ {auth: {required: false, accessTokenBytes: 8, sessionTokenBytes: 32}} as unknown as ConfigType,
fakeLogger(),
);
@@ -221,7 +224,10 @@ describe('Request authorizer', () => {
});
it('overrides localhost authorizer', async () => {
- const ra = new RequestAuthorizer({auth: {required: false, accessTokenBytes: 8}} as ConfigType, fakeLogger());
+ const ra = new RequestAuthorizer(
+ {auth: {required: false, accessTokenBytes: 8}} as unknown as ConfigType,
+ fakeLogger(),
+ );
const fastify = Fastify();
fastify.decorate('requestAuthorizer', ra);
@@ -235,7 +241,7 @@ describe('Request authorizer', () => {
it('overrides session token authorizer', async () => {
const ra = new RequestAuthorizer(
- {auth: {required: false, accessTokenBytes: 8, sessionTokenBytes: 32}} as ConfigType,
+ {auth: {required: false, accessTokenBytes: 8, sessionTokenBytes: 32}} as unknown as ConfigType,
fakeLogger(),
);
const fastify = Fastify();
diff --git a/node-server/src/server/services/request_authorizer.ts b/node-server/src/server/services/request_authorizer.ts
index 6582629..85976d8 100644
--- a/node-server/src/server/services/request_authorizer.ts
+++ b/node-server/src/server/services/request_authorizer.ts
@@ -3,6 +3,8 @@ import type {Logger} from '@shared/logger/logger.js';
import type {DoneFuncWithErrOrRes, FastifyReply, FastifyRequest} from 'fastify';
import crypto from 'node:crypto';
+const MAX_TIMESTAMP = 8640000000000000;
+
export default class RequestAuthorizer {
private _validAccessTokens: {[key: string]: Date} = {};
private _validSessionTokens: {[key: string]: Date} = {};
@@ -14,9 +16,11 @@ export default class RequestAuthorizer {
) {
this._updateAccessTokens();
- setInterval(() => {
- this._updateAccessTokens();
- }, this._config.auth.accessTokenRefreshIntervalMS);
+ if (this._config.auth.required) {
+ setInterval(() => {
+ this._updateAccessTokens();
+ }, this._config.auth.accessTokenRefreshIntervalSec * 1000);
+ }
this.authorizeLocalhost = this.authorizeLocalhost.bind(this);
this.authorizeSessionToken = this.authorizeSessionToken.bind(this);
@@ -27,10 +31,11 @@ export default class RequestAuthorizer {
* Updates this._currentAccessToken, this._validAccessTokens, and this._validSessionTokens
*/
private _updateAccessTokens() {
+ if (!this._config.auth.required) return;
this._log.debug('Updating access tokens');
this._currentAccessToken = crypto.randomBytes(this._config.auth.accessTokenBytes).toString('base64url');
- const expiry = new Date(Date.now() + this._config.auth.accessTokenValidPeriodMS);
+ const expiry = new Date(Date.now() + this._config.auth.accessTokenValidPeriodSec * 1000);
this._validAccessTokens[this._currentAccessToken] = expiry;
this._log.trace({msg: 'Created new access token', accessToken: this._currentAccessToken, expiry});
@@ -48,6 +53,17 @@ export default class RequestAuthorizer {
}
}
+ /**
+ * Computes the expiration date of a new session token
+ * @returns expiry date
+ */
+ private _computeNewSessionExpiry() {
+ if (!this._config.auth.required) {
+ return new Date(MAX_TIMESTAMP);
+ }
+ return new Date(Date.now() + this._config.auth.sessionLengthSec * 1000);
+ }
+
/**
* Gets the currently active access token and the expiration of said access token
* @returns object containg access token and expiry date
@@ -112,11 +128,15 @@ export default class RequestAuthorizer {
* @returns created session token
*/
createSessionToken() {
+ if (!this._config.auth.required) {
+ return {sessionToken: '', expires: this._computeNewSessionExpiry()};
+ }
let sessionToken = crypto.randomBytes(this._config.auth.sessionTokenBytes).toString('base64url');
while (sessionToken in this._validSessionTokens) {
sessionToken = crypto.randomBytes(this._config.auth.sessionTokenBytes).toString('base64url');
}
- const expires = new Date(Date.now() + this._config.auth.sessionLengthMS);
+
+ const expires = this._computeNewSessionExpiry();
this._validSessionTokens[sessionToken] = expires;
this._log.trace({msg: 'Creating new session token', sessionToken, expires});
diff --git a/node-server/src/server/services/transcription_engine.test.ts b/node-server/src/server/services/transcription_engine.test.ts
index 98e9ddc..257982b 100644
--- a/node-server/src/server/services/transcription_engine.test.ts
+++ b/node-server/src/server/services/transcription_engine.test.ts
@@ -32,7 +32,7 @@ describe('Transcription engine', it => {
{
whisper: {
endpoint: address,
- reconnectInterval: 1_000,
+ reconnectIntervalSec: 1,
},
} as ConfigType,
fakeLogger(),
diff --git a/node-server/src/server/services/transcription_engine.ts b/node-server/src/server/services/transcription_engine.ts
index ed7b455..2b944cd 100644
--- a/node-server/src/server/services/transcription_engine.ts
+++ b/node-server/src/server/services/transcription_engine.ts
@@ -22,13 +22,14 @@ export type AudioTranscriptEvents = {
export default class TranscriptionEngine extends TypedEmitter {
private _ws?: WebSocket;
private _reconnectInterval: number;
+ private _resetReconnectIntervalTimeout?: NodeJS.Timeout;
constructor(
private _config: ConfigType,
private _log: Logger,
) {
super();
- this._reconnectInterval = this._config.whisper.reconnectInterval;
+ this._reconnectInterval = this._config.whisper.reconnectIntervalSec * 1000;
this._connectWhisperService();
}
@@ -36,6 +37,7 @@ export default class TranscriptionEngine extends TypedEmitter {
@@ -51,7 +53,10 @@ export default class TranscriptionEngine extends TypedEmitter {
this._log.info('Connected to whisper service');
- this._reconnectInterval = this._config.whisper.reconnectInterval;
+
+ this._resetReconnectIntervalTimeout = setTimeout(() => {
+ this._reconnectInterval = this._config.whisper.reconnectIntervalSec * 1000;
+ }, 30_000);
});
this._ws.on('message', data => {
diff --git a/node-server/src/server/start_server.ts b/node-server/src/server/start_server.ts
index fee8e47..01537e8 100644
--- a/node-server/src/server/start_server.ts
+++ b/node-server/src/server/start_server.ts
@@ -30,7 +30,7 @@ export default function createServer(config: ConfigType, logger: Logger) {
loggerInstance: logger,
https: {
key: fs.readFileSync(config.server.keyPath),
- cert: fs.readFileSync(config.server.certificatePath),
+ cert: fs.readFileSync(config.server.certPath),
},
}) as unknown as FastifyInstance;
} else {
diff --git a/node-server/src/shared/config/config_schema.ts b/node-server/src/shared/config/config_schema.ts
index bd04c6c..fe9485a 100644
--- a/node-server/src/shared/config/config_schema.ts
+++ b/node-server/src/shared/config/config_schema.ts
@@ -16,30 +16,58 @@ export enum LogLevel {
}
// Define environment schema
-export const SCHEMA = Type.Object({
+const RUNTIME_CONFIG = Type.Object({
NODE_ENV: Type.Enum(NodeEnv, {default: NodeEnv.Production}),
-
LOG_LEVEL: Type.Enum(LogLevel, {default: LogLevel.Info}),
+});
+const SERVER_CONFIG = Type.Object({
HOST: Type.String({default: 'localhost'}),
PORT: Type.Number({default: 8080}),
- USE_HTTPS: Type.Boolean({default: false}),
- KEY_FILEPATH: Type.String({default: ''}),
- CERTIFICATE_FILEPATH: Type.String({default: ''}),
CORS_ORIGIN: Type.String({default: '*'}),
SERVER_ADDRESS: Type.String({default: '127.0.0.1:8080'}),
+});
- WHISPER_SERVICE_ENDPOINT: Type.String(),
- WHISPER_RECONNECT_INTERVAL_SEC: Type.Number({default: 1}),
+const HTTPS_CONFIG = Type.Union([
+ Type.Object({
+ USE_HTTPS: Type.Literal(false),
+ KEY_FILEPATH: Type.Any(),
+ CERTIFICATE_FILEPATH: Type.Any(),
+ }),
+ Type.Object({
+ USE_HTTPS: Type.Literal(true),
+ KEY_FILEPATH: Type.String({minLength: 1}),
+ CERTIFICATE_FILEPATH: Type.String({minLength: 1}),
+ }),
+]);
- REQUIRE_AUTH: Type.Boolean({default: true}),
- ACCESS_TOKEN_BYTES: Type.Number({default: 32}),
- ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Number({default: 150}),
- ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Number({default: 300}),
- SESSION_TOKEN_BYTES: Type.Number({default: 8}),
- SESSION_LENGTH_SEC: Type.Number({default: 5400}),
+const WHISPER_CONFIG = Type.Object({
+ WHISPER_SERVICE_ENDPOINT: Type.String({minLength: 1}),
+ WHISPER_RECONNECT_INTERVAL_SEC: Type.Number({default: 1}),
});
+const AUTH_CONFIG = Type.Union([
+ Type.Object({
+ REQUIRE_AUTH: Type.Const(false),
+ ACCESS_TOKEN_BYTES: Type.Any(),
+ ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Any(),
+ ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Any(),
+ SESSION_TOKEN_BYTES: Type.Any(),
+ SESSION_LENGTH_SEC: Type.Any(),
+ }),
+ Type.Object({
+ REQUIRE_AUTH: Type.Const(true),
+ ACCESS_TOKEN_BYTES: Type.Number({minimum: 1}),
+ ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Number({minimum: 1}),
+ ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Number({minimum: 1}),
+ SESSION_TOKEN_BYTES: Type.Number({minimum: 1}),
+ SESSION_LENGTH_SEC: Type.Number({minimum: 1}),
+ }),
+]);
+
+// Merge all configs into one schema
+export const SCHEMA = Type.Intersect([RUNTIME_CONFIG, SERVER_CONFIG, HTTPS_CONFIG, WHISPER_CONFIG, AUTH_CONFIG]);
+
export type ConfigType = Readonly<{
nodeEnv: NodeEnv;
isDevelopment: boolean;
@@ -50,22 +78,30 @@ export type ConfigType = Readonly<{
server: {
host: string;
port: number;
- useHttps: boolean;
- keyPath: string;
- certificatePath: string;
corsOrigin: string;
serverAddress: string;
- };
+ } & (
+ | {
+ useHttps: false;
+ }
+ | {
+ useHttps: true;
+ keyPath: string;
+ certPath: string;
+ }
+ );
whisper: {
endpoint: string;
- reconnectInterval: number;
- };
- auth: {
- required: boolean;
- accessTokenBytes: number;
- accessTokenRefreshIntervalMS: number;
- accessTokenValidPeriodMS: number;
- sessionTokenBytes: number;
- sessionLengthMS: number;
+ reconnectIntervalSec: number;
};
+ auth:
+ | {required: false}
+ | {
+ required: true;
+ accessTokenBytes: number;
+ accessTokenRefreshIntervalSec: number;
+ accessTokenValidPeriodSec: number;
+ sessionTokenBytes: number;
+ sessionLengthSec: number;
+ };
}>;
diff --git a/node-server/src/shared/config/load_config.ts b/node-server/src/shared/config/load_config.ts
index f815009..47e9720 100644
--- a/node-server/src/shared/config/load_config.ts
+++ b/node-server/src/shared/config/load_config.ts
@@ -1,6 +1,7 @@
+import {Ajv} from 'ajv';
import type {Static} from '@sinclair/typebox';
import {NodeEnv, SCHEMA, type ConfigType} from './config_schema.js';
-import envSchema from 'env-schema';
+import {configDotenv} from 'dotenv';
/**
* Loads environment configuration file from privated path
@@ -9,11 +10,24 @@ import envSchema from 'env-schema';
* @returns configuration
*/
export default function loadConfig(path?: string): ConfigType {
- const env = envSchema>({
- dotenv: {path},
- schema: SCHEMA,
+ // Load from .env file
+ configDotenv({path});
+
+ const ajv = new Ajv({
+ allErrors: true,
+ useDefaults: true,
+ coerceTypes: true,
+ allowUnionTypes: true,
});
+ const env = Object.assign({}, process.env) as unknown as Static;
+ const valid = ajv.validate(SCHEMA, env);
+ if (!valid) {
+ const error = new Error('Invalid Configuration! ' + ajv.errorsText());
+ (error as unknown as {errors: unknown}).errors = ajv.errors;
+ throw error;
+ }
+
// Application configuration object
const config: ConfigType = Object.freeze({
nodeEnv: env.NODE_ENV,
@@ -27,21 +41,21 @@ export default function loadConfig(path?: string): ConfigType {
port: env.PORT,
useHttps: env.USE_HTTPS,
keyPath: env.KEY_FILEPATH,
- certificatePath: env.CERTIFICATE_FILEPATH,
+ certPath: env.CERTIFICATE_FILEPATH,
corsOrigin: env.CORS_ORIGIN,
serverAddress: env.SERVER_ADDRESS,
},
whisper: {
endpoint: env.WHISPER_SERVICE_ENDPOINT,
- reconnectInterval: env.WHISPER_RECONNECT_INTERVAL_SEC * 1000,
+ reconnectIntervalSec: env.WHISPER_RECONNECT_INTERVAL_SEC,
},
auth: {
required: env.REQUIRE_AUTH,
accessTokenBytes: env.ACCESS_TOKEN_BYTES,
- accessTokenRefreshIntervalMS: env.ACCESS_TOKEN_REFRESH_INTERVAL_SEC * 1000,
- accessTokenValidPeriodMS: env.ACCESS_TOKEN_VALID_PERIOD_SEC * 1000,
+ accessTokenRefreshIntervalSec: env.ACCESS_TOKEN_REFRESH_INTERVAL_SEC,
+ accessTokenValidPeriodSec: env.ACCESS_TOKEN_VALID_PERIOD_SEC,
sessionTokenBytes: env.SESSION_TOKEN_BYTES,
- sessionLengthMS: env.SESSION_LENGTH_SEC * 1000,
+ sessionLengthSec: env.SESSION_LENGTH_SEC,
},
});
From 964484b589aa9771af8358687bdefb75c0f0b75a Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 05:41:50 -0500
Subject: [PATCH 07/14] update readme for docker containers
---
node-server/README.md | 34 +++++++++-
whisper-service/README.md | 138 +++++++++++++++++++++-----------------
2 files changed, 109 insertions(+), 63 deletions(-)
diff --git a/node-server/README.md b/node-server/README.md
index 2315017..b54b3f8 100644
--- a/node-server/README.md
+++ b/node-server/README.md
@@ -4,7 +4,7 @@ A backend service for ScribeAR to handle authenticating and rebroadcasting trans
# Getting Started
-node-server can either be run via Docker or locally. See [Running via Docker](#running-via-docker) to run with docker or [Running Locally](#running-locally) to run locally. If you'd like to develop node-server, see [Developing](#developing).
+node-server can either be run via Docker or locally. See [Running via Docker](#running-via-docker) to run with docker or [Running Locally](#running-locally) to run locally. If you'd like to develop node-server, see [Developing](#developing). Additional documentation can be found at [Documentation](#documentation).
# Usage
@@ -27,7 +27,7 @@ node-server can either be run via Docker or locally. See [Running via Docker](#r
### Running Server
1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
-2. If you want to run node-server
+2. Run node-server
```
npm start
```
@@ -46,7 +46,7 @@ You can run node-server in development mode for easy to read logs and so that th
## Running Unit Tests
-Unit tests are run via `vitest` with code coverage via istanbul. See `vitest.config.ts` for test configuration.
+Unit tests are run via `vitest` with code coverage via istanbul. See `vitest.config.ts` for test configuration. A Github Action runs unit tests when a PR is created, make sure tests pass before creating a PR.
1. Ensure dependencies are installed (See [Local Setup](#local-setup))
2. Run tests
@@ -54,6 +54,34 @@ Unit tests are run via `vitest` with code coverage via istanbul. See `vitest.con
npm run test
```
+## Running Linter
+
+`eslint` is used to lint code to ensure consistent code style. A Github Action runs linter when a PR is created to enforce this, make sure linter is successful before creating a PR.
+
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
+2. Run linter
+ ```
+ npm run lint
+ ```
+3. `eslint` can attempt to automatically fix linter issues
+ ```
+ npm run fix
+ ```
+
+## Build Docker Container
+
+To test your local changes in a Docker container you can build it locally. A Github Action builds Docker container when a PR is created, make sure build is successful before creating a PR.
+
+1. Ensure you have Docker installed.
+2. Build container
+ ```
+ docker -t scribear-node-server -f ./Dockerfile .
+ ```
+3. Run container (additional configuration can be passed in via `--env` flag)
+ ```
+ docker run --env WHISPER_SERVICE_ENDPOINT=[your-whisper-service-endpoint] --env HOST=0.0.0.0 -p 8080:8080 scribear-node-server:latest
+ ```
+
# Documentation
## Configuration Options
diff --git a/whisper-service/README.md b/whisper-service/README.md
index 04004f5..046c79b 100644
--- a/whisper-service/README.md
+++ b/whisper-service/README.md
@@ -1,54 +1,70 @@
# ScribeAR Whisper Service
-A backend service for ScribeAR to generate transcriptions from a WAV audio stream. Supports several whisper implementations. Flexible interface allows easy integration with new models. Built in Python with FastAPI.
+A backend service for ScribeAR to generate transcriptions from a WAV audio stream. Uses a flexible interface Python to allow for multiple transcription backends.
-## Getting Started
+# Getting Started
-### Prerequisites
+whisper-service can either be run via Docker or locally. See [Running via Docker](#running-via-docker) to run with docker or [Running Locally](#running-locally) to run locally. If you'd like to develop node-server, see [Developing](#developing). Additional documentation can be found at [Documentation](#documentation).
-* Python 3 (tested with Python 3.12)
+# Usage
-### Installing dependencies
+## Running via Docker
-0. (optional) Create python virtual environment
-1. Install global python dependencies
- ```
- pip install -r requirements.txt
- ```
+* TODO: Instructions for pulling container image and running
-2. Per model implementation dependencies. Each model implementation has its own `{model_implementation}_requirements.txt` found in the `/models` directory. Install the dependencies for the models you'd like to run. See [Model Implementations and Model Keys](#model-implementations-and-model-keys) for details.
+## Running Locally
+
+### Local Setup
+
+1. Make sure you ahve Python 3 installed. whisper-service has been tested to work with with Python 3.12.
+2. (optional) Create python virtual environment
+3. Install global python dependencies
```
- pip install -r models/{model_implementation}_requirements.txt
+ pip install -r requirements.txt
```
+4. Make a copy of `template.env` and name it `.env`
+5. Edit `.env` to configure server. See [Configuration Options](#configuration-options) for details.
-### Configuration
+### Running Server
-* Make a copy of `template.env` and name it `.env`
-* Edit `.env` to configure server. See [Configuration Options](#configuration-options) for details.
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
+2. Run whisper-service
+ ```
+ python index.py
+ ```
-## Developing
+# Developing
-**Run unit tests**
+## Running in Development Mode
-1. Ensure that you have installed global dependencies dependencies
-2. Run tests with pytest
+You can run whisper-service in development mode so that the server is automatically restarted when you save changes.
+
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
+2. Start webserver
```
- pytest
+ fastapi dev create_server.py
```
-**Test coverage**
+## Running Unit Tests
-1. Ensure that you have installed global dependencies dependencies
-2. Run tests with pytest
- ```
- pytest --cov=. --cov-report=html
- ```
-3. Coverage results can be found in `htmlcov` folder
+Unit tests are run using `pytest`. Test coverage stats can be seen by passing additional arguments to `pytest`. A Github Action runs unit tests when a PR is created, make sure tests pass before creating a PR.
-**Linting**
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
+2.
+ * Run tests with pytest without code coverage
+ ```
+ pytest
+ ```
+ * Run tests with pytest with code coverage. Coverage results can be found in `htmlcov` folder
+ ```
+ pytest --cov=. --cov-report=html
+ ```
-1. Ensure that you have installed global dependencies dependencies
+## Running Linter
+`pylint` is used to lint code to ensure consistent code style. A Github Action runs linter when a PR is created to enforce this, make sure linter is successful before creating a PR.
+
+1. Ensure that the app is installed and configured locally (See [Local Setup](#local-setup))
2.
* For a single file:
```
@@ -59,46 +75,48 @@ A backend service for ScribeAR to generate transcriptions from a WAV audio strea
pylint $(git ls-files '*.py')
```
+## Build Docker Container
-**Implementing a new model**
+To test your local changes in a Docker container you can build it locally. A Github Action builds Docker container when a PR is created, make sure build is successful before creating a PR.
+
+1. Ensure you have Docker installed.
+2. Build container
+ * For CPU only container
+ ```
+ docker -t scribear-whisper-service -f ./Dockerfile_CPU .
+ ```
+ * For CUDA support
+ ```
+ docker -t scribear-whisper-service-cuda -f ./Dockerfile_CUDA .
+ ```
+3. Run container (additional configuration can be passed in via `--env` flag)
+ * For CPU only container
+ ```
+ docker run --env PORT=8080 --env HOST=0.0.0.0 --env API_KEY=CHANGEME -p 8080:8080 scribear-whisper-service:latest
+ ```
+ * For CUDA support
+ ```
+ docker run --env PORT=8080 --env HOST=0.0.0.0 --env API_KEY=CHANGEME -p 8080:8080 --gpus all scribear-whisper-service-cuda:latest
+ ```
+## Implementing a New Model
1. Implement `TranscriptionModelBase`. See `/model_bases/transcription_model_base.py` for required methods and usage.
* Other model bases found in `/model_bases` can be helpful for implementing commonly used functions.
-2. Create `{model_implementation}_requirements.txt` and populate with python dependencies for your model.
+2. Add python dependencies for your model to `requirements.txt`.
3. Associate [model key(s)](#model-implementations-and-model-keys) to your model implementation in `model_factory.py`.
-**Running in development mode**
-
-1. Ensure dependencies are installed
-2. Configure service in `.env`
-3. Start webserver
- ```
- fastapi dev create_server.py
- ```
-
-## Usage
-
-**Running in production mode**
-
-1. Ensure dependencies are installed
-2. Configure service in `.env`
-3. Start webserver
- ```
- python index.py
- ```
-
-## Documentation
+# Documentation
-### Configuration Options
+## Configuration Options
-| Option | Options | Description |
-|-------------|--------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
-| `LOG_LEVEL` | `info`, `debug`, `trace` | Sets the verbosity of logging. |
+| Option | Options | Description |
+|-------------|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| `LOG_LEVEL` | `info`, `debug`, `trace` | Sets the verbosity of logging. |
| `API_KEY` | `string` | The api key that must be passed to whisper-service in order to establish a connection. Should match `api_key=` url parameter for node server. |
-| `HOST` | `ip address` | The socket the whisper service will bind to. Use `0.0.0.0` to make available to local network, `127.0.0.1` to localhost only. |
-| `PORT` | `number` | Port number that whisper service will listen for connections on. Should match the port node server is trying to connect to. |
+| `HOST` | `ip address` | The socket the whisper service will bind to. Use `0.0.0.0` to make available to local network, `127.0.0.1` to localhost only. |
+| `PORT` | `number` | Port number that whisper service will listen for connections on. Should match the port node server is trying to connect to. |
-### Model Implementations and Model Keys
+## Model Implementations and Model Keys
Each model key (e.g. `faster-whisper:cpu-tiny-en`, `faster-whisper:gpu-tiny-en`) must be mapped to an model implementation (e.g. `faster-whisper`, `mock-transcription-duration`).
@@ -111,5 +129,5 @@ Below is a table of model keys and model implementations
| Model Key | Model Implementation | Description |
|-------------------------------|-------------------------------|---------------------------------------------------------------------------------------------------------------------------|
| `mock_transcription_duration` | `mock-transcription-duration` | This model does not perform transcriptions. It returns the transcription blocks describing the duration of audio recieved |
-| `faster-whisper:gpu-large-v3` | `faster-whisper` | Run faster whisper using large-v3 model with 2-dim local agreement in 3 second chunks on gpu |
+| `faster-whisper:gpu-large-v3` | `faster-whisper` | Run faster whisper using large-v3 model with 2-dim local agreement in 3 second chunks on gpu |
| `faster-whisper:cpu-tiny-en` | `faster-whisper` | Run faster whisper using tiny.en model with 2-dim local agreement in 3 second chunks on cpu |
From 8c08a91dcd3a7b7d40a5384b83d6d079ae1edc35 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 06:39:17 -0500
Subject: [PATCH 08/14] create docker compose file
---
.env | 31 ++++++++++++++
README.md | 36 +++++++++++++---
compose_cpu.yaml | 41 +++++++++++++++++++
compose_cuda.yaml | 41 +++++++++++++++++++
node-server/Dockerfile | 2 -
node-server/README.md | 14 +++----
.../src/shared/config/config_schema.ts | 7 ----
node-server/src/shared/config/load_config.ts | 6 ++-
node-server/template.env | 6 +--
9 files changed, 157 insertions(+), 27 deletions(-)
create mode 100644 .env
create mode 100644 compose_cpu.yaml
create mode 100644 compose_cuda.yaml
diff --git a/.env b/.env
new file mode 100644
index 0000000..9b85a44
--- /dev/null
+++ b/.env
@@ -0,0 +1,31 @@
+NODE_ENV="production"
+LOG_LEVEL="info"
+
+#### Host and port API webserver should listen on
+HOST="0.0.0.0"
+PORT=8080
+CORS_ORIGIN='*'
+SERVER_ADDRESS="127.0.0.1:8080"
+# Enable or disable HTTPS
+USE_HTTPS=false
+KEY_FILEPATH=''
+CERTIFICATE_FILEPATH=''
+
+WHISPER_RECONNECT_INTERVAL_SEC=1
+
+#### Authentication settings
+# Enable or disable authentication
+REQUIRE_AUTH=false
+# How many bytes of random data should be used to generate access token
+ACCESS_TOKEN_BYTES=8
+# How often access token should be refreshed in seconds
+ACCESS_TOKEN_REFRESH_INTERVAL_SEC=150
+# How long a single access token is valid for after generation in seconds
+ACCESS_TOKEN_VALID_PERIOD_SEC=300
+# How many bytes of random data should be used to generate session token
+SESSION_TOKEN_BYTES=32
+# How long a single session token is valid for after generation in seconds
+SESSION_LENGTH_SEC=5400
+
+MODEL_KEY="faster-whisper:cpu-tiny-en"
+API_KEY="CHANGEME"
\ No newline at end of file
diff --git a/README.md b/README.md
index e9f4a38..22e8f46 100644
--- a/README.md
+++ b/README.md
@@ -2,17 +2,41 @@
Backend for ScribeAR. Handles transcribing audio stream and rebroadcasting transcriptions to multiple devices.
-## Getting Started
+# Getting Started
-* See `README.md` in `/node-server` and `/whisper-service` folders for detailed instructions for installation, development, and usage.
+See the `README.md` files in `/node-server` and `/whisper-service` folders for detailed instructions for installation, configuration, development, and usage of node-server and whisper-service. See [Setup](#setup)
-## Usage
+# Setup ScribeAR Server
-### All-in-one Deployment
+## Docker Deployment
-Deploys node-server, whisper-service, and ScribeAR frontend to be running on the same system.
+1. Install Docker using official methods
+ * https://www.docker.com/
+ * If you'd like to use CUDA, make sure you have the Nvidia Container Toolkit installed as well
+ * https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html
+2. Clone this repository
+ ```
+ git clone https://github.com/scribear/ScribeAR-NodeServer
+ ```
+3. Make a copy of `template.env` and name it `.env`
+4. Edit `.env` to configure deployment.
+ * Configuration matches that of [Node Server Configuration](./node-server/README.md#configuration-options) and [Whisper Service Configuration](./whisper-service/README.md#configuration-options) with a few exceptions, noted below.
+ * `HOST` and `PORT` for whisper-service and node-server are disabled.
+ * `MODEL_KEY` is added for node-server. This is the [model key](#model-implementations-and-model-keys) that node-server will use when connecting to whisper-service.
+ * `WHISPER_SERVICE_ENDPOINT` for node-server is removed and instead automatically generated using `MODEL_KEY` and `API_KEY`.
+5. Start containers via Docker compose
+ * For CPU only deployment
+ ```
+ docker compose -f ./compose_cpu.yaml up -d
+ ```
+ * For CUDA deployment
+ ```
+ docker compose -f ./compose_cpu.yaml up -d
+ ```
-**Setup**
+## Local All-in-one Deployment
+
+Deploys node-server, whisper-service, and ScribeAR frontend to be running on the same system.
1. Install Node 20, Python 3, and Google Chrome using official methods
* https://nodejs.org/en/download
diff --git a/compose_cpu.yaml b/compose_cpu.yaml
new file mode 100644
index 0000000..a64f373
--- /dev/null
+++ b/compose_cpu.yaml
@@ -0,0 +1,41 @@
+name: 'scribear-server'
+services:
+ node-server:
+ build:
+ context: ./node-server
+ dockerfile: Dockerfile
+ environment:
+ - NODE_ENV=${NODE_ENV}
+ - LOG_LEVEL=${LOG_LEVEL}
+ - HOST=0.0.0.0
+ - PORT=8080
+ - USE_HTTPS=${USE_HTTPS}
+ - CORS_ORIGIN=${CORS_ORIGIN}
+ - SERVER_ADDRESS=${SERVER_ADDRESS}
+ - WHISPER_SERVICE_ENDPOINT=ws://whisper-service:8000/whisper?api_key=${API_KEY}&model_key=${MODEL_KEY}
+ - WHISPER_RECONNECT_INTERVAL_SEC=${WHISPER_RECONNECT_INTERVAL_SEC}
+ - REQUIRE_AUTH=${REQUIRE_AUTH}
+ - ACCESS_TOKEN_BYTES=${ACCESS_TOKEN_BYTES}
+ - ACCESS_TOKEN_REFRESH_INTERVAL_SEC=${ACCESS_TOKEN_REFRESH_INTERVAL_SEC}
+ - ACCESS_TOKEN_VALID_PERIOD_SEC=${ACCESS_TOKEN_VALID_PERIOD_SEC}
+ - SESSION_TOKEN_BYTES=${SESSION_TOKEN_BYTES}
+ - SESSION_LENGTH_SEC=${SESSION_LENGTH_SEC}
+ ports:
+ - ${PORT}:8080
+ volumes:
+ - ${KEY_FILEPATH:-/dev/null}:/app/cert/key.pem
+ - ${CERTIFICATE_FILEPATH:-/dev/null}:/app/cert/key.pem
+ restart: unless-stopped
+
+ whisper-service:
+ build:
+ context: ./whisper-service
+ dockerfile: Dockerfile_CPU
+ environment:
+ - LOG_LEVEL=${LOG_LEVEL}
+ - API_KEY=${API_KEY}
+ - HOST=0.0.0.0
+ - PORT=8000
+ expose:
+ - 8000
+ restart: unless-stopped
\ No newline at end of file
diff --git a/compose_cuda.yaml b/compose_cuda.yaml
new file mode 100644
index 0000000..462918f
--- /dev/null
+++ b/compose_cuda.yaml
@@ -0,0 +1,41 @@
+name: 'scribear-server'
+services:
+ node-server:
+ build:
+ context: ./node-server
+ dockerfile: Dockerfile
+ environment:
+ - NODE_ENV=${NODE_ENV}
+ - LOG_LEVEL=${LOG_LEVEL}
+ - HOST=0.0.0.0
+ - PORT=8080
+ - USE_HTTPS=${USE_HTTPS}
+ - CORS_ORIGIN=${CORS_ORIGIN}
+ - SERVER_ADDRESS=${SERVER_ADDRESS}
+ - WHISPER_SERVICE_ENDPOINT=ws://whisper-service:8000/whisper?api_key=${API_KEY}&model_key=${MODEL_KEY}
+ - WHISPER_RECONNECT_INTERVAL_SEC=${WHISPER_RECONNECT_INTERVAL_SEC}
+ - REQUIRE_AUTH=${REQUIRE_AUTH}
+ - ACCESS_TOKEN_BYTES=${ACCESS_TOKEN_BYTES}
+ - ACCESS_TOKEN_REFRESH_INTERVAL_SEC=${ACCESS_TOKEN_REFRESH_INTERVAL_SEC}
+ - ACCESS_TOKEN_VALID_PERIOD_SEC=${ACCESS_TOKEN_VALID_PERIOD_SEC}
+ - SESSION_TOKEN_BYTES=${SESSION_TOKEN_BYTES}
+ - SESSION_LENGTH_SEC=${SESSION_LENGTH_SEC}
+ ports:
+ - ${PORT}:8080
+ volumes:
+ - ${KEY_FILEPATH:-/dev/null}:/app/cert/key.pem
+ - ${CERTIFICATE_FILEPATH:-/dev/null}:/app/cert/key.pem
+ restart: unless-stopped
+
+ whisper-service:
+ build:
+ context: ./whisper-service
+ dockerfile: Dockerfile_CUDA
+ environment:
+ - LOG_LEVEL=${LOG_LEVEL}
+ - API_KEY=${API_KEY}
+ - HOST=0.0.0.0
+ - PORT=8000
+ expose:
+ - 8000
+ restart: unless-stopped
\ No newline at end of file
diff --git a/node-server/Dockerfile b/node-server/Dockerfile
index 6dfc50f..51bb7bd 100644
--- a/node-server/Dockerfile
+++ b/node-server/Dockerfile
@@ -9,8 +9,6 @@ RUN npm run build
WORKDIR /app/build/src
-ENV NODE_ENV="production"
-ENV LOG_DIR="/app/logs"
ENV KEY_FILEPATH="/app/cert/key.pem"
ENV CERTIFICATE_FILEPATH="/app/cert/cert.pem"
diff --git a/node-server/README.md b/node-server/README.md
index b54b3f8..c5d0830 100644
--- a/node-server/README.md
+++ b/node-server/README.md
@@ -99,18 +99,18 @@ The following options can be configured via environment variable.
| `CORS_ORIGIN` | `string` | `*` | Cors origin configuration for node server. |
| `SERVER_ADDRESS` | `string` | `127.0.0.1:8080` | Address the node server is reachable at. Used for ScribeAR QR code to allow other device to connect. |
| `USE_HTTPS` | `boolean` | `false` | Whether to use HTTPS or not |
-| `KEY_FILEPATH` | `string` | Empty string | File path to the private key file for use in when `USE_HTTPS` is set to `true` |
-| `CERTIFICATE_FILEPATH` | `string` | Empty string | File path to the certificate file for use in when `USE_HTTPS` is set to `true` |
+| `KEY_FILEPATH` | `string` | Required if `USE_HTTPS=true`, no default value | File path to the private key file for use in when `USE_HTTPS` is set to `true` |
+| `CERTIFICATE_FILEPATH` | `string` | Required if `USE_HTTPS=true`, no default value | File path to the certificate file for use in when `USE_HTTPS` is set to `true` |
|**Whisper Service Options**|||
| `WHISPER_SERVICE_ENDPOINT` | `websocket address` | Required, no default value | Websocket address for whisper service endpoint. Should be in the format:
`ws://{ADDRESS}:{PORT}/whisper?api_key={API_KEY}&model_key={MODEL_KEY}`
ADDRESS is the address or ip of the whisper service. For an all-in-one deployment, this is `127.0.0.1` (localhost).
`PORT` is the port the whisper service is listening on. This should match what the whisper service is configured to use.
`API_KEY` is the api key for the whisper service. This should match what the whisper service is configured to use.
`MODEL_KEY` is the model key of the model that the whisper service should run. See [Model Implementations and Model Keys](../whisper-service/README.md#model-implementations-and-model-keys) for more information. |
| `WHISPER_RECONNECT_INTERVAL` | `number` | `1` | Number of seconds to wait before attempting to reconnect to whisper service. Server implements exponential backoff, so interval will double each time connection fails up to a maximum of 30 seconds. |
|**Authentication Options**|||
| `REQUIRE_AUTH` | `true`, `false` | `true` | If `true`, requires authentication to connect to node server api, otherwise no authentication is used. See [Authentication](#authentication) for details. |
-| `ACCESS_TOKEN_REFRESH_INTERVAL_SEC` | `number` | `150` | Number of seconds to wait before generating a new refresh token. See [Authentication](#authentication) for details. |
-| `ACCESS_TOKEN_BYTES` | `number` | `32` | The number of random bytes used to generate access tokens |
-| `ACCESS_TOKEN_VALID_PERIOD_SEC` | `number` | `300` | Number of seconds a newly generated refresh token is valid for. See [Authentication](#authentication) for details. |
-| `SESSION_TOKEN_BYTES` | `number` | `8` | The number of random bytes used to generate session tokens |
-| `SESSION_LENGTH_SEC` | `number` | `5400` | Number of seconds a newly generated session token is valid for. See [Authentication](#authentication) for details. |
+| `ACCESS_TOKEN_REFRESH_INTERVAL_SEC` | `number` | Required if `REQUIRED_AUTH=true`, no default value | Number of seconds to wait before generating a new refresh token. See [Authentication](#authentication) for details. |
+| `ACCESS_TOKEN_BYTES` | `number` | Required if `REQUIRED_AUTH=true`, no default value | The number of random bytes used to generate access tokens |
+| `ACCESS_TOKEN_VALID_PERIOD_SEC` | `number` | Required if `REQUIRED_AUTH=true`, no default value | Number of seconds a newly generated refresh token is valid for. See [Authentication](#authentication) for details. |
+| `SESSION_TOKEN_BYTES` | `number` | Required if `REQUIRED_AUTH=true`, no default value | The number of random bytes used to generate session tokens |
+| `SESSION_LENGTH_SEC` | `number` | Required if `REQUIRED_AUTH=true`, no default value | Number of seconds a newly generated session token is valid for. See [Authentication](#authentication) for details. |
## Authentication
diff --git a/node-server/src/shared/config/config_schema.ts b/node-server/src/shared/config/config_schema.ts
index fe9485a..829f78b 100644
--- a/node-server/src/shared/config/config_schema.ts
+++ b/node-server/src/shared/config/config_schema.ts
@@ -31,8 +31,6 @@ const SERVER_CONFIG = Type.Object({
const HTTPS_CONFIG = Type.Union([
Type.Object({
USE_HTTPS: Type.Literal(false),
- KEY_FILEPATH: Type.Any(),
- CERTIFICATE_FILEPATH: Type.Any(),
}),
Type.Object({
USE_HTTPS: Type.Literal(true),
@@ -49,11 +47,6 @@ const WHISPER_CONFIG = Type.Object({
const AUTH_CONFIG = Type.Union([
Type.Object({
REQUIRE_AUTH: Type.Const(false),
- ACCESS_TOKEN_BYTES: Type.Any(),
- ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Any(),
- ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Any(),
- SESSION_TOKEN_BYTES: Type.Any(),
- SESSION_LENGTH_SEC: Type.Any(),
}),
Type.Object({
REQUIRE_AUTH: Type.Const(true),
diff --git a/node-server/src/shared/config/load_config.ts b/node-server/src/shared/config/load_config.ts
index 47e9720..842b825 100644
--- a/node-server/src/shared/config/load_config.ts
+++ b/node-server/src/shared/config/load_config.ts
@@ -1,4 +1,5 @@
import {Ajv} from 'ajv';
+import {Value} from '@sinclair/typebox/value';
import type {Static} from '@sinclair/typebox';
import {NodeEnv, SCHEMA, type ConfigType} from './config_schema.js';
import {configDotenv} from 'dotenv';
@@ -20,7 +21,8 @@ export default function loadConfig(path?: string): ConfigType {
allowUnionTypes: true,
});
- const env = Object.assign({}, process.env) as unknown as Static;
+ const defaults = Value.Default(SCHEMA, {}) as Static;
+ const env = Object.assign(defaults, process.env);
const valid = ajv.validate(SCHEMA, env);
if (!valid) {
const error = new Error('Invalid Configuration! ' + ajv.errorsText());
@@ -57,7 +59,7 @@ export default function loadConfig(path?: string): ConfigType {
sessionTokenBytes: env.SESSION_TOKEN_BYTES,
sessionLengthSec: env.SESSION_LENGTH_SEC,
},
- });
+ }) as unknown as ConfigType;
return config;
}
diff --git a/node-server/template.env b/node-server/template.env
index b6aa25f..d012af3 100644
--- a/node-server/template.env
+++ b/node-server/template.env
@@ -4,12 +4,12 @@ LOG_LEVEL="debug"
#### Host and port API webserver should listen on
HOST="0.0.0.0"
PORT=8080
+CORS_ORIGIN='*'
+SERVER_ADDRESS="127.0.0.1:8080"
# Enable or disable HTTPS
-USE_HTTPS=true
+USE_HTTPS=false
KEY_FILEPATH=''
CERTIFICATE_FILEPATH=''
-CORS_ORIGIN='*'
-SERVER_ADDRESS="127.0.0.1:8080"
#### URL to whisper service
WHISPER_SERVICE_ENDPOINT="ws://127.0.0.1:8000/whisper?api_key=CHANGEME&model_key=faster-whisper:cpu-tiny-en"
From 20c7c75867563886f5a9cd69498935592f54893d Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 07:50:17 -0500
Subject: [PATCH 09/14] make disabled auth actually work
---
.env | 2 +-
node-server/Dockerfile | 5 +++-
.../src/server/routes/websocket_handler.ts | 11 +++++----
.../services/request_authorizer.test.ts | 23 ++++++-------------
.../src/server/services/request_authorizer.ts | 9 ++++++++
whisper-service/Dockerfile_CPU | 4 +++-
6 files changed, 31 insertions(+), 23 deletions(-)
diff --git a/.env b/.env
index 9b85a44..59a3711 100644
--- a/.env
+++ b/.env
@@ -5,7 +5,7 @@ LOG_LEVEL="info"
HOST="0.0.0.0"
PORT=8080
CORS_ORIGIN='*'
-SERVER_ADDRESS="127.0.0.1:8080"
+SERVER_ADDRESS="192.168.10.160:8080"
# Enable or disable HTTPS
USE_HTTPS=false
KEY_FILEPATH=''
diff --git a/node-server/Dockerfile b/node-server/Dockerfile
index 51bb7bd..42fed2b 100644
--- a/node-server/Dockerfile
+++ b/node-server/Dockerfile
@@ -2,9 +2,12 @@ FROM node:20
WORKDIR /app
-COPY . .
+COPY package*.json .
RUN npm install
+
+COPY . .
+
RUN npm run build
WORKDIR /app/build/src
diff --git a/node-server/src/server/routes/websocket_handler.ts b/node-server/src/server/routes/websocket_handler.ts
index 5bdd4cb..821fa13 100644
--- a/node-server/src/server/routes/websocket_handler.ts
+++ b/node-server/src/server/routes/websocket_handler.ts
@@ -74,10 +74,13 @@ export default function websocketHandler(fastify: FastifyInstance) {
registerSink(fastify, ws);
// Close websocket when session expires
- const expirationTimeout = setTimeout(() => {
- fastify.log.info('Session token expired, closing socket.');
- ws.close(3000);
- }, expiration.getTime() - new Date().getTime());
+ let expirationTimeout: NodeJS.Timeout | undefined;
+ if (fastify.config.auth.required) {
+ expirationTimeout = setTimeout(() => {
+ fastify.log.info('Session token expired, closing socket.');
+ ws.close(3000);
+ }, expiration.getTime() - new Date().getTime());
+ }
ws.on('close', code => {
clearTimeout(expirationTimeout);
diff --git a/node-server/src/server/services/request_authorizer.test.ts b/node-server/src/server/services/request_authorizer.test.ts
index 95bfb08..fd1e58c 100644
--- a/node-server/src/server/services/request_authorizer.test.ts
+++ b/node-server/src/server/services/request_authorizer.test.ts
@@ -4,6 +4,8 @@ import fakeLogger from '../../../test/fakes/fake_logger.js';
import type {ConfigType} from '@shared/config/config_schema.js';
import Fastify from 'fastify';
+const MAX_TIMESTAMP = 8640000000000000;
+
describe('Request authorizer', () => {
function setupTest() {
vi.useFakeTimers();
@@ -200,10 +202,7 @@ describe('Request authorizer', () => {
describe('Authorization override', it => {
it('always accepts access tokens', () => {
- const ra = new RequestAuthorizer(
- {auth: {required: false, accessTokenBytes: 8}} as unknown as ConfigType,
- fakeLogger(),
- );
+ const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
const {accessToken} = ra.getAccessToken();
@@ -212,22 +211,17 @@ describe('Request authorizer', () => {
});
it('always accepts session tokens', () => {
- const ra = new RequestAuthorizer(
- {auth: {required: false, accessTokenBytes: 8, sessionTokenBytes: 32}} as unknown as ConfigType,
- fakeLogger(),
- );
+ const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
const {sessionToken} = ra.createSessionToken();
expect(ra.accessTokenIsValid(sessionToken)).toBeTruthy();
expect(ra.sessionTokenIsValid('invalid')).toBeTruthy();
+ expect(ra.getSessionTokenExpiry(sessionToken)).toEqual(new Date(MAX_TIMESTAMP));
});
it('overrides localhost authorizer', async () => {
- const ra = new RequestAuthorizer(
- {auth: {required: false, accessTokenBytes: 8}} as unknown as ConfigType,
- fakeLogger(),
- );
+ const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
const fastify = Fastify();
fastify.decorate('requestAuthorizer', ra);
@@ -240,10 +234,7 @@ describe('Request authorizer', () => {
});
it('overrides session token authorizer', async () => {
- const ra = new RequestAuthorizer(
- {auth: {required: false, accessTokenBytes: 8, sessionTokenBytes: 32}} as unknown as ConfigType,
- fakeLogger(),
- );
+ const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
const fastify = Fastify();
fastify.decorate('requestAuthorizer', ra);
diff --git a/node-server/src/server/services/request_authorizer.ts b/node-server/src/server/services/request_authorizer.ts
index 85976d8..da44cc6 100644
--- a/node-server/src/server/services/request_authorizer.ts
+++ b/node-server/src/server/services/request_authorizer.ts
@@ -69,6 +69,13 @@ export default class RequestAuthorizer {
* @returns object containg access token and expiry date
*/
getAccessToken() {
+ if (!this._config.auth.required) {
+ return {
+ accessToken: this._currentAccessToken,
+ expires: new Date(MAX_TIMESTAMP),
+ };
+ }
+
return {
accessToken: this._currentAccessToken,
expires: this._validAccessTokens[this._currentAccessToken],
@@ -100,6 +107,8 @@ export default class RequestAuthorizer {
* @returns expiry date or undefined if no valid session token found
*/
getSessionTokenExpiry(sessionToken: string | undefined) {
+ if (!this._config.auth.required) return new Date(MAX_TIMESTAMP);
+
if (typeof sessionToken !== 'string' || !(sessionToken in this._validSessionTokens)) return undefined;
return this._validSessionTokens[sessionToken];
}
diff --git a/whisper-service/Dockerfile_CPU b/whisper-service/Dockerfile_CPU
index aba2801..4277ece 100644
--- a/whisper-service/Dockerfile_CPU
+++ b/whisper-service/Dockerfile_CPU
@@ -2,8 +2,10 @@ FROM python:3.12
WORKDIR /app
-COPY . .
+COPY requirements.txt .
RUN pip install -r requirements.txt
+COPY . .
+
CMD ["python", "index.py"]
\ No newline at end of file
From d4f8ff41b583d06cab951323195b349eca001a29 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 08:05:49 -0500
Subject: [PATCH 10/14] add docker build actions
---
.github/workflows/node-server-ci.yml | 16 +++++++++++
.github/workflows/whisper-service-ci.yml | 34 ++++++++++++++++++++++++
2 files changed, 50 insertions(+)
diff --git a/.github/workflows/node-server-ci.yml b/.github/workflows/node-server-ci.yml
index b8d8a26..8eaf9ad 100644
--- a/.github/workflows/node-server-ci.yml
+++ b/.github/workflows/node-server-ci.yml
@@ -73,3 +73,19 @@ jobs:
- name: Run build
working-directory: 'node-server'
run: npm run build
+
+ build-container-node-server:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: ./node-server
+ file: Dockerfile
+ push: false
diff --git a/.github/workflows/whisper-service-ci.yml b/.github/workflows/whisper-service-ci.yml
index d6d7ce0..d42e8fa 100644
--- a/.github/workflows/whisper-service-ci.yml
+++ b/.github/workflows/whisper-service-ci.yml
@@ -54,3 +54,37 @@ jobs:
- name: Run tests
working-directory: "whisper-service"
run: pylint --disable=import-error $(git ls-files '*.py')
+
+
+ build-cpu-container-whisper-service:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: ./whisper-service
+ file: Dockerfile_CPU
+ push: false
+
+ build-cuda-container-whisper-service:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Build and push
+ uses: docker/build-push-action@v6
+ with:
+ context: ./whisper-service
+ file: Dockerfile_CUDA
+ push: false
+
\ No newline at end of file
From 151c7825beed288d0f97f18f15016515bafe6368 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 08:08:43 -0500
Subject: [PATCH 11/14] actually pull repo
---
.github/workflows/node-server-ci.yml | 21 ++++++++++++---------
.github/workflows/whisper-service-ci.yml | 8 ++++++--
2 files changed, 18 insertions(+), 11 deletions(-)
diff --git a/.github/workflows/node-server-ci.yml b/.github/workflows/node-server-ci.yml
index 8eaf9ad..8e20765 100644
--- a/.github/workflows/node-server-ci.yml
+++ b/.github/workflows/node-server-ci.yml
@@ -29,13 +29,13 @@ jobs:
node-version: ${{ env.node-version }}
- name: Install dependencies
- working-directory: 'node-server'
+ working-directory: "node-server"
run: npm ci
- name: Run tests
- working-directory: 'node-server'
+ working-directory: "node-server"
run: npm run ci-test
-
+
lint-node-server:
runs-on: ubuntu-latest
steps:
@@ -48,11 +48,11 @@ jobs:
node-version: ${{ env.node-version }}
- name: Install dependencies
- working-directory: 'node-server'
- run: npm ci
+ working-directory: "node-server"
+ run: npm ci
- name: Run linter
- working-directory: 'node-server'
+ working-directory: "node-server"
run: npm run lint
build-node-server:
@@ -67,16 +67,19 @@ jobs:
node-version: ${{ env.node-version }}
- name: Install dependencies
- working-directory: 'node-server'
+ working-directory: "node-server"
run: npm ci
-
+
- name: Run build
- working-directory: 'node-server'
+ working-directory: "node-server"
run: npm run build
build-container-node-server:
runs-on: ubuntu-latest
steps:
+ - name: Set up Git repository
+ uses: actions/checkout@v4
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
diff --git a/.github/workflows/whisper-service-ci.yml b/.github/workflows/whisper-service-ci.yml
index d42e8fa..f145d40 100644
--- a/.github/workflows/whisper-service-ci.yml
+++ b/.github/workflows/whisper-service-ci.yml
@@ -55,10 +55,12 @@ jobs:
working-directory: "whisper-service"
run: pylint --disable=import-error $(git ls-files '*.py')
-
build-cpu-container-whisper-service:
runs-on: ubuntu-latest
steps:
+ - name: Set up Git repository
+ uses: actions/checkout@v4
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -75,6 +77,9 @@ jobs:
build-cuda-container-whisper-service:
runs-on: ubuntu-latest
steps:
+ - name: Set up Git repository
+ uses: actions/checkout@v4
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -87,4 +92,3 @@ jobs:
context: ./whisper-service
file: Dockerfile_CUDA
push: false
-
\ No newline at end of file
From 9ff1f666590d986ec53b4ddb383df2275d46c4f7 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 08:10:23 -0500
Subject: [PATCH 12/14] use correct? dockerfile path
---
.github/workflows/node-server-ci.yml | 2 +-
.github/workflows/whisper-service-ci.yml | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/node-server-ci.yml b/.github/workflows/node-server-ci.yml
index 8e20765..428d4f2 100644
--- a/.github/workflows/node-server-ci.yml
+++ b/.github/workflows/node-server-ci.yml
@@ -90,5 +90,5 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ./node-server
- file: Dockerfile
+ file: ./whisper-service/Dockerfile
push: false
diff --git a/.github/workflows/whisper-service-ci.yml b/.github/workflows/whisper-service-ci.yml
index f145d40..1628797 100644
--- a/.github/workflows/whisper-service-ci.yml
+++ b/.github/workflows/whisper-service-ci.yml
@@ -71,7 +71,7 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ./whisper-service
- file: Dockerfile_CPU
+ file: ./whisper-service/Dockerfile_CPU
push: false
build-cuda-container-whisper-service:
@@ -90,5 +90,5 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ./whisper-service
- file: Dockerfile_CUDA
+ file: ./whisper-service/Dockerfile_CUDA
push: false
From 6d6f3ef94d1e942165eced55d887bb6f97d17fc7 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 08:29:29 -0500
Subject: [PATCH 13/14] add steps to push to docker hub
---
.github/workflows/node-server-ci.yml | 50 +++++++++++++---
.github/workflows/whisper-service-ci.yml | 74 +++++++++++++++++++++---
2 files changed, 108 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/node-server-ci.yml b/.github/workflows/node-server-ci.yml
index 428d4f2..7686315 100644
--- a/.github/workflows/node-server-ci.yml
+++ b/.github/workflows/node-server-ci.yml
@@ -6,6 +6,9 @@ on:
- node-server/**
branches:
- main
+ - staging
+ tags:
+ - "v*.*.*"
pull_request:
workflow_dispatch:
@@ -14,7 +17,8 @@ permissions:
contents: read
env:
- node-version: 20
+ NODE_VERSION: 20
+ DOCKERHUB_ORG: 'scribear'
jobs:
test-node-server:
@@ -23,10 +27,10 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
- - name: Set up Node.js ${{ env.node-version }}
+ - name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
- node-version: ${{ env.node-version }}
+ node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
working-directory: "node-server"
@@ -42,10 +46,10 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
- - name: Set up Node.js ${{ env.node-version }}
+ - name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
- node-version: ${{ env.node-version }}
+ node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
working-directory: "node-server"
@@ -61,10 +65,10 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
- - name: Set up Node.js ${{ env.node-version }}
+ - name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4
with:
- node-version: ${{ env.node-version }}
+ node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
working-directory: "node-server"
@@ -80,15 +84,43 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_ORG }}/node-server
+ tags: |
+ type=schedule
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_PASSWORD }}
+
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./node-server
- file: ./whisper-service/Dockerfile
- push: false
+ file: ./node-server/Dockerfile
+ push: true
+ platforms: "linux/amd64,linux/arm64"
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/node-server:buildcache
+ cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/node-server:buildcache,mode=max
+ build-args: |
+ BRANCH=${{ steps.meta.outputs.version }}
+ BUILDNUMBER=${{ github.run_number }}
+ ARG GITSHA1=${{ github.sha }}
\ No newline at end of file
diff --git a/.github/workflows/whisper-service-ci.yml b/.github/workflows/whisper-service-ci.yml
index 1628797..6d24151 100644
--- a/.github/workflows/whisper-service-ci.yml
+++ b/.github/workflows/whisper-service-ci.yml
@@ -6,6 +6,9 @@ on:
- whisper-service/**
branches:
- main
+ - staging
+ tags:
+ - "v*.*.*"
pull_request:
workflow_dispatch:
@@ -14,7 +17,8 @@ permissions:
contents: read
env:
- python-version: 3.12
+ PYTHON_VERSION: 3.12
+ DOCKERHUB_ORG: 'scribear'
jobs:
test-whisper-service:
@@ -23,10 +27,10 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
- - name: Set up Python ${{ env.python-version }}
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
- python-version: ${{ env.python-version }}
+ python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
working-directory: "whisper-service"
@@ -42,10 +46,10 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
- - name: Set up Python ${{ env.python-version }}
+ - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
- python-version: ${{ env.python-version }}
+ python-version: ${{ env.PYTHON_VERSION }}
- name: Install dependencies
working-directory: "whisper-service"
@@ -61,18 +65,46 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_ORG }}/whisper-service-cpu
+ tags: |
+ type=schedule
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_PASSWORD }}
+
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./whisper-service
file: ./whisper-service/Dockerfile_CPU
- push: false
+ push: true
+ platforms: "linux/amd64,linux/arm64"
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cpu:buildcache
+ cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cpu:buildcache,mode=max
+ build-args: |
+ BRANCH=${{ steps.meta.outputs.version }}
+ BUILDNUMBER=${{ github.run_number }}
+ ARG GITSHA1=${{ github.sha }}
build-cuda-container-whisper-service:
runs-on: ubuntu-latest
@@ -80,15 +112,43 @@ jobs:
- name: Set up Git repository
uses: actions/checkout@v4
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_ORG }}/whisper-service-cuda
+ tags: |
+ type=schedule
+ type=ref,event=branch
+ type=ref,event=pr
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+ type=semver,pattern={{major}}
+
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
+ - name: Login to DockerHub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_PASSWORD }}
+
- name: Build and push
uses: docker/build-push-action@v6
with:
context: ./whisper-service
file: ./whisper-service/Dockerfile_CUDA
- push: false
+ push: true
+ platforms: "linux/amd64,linux/arm64"
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+ cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cuda:buildcache
+ cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/whisper-service-cuda:buildcache,mode=max
+ build-args: |
+ BRANCH=${{ steps.meta.outputs.version }}
+ BUILDNUMBER=${{ github.run_number }}
+ ARG GITSHA1=${{ github.sha }}
From 1add683e4465a48cf2fd7c72137ff8008c140212 Mon Sep 17 00:00:00 2001
From: Bennett Wu <57691028+bennettrwu@users.noreply.github.com>
Date: Wed, 30 Apr 2025 10:39:10 -0500
Subject: [PATCH 14/14] implement sourceTokens
---
.gitignore | 3 +-
compose_cpu.yaml | 1 +
compose_cuda.yaml | 1 +
node-server/README.md | 3 +-
.../hooks/create_authorize_hook.test.ts | 262 ++++++++++++++++++
.../src/server/hooks/create_authorize_hook.ts | 63 +++++
.../src/server/routes/session_auth_handler.ts | 45 +--
.../src/server/routes/websocket_handler.ts | 87 +++---
...thorizer.test.ts => token_service.test.ts} | 100 +------
...request_authorizer.ts => token_service.ts} | 43 +--
node-server/src/server/start_server.ts | 10 +-
.../src/shared/config/config_schema.ts | 2 +
node-server/src/shared/config/load_config.ts | 1 +
node-server/template.env | 8 +-
.env => template.env | 2 +
15 files changed, 441 insertions(+), 190 deletions(-)
create mode 100644 node-server/src/server/hooks/create_authorize_hook.test.ts
create mode 100644 node-server/src/server/hooks/create_authorize_hook.ts
rename node-server/src/server/services/{request_authorizer.test.ts => token_service.test.ts} (60%)
rename node-server/src/server/services/{request_authorizer.ts => token_service.ts} (77%)
rename .env => template.env (92%)
diff --git a/.gitignore b/.gitignore
index cd3d225..51a7fbb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
-logs
\ No newline at end of file
+logs
+.env
\ No newline at end of file
diff --git a/compose_cpu.yaml b/compose_cpu.yaml
index a64f373..600b6b3 100644
--- a/compose_cpu.yaml
+++ b/compose_cpu.yaml
@@ -15,6 +15,7 @@ services:
- WHISPER_SERVICE_ENDPOINT=ws://whisper-service:8000/whisper?api_key=${API_KEY}&model_key=${MODEL_KEY}
- WHISPER_RECONNECT_INTERVAL_SEC=${WHISPER_RECONNECT_INTERVAL_SEC}
- REQUIRE_AUTH=${REQUIRE_AUTH}
+ - SOURCE_TOKEN=${SOURCE_TOKEN}
- ACCESS_TOKEN_BYTES=${ACCESS_TOKEN_BYTES}
- ACCESS_TOKEN_REFRESH_INTERVAL_SEC=${ACCESS_TOKEN_REFRESH_INTERVAL_SEC}
- ACCESS_TOKEN_VALID_PERIOD_SEC=${ACCESS_TOKEN_VALID_PERIOD_SEC}
diff --git a/compose_cuda.yaml b/compose_cuda.yaml
index 462918f..3864003 100644
--- a/compose_cuda.yaml
+++ b/compose_cuda.yaml
@@ -15,6 +15,7 @@ services:
- WHISPER_SERVICE_ENDPOINT=ws://whisper-service:8000/whisper?api_key=${API_KEY}&model_key=${MODEL_KEY}
- WHISPER_RECONNECT_INTERVAL_SEC=${WHISPER_RECONNECT_INTERVAL_SEC}
- REQUIRE_AUTH=${REQUIRE_AUTH}
+ - SOURCE_TOKEN=${SOURCE_TOKEN}
- ACCESS_TOKEN_BYTES=${ACCESS_TOKEN_BYTES}
- ACCESS_TOKEN_REFRESH_INTERVAL_SEC=${ACCESS_TOKEN_REFRESH_INTERVAL_SEC}
- ACCESS_TOKEN_VALID_PERIOD_SEC=${ACCESS_TOKEN_VALID_PERIOD_SEC}
diff --git a/node-server/README.md b/node-server/README.md
index c5d0830..a7a852b 100644
--- a/node-server/README.md
+++ b/node-server/README.md
@@ -106,7 +106,8 @@ The following options can be configured via environment variable.
| `WHISPER_RECONNECT_INTERVAL` | `number` | `1` | Number of seconds to wait before attempting to reconnect to whisper service. Server implements exponential backoff, so interval will double each time connection fails up to a maximum of 30 seconds. |
|**Authentication Options**|||
| `REQUIRE_AUTH` | `true`, `false` | `true` | If `true`, requires authentication to connect to node server api, otherwise no authentication is used. See [Authentication](#authentication) for details. |
-| `ACCESS_TOKEN_REFRESH_INTERVAL_SEC` | `number` | Required if `REQUIRED_AUTH=true`, no default value | Number of seconds to wait before generating a new refresh token. See [Authentication](#authentication) for details. |
+| `SOURCE_TOKEN` | `string` | Required if `REQUIRED_AUTH=true`, no default value | The key used by frontend to connect as audio source. See [Authentication](#authentication) for details. |
+| `ACCESS_TOKEN_REFRESH_INTERVAL_SEC` | `number` | Required if `REQUIRED_AUTH=true`, no default value | Number of seconds to wait before generating a new refresh token. See [Authentication](#authentication) for details. |
| `ACCESS_TOKEN_BYTES` | `number` | Required if `REQUIRED_AUTH=true`, no default value | The number of random bytes used to generate access tokens |
| `ACCESS_TOKEN_VALID_PERIOD_SEC` | `number` | Required if `REQUIRED_AUTH=true`, no default value | Number of seconds a newly generated refresh token is valid for. See [Authentication](#authentication) for details. |
| `SESSION_TOKEN_BYTES` | `number` | Required if `REQUIRED_AUTH=true`, no default value | The number of random bytes used to generate session tokens |
diff --git a/node-server/src/server/hooks/create_authorize_hook.test.ts b/node-server/src/server/hooks/create_authorize_hook.test.ts
new file mode 100644
index 0000000..57fca9e
--- /dev/null
+++ b/node-server/src/server/hooks/create_authorize_hook.test.ts
@@ -0,0 +1,262 @@
+import type TokenService from '@server/services/token_service.js';
+import type {ConfigType} from '@shared/config/config_schema.js';
+import Fastify from 'fastify';
+import {describe, expect, vi} from 'vitest';
+import createAuthorizeHook, {Identities} from './create_authorize_hook.js';
+import formatTestNames from '@test/utils/format_test_names.js';
+
+describe('createAuthorizeHook', it => {
+ const validAccessToken = 'valid-access-token';
+ const validSessionToken = 'valid-session-token';
+ const validSourceToken = 'valid-source-token';
+
+ const fakeConfig = {
+ auth: {
+ required: true,
+ sourceToken: validSourceToken,
+ },
+ } as ConfigType;
+
+ const fakeTokenService = {
+ accessTokenIsValid: vi.fn(token => {
+ return token === validAccessToken;
+ }),
+ sessionTokenIsValid: vi.fn(token => {
+ return token === validSessionToken;
+ }),
+ getSessionTokenExpiry: vi.fn(() => {
+ return new Date(Date.now() + 10_000);
+ }),
+ } as unknown as TokenService;
+
+ function setupTest(allowedIdentities: Array) {
+ const fastify = Fastify();
+ const reqHandlerSpy = vi.fn((req, reply) => reply.code(200).send('hi'));
+ fastify.post(
+ '/test',
+ {preHandler: createAuthorizeHook(fakeConfig, fakeTokenService, allowedIdentities)},
+ reqHandlerSpy,
+ );
+
+ return {fastify, reqHandlerSpy};
+ }
+
+ it.for(
+ formatTestNames([
+ {name: 'AccessToken', identity: Identities.AccessToken, param: `accessToken=${validAccessToken}`},
+ {name: 'SessionToken', identity: Identities.SessionToken, param: `sessionToken=${validSessionToken}`},
+ {name: 'SourceToken', identity: Identities.SourceToken, param: `sourceToken=${validSourceToken}`},
+ ]),
+ )('authorizes valid tokens in request query string for identity: %s', async ([, {identity, param}]) => {
+ const {fastify, reqHandlerSpy} = setupTest([identity]);
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: `/test?${param}`,
+ });
+
+ expect(reply.statusCode).toBe(200);
+ expect(reqHandlerSpy).toHaveBeenCalled();
+ });
+
+ it.for(
+ formatTestNames([
+ {name: 'AccessToken', identity: Identities.AccessToken, body: {accessToken: validAccessToken}},
+ {name: 'SessionToken', identity: Identities.SessionToken, body: {sessionToken: validSessionToken}},
+ {name: 'SourceToken', identity: Identities.SourceToken, body: {sourceToken: validSourceToken}},
+ ]),
+ )('authorizes valid tokens in request body for identity: %s', async ([, {identity, body}]) => {
+ const {fastify, reqHandlerSpy} = setupTest([identity]);
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: '/test',
+ body,
+ });
+
+ expect(reply.statusCode).toBe(200);
+ expect(reqHandlerSpy).toHaveBeenCalled();
+ });
+
+ it.for(
+ formatTestNames([
+ {name: 'AccessToken', identity: Identities.AccessToken, param: `accessToken=${validSessionToken}`},
+ {name: 'SessionToken', identity: Identities.SessionToken, param: `sessionToken=${validAccessToken}`},
+ {name: 'SourceToken', identity: Identities.SourceToken, param: `sourceToken=${validAccessToken}`},
+ ]),
+ )('rejects invalid tokens in request query string for identity: %s', async ([, {identity, param}]) => {
+ const {fastify, reqHandlerSpy} = setupTest([identity]);
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: `/test?${param}`,
+ });
+
+ expect(reply.statusCode).toBe(403);
+ expect(reqHandlerSpy).not.toHaveBeenCalled();
+ });
+
+ it.for(
+ formatTestNames([
+ {name: 'AccessToken', identity: Identities.AccessToken, body: {accessToken: validSourceToken}},
+ {name: 'SessionToken', identity: Identities.SessionToken, body: {sessionToken: validSourceToken}},
+ {name: 'SourceToken', identity: Identities.SourceToken, body: {sourceToken: validSessionToken}},
+ ]),
+ )('rejects invalid tokens in request body for identity: %s', async ([, {identity, body}]) => {
+ const {fastify, reqHandlerSpy} = setupTest([identity]);
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: '/test',
+ body,
+ });
+
+ expect(reply.statusCode).toBe(403);
+ expect(reqHandlerSpy).not.toHaveBeenCalled();
+ });
+
+ it.for(
+ formatTestNames([
+ {
+ name: 'AccessToken',
+ identity: Identities.AccessToken,
+ param: `accessToken=${validAccessToken}`,
+ body: {accessToken: 'invalid-token'},
+ },
+ {
+ name: 'SessionToken',
+ identity: Identities.SessionToken,
+ param: `sessionToken=${validSessionToken}`,
+ body: {sessionToken: 'invalid-token'},
+ },
+ {
+ name: 'SourceToken',
+ identity: Identities.SourceToken,
+ param: `sourceToken=${validSourceToken}`,
+ body: {sourceToken: 'invalid-token'},
+ },
+ ]),
+ )('prioritizes tokens in request body for identity (invalid): %s', async ([, {identity, param, body}]) => {
+ const {fastify, reqHandlerSpy} = setupTest([identity]);
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: `/test?${param}`,
+ body,
+ });
+
+ expect(reply.statusCode).toBe(403);
+ expect(reqHandlerSpy).not.toHaveBeenCalled();
+ });
+
+ it.for(
+ formatTestNames([
+ {
+ name: 'AccessToken',
+ identity: Identities.AccessToken,
+ param: 'accessToken=invalid-token',
+ body: {accessToken: validAccessToken},
+ },
+ {
+ name: 'SessionToken',
+ identity: Identities.SessionToken,
+ param: 'sessionToken=invalid-token',
+ body: {sessionToken: validSessionToken},
+ },
+ {
+ name: 'SourceToken',
+ identity: Identities.SourceToken,
+ param: 'sourceToken=invalid-token',
+ body: {sourceToken: validSourceToken},
+ },
+ ]),
+ )('prioritizes tokens in request body for identity (valid): %s', async ([, {identity, param, body}]) => {
+ const {fastify, reqHandlerSpy} = setupTest([identity]);
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: `/test?${param}`,
+ body,
+ });
+
+ expect(reply.statusCode).toBe(200);
+ expect(reqHandlerSpy).toHaveBeenCalled();
+ });
+
+ it.for(
+ formatTestNames([
+ {
+ name: 'AccessToken, SessionToken',
+ identities: [Identities.AccessToken, Identities.SessionToken],
+ expected: [true, true, false],
+ },
+ {
+ name: 'AccessToken, SourceToken',
+ identities: [Identities.AccessToken, Identities.SourceToken],
+ expected: [true, false, true],
+ },
+ {
+ name: 'SessionToken, SourceToken',
+ identities: [Identities.SessionToken, Identities.SourceToken],
+ expected: [false, true, true],
+ },
+ {
+ name: 'AccessToken, SessionToken, SourceToken',
+ identities: [Identities.AccessToken, Identities.SessionToken, Identities.SourceToken],
+ expected: [true, true, true],
+ },
+ ]),
+ )('authorizes multiple identities: %s', async ([, {identities, expected}]) => {
+ const {fastify, reqHandlerSpy} = setupTest(identities);
+
+ const toTest = [
+ {accessToken: validAccessToken},
+ {sessionToken: validSessionToken},
+ {sourceToken: validSourceToken},
+ ];
+
+ for (let i = 0; i < toTest.length; i++) {
+ reqHandlerSpy.mockClear();
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: '/test',
+ body: toTest[i],
+ });
+
+ if (expected[i]) {
+ expect(reply.statusCode).toBe(200);
+ expect(reqHandlerSpy).toHaveBeenCalled();
+ } else {
+ expect(reply.statusCode).toBe(403);
+ expect(reqHandlerSpy).not.toHaveBeenCalled();
+ }
+ }
+ });
+
+ it('authorizes if auth is disabled for identity', async () => {
+ const fastify = Fastify();
+ const reqHandlerSpy = vi.fn((req, reply) => reply.code(200).send('hi'));
+ fastify.post(
+ '/test',
+ {preHandler: createAuthorizeHook({auth: {required: false}} as ConfigType, fakeTokenService, [])},
+ reqHandlerSpy,
+ );
+
+ const reply = await fastify.inject({
+ method: 'post',
+ url: '/test',
+ });
+
+ expect(reply.statusCode).toBe(200);
+ expect(reqHandlerSpy).toHaveBeenCalled();
+ });
+
+ it('provides session expiration for sessionTokens', async () => {
+ const {fastify, reqHandlerSpy} = setupTest([Identities.SessionToken]);
+
+ await fastify.inject({method: 'POST', path: '/test', body: {sessionToken: validSessionToken}});
+
+ expect(Math.abs(reqHandlerSpy.mock.calls[0][0]['authorizationExpiryTimeout'] - 10_000)).toBeLessThan(100);
+ });
+});
diff --git a/node-server/src/server/hooks/create_authorize_hook.ts b/node-server/src/server/hooks/create_authorize_hook.ts
new file mode 100644
index 0000000..79bf0fd
--- /dev/null
+++ b/node-server/src/server/hooks/create_authorize_hook.ts
@@ -0,0 +1,63 @@
+import type RequestAuthorizer from '@server/services/token_service.js';
+import type {ConfigType} from '@shared/config/config_schema.js';
+import type {DoneFuncWithErrOrRes, FastifyReply, FastifyRequest} from 'fastify';
+
+export enum Identities {
+ SourceToken,
+ AccessToken,
+ SessionToken,
+}
+
+/**
+ * Creates a fastify preHandler hook to handle authorizing requests
+ * Checks request query string for sessionToken or sourceToken and checks if they are valid
+ * A valid sessionToken corresponds to a TranscriptionSink identity
+ * A valid sourceToken corresponds to a Kiosk identity
+ * If the parsed identity is in allowedIdentities, request is authorized, otherwise it is rejected
+ * @param config server config object
+ * @param requestAuthorizer requestAuthorizer instance
+ * @param allowedIdentities array of identities that should be accepted
+ * @returns fastify preHandler hook
+ */
+export default function createAuthorizeHook(
+ config: ConfigType,
+ requestAuthorizer: RequestAuthorizer,
+ allowedIdentities: Array,
+) {
+ return (
+ request: FastifyRequest<{
+ Querystring: {accessToken?: string; sessionToken?: string; sourceToken?: string};
+ Body?: {accessToken?: string; sessionToken?: string; sourceToken?: string};
+ }>,
+ reply: FastifyReply,
+ done: DoneFuncWithErrOrRes,
+ ) => {
+ if (!config.auth.required) return done();
+
+ const accessToken = request.body?.accessToken ? request.body?.accessToken : request.query.accessToken;
+ if (allowedIdentities.includes(Identities.AccessToken) && requestAuthorizer.accessTokenIsValid(accessToken)) {
+ request.log.debug('Request authorized via access token');
+ return done();
+ }
+
+ const sessionToken = request.body?.sessionToken ? request.body?.sessionToken : request.query.sessionToken;
+ if (allowedIdentities.includes(Identities.SessionToken) && requestAuthorizer.sessionTokenIsValid(sessionToken)) {
+ const expiration = requestAuthorizer.getSessionTokenExpiry(sessionToken);
+ if (expiration) {
+ request.log.debug('Request authorized via session token');
+
+ request.authorizationExpiryTimeout = expiration.getTime() - new Date().getTime();
+ return done();
+ }
+ }
+
+ const sourceToken = request.body?.sourceToken ? request.body?.sourceToken : request.query.sourceToken;
+ if (allowedIdentities.includes(Identities.SourceToken) && sourceToken === config.auth.sourceToken) {
+ request.log.debug('Request authorized via source token');
+ return done();
+ }
+
+ reply.code(403);
+ done(new Error('Unauthorized'));
+ };
+}
diff --git a/node-server/src/server/routes/session_auth_handler.ts b/node-server/src/server/routes/session_auth_handler.ts
index 330cc2b..3e84729 100644
--- a/node-server/src/server/routes/session_auth_handler.ts
+++ b/node-server/src/server/routes/session_auth_handler.ts
@@ -1,28 +1,37 @@
+import createAuthorizeHook, {Identities} from '@server/hooks/create_authorize_hook.js';
import {FastifyInstance} from 'fastify';
/**
* Registers access token and session token endpoints for managing authentication
- * @param fastify
+ * @param fastify fastify webserver instance
*/
export default function sessionAuthHandler(fastify: FastifyInstance) {
- fastify.get('/accessToken', {preHandler: fastify.requestAuthorizer.authorizeLocalhost}, (request, reply) => {
- const {accessToken, expires} = fastify.requestAuthorizer.getAccessToken();
- return reply.send({
- accessToken,
- serverAddress: fastify.config.server.serverAddress,
- expires: expires.toISOString(),
- });
- });
+ fastify.post(
+ '/accessToken',
+ {preHandler: createAuthorizeHook(fastify.config, fastify.tokenService, [Identities.SourceToken])},
+ (request, reply) => {
+ const {accessToken, expires} = fastify.tokenService.getAccessToken();
+ return reply.send({
+ accessToken,
+ serverAddress: fastify.config.server.serverAddress,
+ expires: expires.toISOString(),
+ });
+ },
+ );
- fastify.post('/startSession', (request, reply) => {
- if (typeof request.body !== 'object') return reply.code(400).send();
- const {accessToken} = request.body as {accessToken?: string};
+ fastify.post(
+ '/startSession',
+ {preHandler: createAuthorizeHook(fastify.config, fastify.tokenService, [Identities.AccessToken])},
+ (request, reply) => {
+ if (typeof request.body !== 'object') return reply.code(400).send();
+ const {accessToken} = request.body as {accessToken?: string};
- if (!fastify.requestAuthorizer.accessTokenIsValid(accessToken)) {
- return reply.code(401).send();
- }
+ if (!fastify.tokenService.accessTokenIsValid(accessToken)) {
+ return reply.code(401).send();
+ }
- const {sessionToken, expires} = fastify.requestAuthorizer.createSessionToken();
- return reply.send({sessionToken, expires: expires.toISOString()});
- });
+ const {sessionToken, expires} = fastify.tokenService.createSessionToken();
+ return reply.send({sessionToken, expires: expires.toISOString()});
+ },
+ );
}
diff --git a/node-server/src/server/routes/websocket_handler.ts b/node-server/src/server/routes/websocket_handler.ts
index 821fa13..cd966c8 100644
--- a/node-server/src/server/routes/websocket_handler.ts
+++ b/node-server/src/server/routes/websocket_handler.ts
@@ -1,3 +1,4 @@
+import createAuthorizeHook, {Identities} from '@server/hooks/create_authorize_hook.js';
import type {BackendTranscriptBlock} from '@server/services/transcription_engine.js';
import {FastifyInstance} from 'fastify';
import WebSocket from 'ws';
@@ -47,44 +48,62 @@ function registerSource(fastify: FastifyInstance, ws: WebSocket) {
* @param fastify
*/
export default function websocketHandler(fastify: FastifyInstance) {
- fastify.get('/sourcesink', {websocket: true, preHandler: fastify.requestAuthorizer.authorizeLocalhost}, (ws, req) => {
- registerSink(fastify, ws);
- registerSource(fastify, ws);
+ fastify.get(
+ '/sourcesink',
+ {
+ websocket: true,
+ preHandler: createAuthorizeHook(fastify.config, fastify.tokenService, [Identities.SourceToken]),
+ },
+ (ws, req) => {
+ registerSink(fastify, ws);
+ registerSource(fastify, ws);
- ws.on('close', code => {
- req.log.info({msg: 'Websocket closed', code});
- });
- });
-
- fastify.get('/source', {websocket: true, preHandler: fastify.requestAuthorizer.authorizeLocalhost}, (ws, req) => {
- registerSource(fastify, ws);
+ ws.on('close', code => {
+ req.log.info({msg: 'Websocket closed', code});
+ });
+ },
+ );
- ws.on('close', code => {
- req.log.info({msg: 'Websocket closed', code});
- });
- });
+ fastify.get(
+ '/source',
+ {
+ websocket: true,
+ preHandler: createAuthorizeHook(fastify.config, fastify.tokenService, [Identities.SourceToken]),
+ },
+ (ws, req) => {
+ registerSource(fastify, ws);
- fastify.get('/sink', {websocket: true, preHandler: fastify.requestAuthorizer.authorizeSessionToken}, (ws, req) => {
- const expiration = fastify.requestAuthorizer.getSessionTokenExpiry(req.query.sessionToken);
- if (expiration === undefined) {
- ws.close();
- return;
- }
+ ws.on('close', code => {
+ req.log.info({msg: 'Websocket closed', code});
+ });
+ },
+ );
- registerSink(fastify, ws);
+ fastify.get(
+ '/sink',
+ {
+ websocket: true,
+ preHandler: createAuthorizeHook(fastify.config, fastify.tokenService, [
+ Identities.SourceToken,
+ Identities.SessionToken,
+ ]),
+ },
+ (ws, req) => {
+ registerSink(fastify, ws);
- // Close websocket when session expires
- let expirationTimeout: NodeJS.Timeout | undefined;
- if (fastify.config.auth.required) {
- expirationTimeout = setTimeout(() => {
- fastify.log.info('Session token expired, closing socket.');
- ws.close(3000);
- }, expiration.getTime() - new Date().getTime());
- }
+ // Close websocket when session expires
+ let expirationTimeout: NodeJS.Timeout | undefined;
+ if (req.authorizationExpiryTimeout) {
+ expirationTimeout = setTimeout(() => {
+ fastify.log.info('Session token expired, closing socket.');
+ ws.close(3000);
+ }, req.authorizationExpiryTimeout);
+ }
- ws.on('close', code => {
- clearTimeout(expirationTimeout);
- req.log.info({msg: 'Websocket closed', code});
- });
- });
+ ws.on('close', code => {
+ clearTimeout(expirationTimeout);
+ req.log.info({msg: 'Websocket closed', code});
+ });
+ },
+ );
}
diff --git a/node-server/src/server/services/request_authorizer.test.ts b/node-server/src/server/services/token_service.test.ts
similarity index 60%
rename from node-server/src/server/services/request_authorizer.test.ts
rename to node-server/src/server/services/token_service.test.ts
index fd1e58c..1f2c8ea 100644
--- a/node-server/src/server/services/request_authorizer.test.ts
+++ b/node-server/src/server/services/token_service.test.ts
@@ -1,12 +1,11 @@
import {describe, expect, vi} from 'vitest';
-import RequestAuthorizer from './request_authorizer.js';
+import RequestAuthorizer from './token_service.js';
import fakeLogger from '../../../test/fakes/fake_logger.js';
import type {ConfigType} from '@shared/config/config_schema.js';
-import Fastify from 'fastify';
const MAX_TIMESTAMP = 8640000000000000;
-describe('Request authorizer', () => {
+describe('Token service', () => {
function setupTest() {
vi.useFakeTimers();
vi.setSystemTime(new Date(0));
@@ -136,71 +135,15 @@ describe('Request authorizer', () => {
});
});
- describe('Fastify authentication middleware', it => {
- function setupFastify() {
- const ra = setupTest();
- vi.useRealTimers();
-
- const fastify = Fastify();
- fastify.decorate('requestAuthorizer', ra);
-
- fastify.get('/localhost', {preHandler: fastify.requestAuthorizer.authorizeLocalhost}, (req, reply) => {
- return reply.code(200).send();
- });
- fastify.get('/sessionToken', {preHandler: fastify.requestAuthorizer.authorizeSessionToken}, (req, reply) => {
- return reply.code(200).send();
- });
-
- return fastify;
- }
-
- it('localhost authorizer accepts localhost request', async () => {
- const fastify = setupFastify();
-
- const result = await fastify.inject({method: 'GET', url: '/localhost', remoteAddress: '127.0.0.1'});
-
- expect(result.statusCode).toBe(200);
- });
-
- it('localhost authorizer rejects non localhost request', async () => {
- const fastify = setupFastify();
-
- const result = await fastify.inject({method: 'GET', url: '/localhost', remoteAddress: '192.168.0.1'});
-
- expect(result.statusCode).toBe(403);
- });
-
- it('session token authorizer accepts localhost request', async () => {
- const fastify = setupFastify();
-
- const result = await fastify.inject({method: 'GET', url: '/sessionToken', remoteAddress: '127.0.0.1'});
-
- expect(result.statusCode).toBe(200);
- });
-
- it('session token authorizer rejects non localhost request without session token', async () => {
- const fastify = setupFastify();
-
- const result = await fastify.inject({method: 'GET', url: '/sessionToken', remoteAddress: '192.168.0.1'});
-
- expect(result.statusCode).toBe(403);
- });
-
- it('session token authorizer accepts non localhost request with valid session token', async () => {
- const fastify = setupFastify();
+ describe('Authorization override', it => {
+ it('generates access tokens with max expiry', () => {
+ const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
- const {sessionToken} = fastify.requestAuthorizer.createSessionToken();
- const result = await fastify.inject({
- method: 'GET',
- url: `/sessionToken?sessionToken=${sessionToken}`,
- remoteAddress: '192.168.0.1',
- });
+ const {expires} = ra.getAccessToken();
- expect(result.statusCode).toBe(200);
+ expect(expires).toEqual(new Date(MAX_TIMESTAMP));
});
- });
- describe('Authorization override', it => {
it('always accepts access tokens', () => {
const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
@@ -210,40 +153,21 @@ describe('Request authorizer', () => {
expect(ra.accessTokenIsValid('invalid')).toBeTruthy();
});
- it('always accepts session tokens', () => {
+ it('generates session tokens with max expiry', () => {
const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
const {sessionToken} = ra.createSessionToken();
- expect(ra.accessTokenIsValid(sessionToken)).toBeTruthy();
- expect(ra.sessionTokenIsValid('invalid')).toBeTruthy();
expect(ra.getSessionTokenExpiry(sessionToken)).toEqual(new Date(MAX_TIMESTAMP));
});
- it('overrides localhost authorizer', async () => {
- const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
- const fastify = Fastify();
- fastify.decorate('requestAuthorizer', ra);
-
- fastify.get('/localhost', {preHandler: fastify.requestAuthorizer.authorizeLocalhost}, (req, reply) => {
- return reply.code(200).send();
- });
- const result = await fastify.inject({method: 'GET', url: '/localhost', remoteAddress: '192.168.0.1'});
-
- expect(result.statusCode).toBe(200);
- });
-
- it('overrides session token authorizer', async () => {
+ it('always accepts session tokens', () => {
const ra = new RequestAuthorizer({auth: {required: false}} as unknown as ConfigType, fakeLogger());
- const fastify = Fastify();
- fastify.decorate('requestAuthorizer', ra);
- fastify.get('/sessionToken', {preHandler: fastify.requestAuthorizer.authorizeSessionToken}, (req, reply) => {
- return reply.code(200).send();
- });
- const result = await fastify.inject({method: 'GET', url: '/sessionToken', remoteAddress: '192.168.0.1'});
+ const {sessionToken} = ra.createSessionToken();
- expect(result.statusCode).toBe(200);
+ expect(ra.accessTokenIsValid(sessionToken)).toBeTruthy();
+ expect(ra.sessionTokenIsValid('invalid')).toBeTruthy();
});
});
});
diff --git a/node-server/src/server/services/request_authorizer.ts b/node-server/src/server/services/token_service.ts
similarity index 77%
rename from node-server/src/server/services/request_authorizer.ts
rename to node-server/src/server/services/token_service.ts
index da44cc6..34b9400 100644
--- a/node-server/src/server/services/request_authorizer.ts
+++ b/node-server/src/server/services/token_service.ts
@@ -1,11 +1,10 @@
import type {ConfigType} from '@shared/config/config_schema.js';
import type {Logger} from '@shared/logger/logger.js';
-import type {DoneFuncWithErrOrRes, FastifyReply, FastifyRequest} from 'fastify';
import crypto from 'node:crypto';
const MAX_TIMESTAMP = 8640000000000000;
-export default class RequestAuthorizer {
+export default class TokenService {
private _validAccessTokens: {[key: string]: Date} = {};
private _validSessionTokens: {[key: string]: Date} = {};
private _currentAccessToken = '';
@@ -21,9 +20,6 @@ export default class RequestAuthorizer {
this._updateAccessTokens();
}, this._config.auth.accessTokenRefreshIntervalSec * 1000);
}
-
- this.authorizeLocalhost = this.authorizeLocalhost.bind(this);
- this.authorizeSessionToken = this.authorizeSessionToken.bind(this);
}
/**
@@ -152,41 +148,4 @@ export default class RequestAuthorizer {
return {sessionToken, expires};
}
-
- /**
- * Fastify preHandler hook to accept/reject localhost connections
- * @param request Fastify request object
- * @param reply Fastify reply object
- * @param done Fastify done function
- */
- authorizeLocalhost(
- request: FastifyRequest<{Querystring: {sessionToken?: string}}>,
- reply: FastifyReply,
- done: DoneFuncWithErrOrRes,
- ) {
- if (!this._config.auth.required) return done();
- if (request.socket.remoteAddress === '127.0.0.1') return done();
-
- reply.code(403);
- done(new Error('Unauthorized'));
- }
-
- /**
- * Fastify preHandler hook to accept/reject localhost and session token connections
- * @param request Fastify request object
- * @param reply Fastify reply object
- * @param done Fastify done function
- */
- authorizeSessionToken(
- request: FastifyRequest<{Querystring: {sessionToken?: string}}>,
- reply: FastifyReply,
- done: DoneFuncWithErrOrRes,
- ) {
- if (!this._config.auth.required) return done();
- if (request.socket.remoteAddress === '127.0.0.1') return done();
- if (this.sessionTokenIsValid(request.query.sessionToken)) return done();
-
- reply.code(403);
- done(new Error('Unauthorized'));
- }
}
diff --git a/node-server/src/server/start_server.ts b/node-server/src/server/start_server.ts
index 01537e8..8c4308b 100644
--- a/node-server/src/server/start_server.ts
+++ b/node-server/src/server/start_server.ts
@@ -8,14 +8,18 @@ import fastifyWebsocket from '@fastify/websocket';
import websocketHandler from './routes/websocket_handler.js';
import fastifyHelmet from '@fastify/helmet';
import fastifySensible from '@fastify/sensible';
-import RequestAuthorizer from './services/request_authorizer.js';
+import TokenService from './services/token_service.js';
import accessTokenHandler from './routes/session_auth_handler.js';
declare module 'fastify' {
export interface FastifyInstance {
config: ConfigType;
transcriptionEngine: TranscriptionEngine;
- requestAuthorizer: RequestAuthorizer;
+ tokenService: TokenService;
+ }
+
+ export interface FastifyRequest {
+ authorizationExpiryTimeout?: number;
}
}
@@ -51,7 +55,7 @@ export default function createServer(config: ConfigType, logger: Logger) {
// Make configuration and transcription engine avaiable on fastify instance (dependency injection)
fastify.decorate('config', config);
fastify.decorate('transcriptionEngine', new TranscriptionEngine(config, logger));
- fastify.decorate('requestAuthorizer', new RequestAuthorizer(config, logger));
+ fastify.decorate('tokenService', new TokenService(config, logger));
// Register routes
fastify.register(websocketHandler);
diff --git a/node-server/src/shared/config/config_schema.ts b/node-server/src/shared/config/config_schema.ts
index 829f78b..980cc41 100644
--- a/node-server/src/shared/config/config_schema.ts
+++ b/node-server/src/shared/config/config_schema.ts
@@ -50,6 +50,7 @@ const AUTH_CONFIG = Type.Union([
}),
Type.Object({
REQUIRE_AUTH: Type.Const(true),
+ SOURCE_TOKEN: Type.String({minLength: 1}),
ACCESS_TOKEN_BYTES: Type.Number({minimum: 1}),
ACCESS_TOKEN_REFRESH_INTERVAL_SEC: Type.Number({minimum: 1}),
ACCESS_TOKEN_VALID_PERIOD_SEC: Type.Number({minimum: 1}),
@@ -91,6 +92,7 @@ export type ConfigType = Readonly<{
| {required: false}
| {
required: true;
+ sourceToken: string;
accessTokenBytes: number;
accessTokenRefreshIntervalSec: number;
accessTokenValidPeriodSec: number;
diff --git a/node-server/src/shared/config/load_config.ts b/node-server/src/shared/config/load_config.ts
index 842b825..a6c2654 100644
--- a/node-server/src/shared/config/load_config.ts
+++ b/node-server/src/shared/config/load_config.ts
@@ -53,6 +53,7 @@ export default function loadConfig(path?: string): ConfigType {
},
auth: {
required: env.REQUIRE_AUTH,
+ sourceToken: env.SOURCE_TOKEN,
accessTokenBytes: env.ACCESS_TOKEN_BYTES,
accessTokenRefreshIntervalSec: env.ACCESS_TOKEN_REFRESH_INTERVAL_SEC,
accessTokenValidPeriodSec: env.ACCESS_TOKEN_VALID_PERIOD_SEC,
diff --git a/node-server/template.env b/node-server/template.env
index d012af3..d88ef8a 100644
--- a/node-server/template.env
+++ b/node-server/template.env
@@ -1,7 +1,7 @@
NODE_ENV="development"
LOG_LEVEL="debug"
-#### Host and port API webserver should listen on
+# ### Host and port API webserver should listen on
HOST="0.0.0.0"
PORT=8080
CORS_ORIGIN='*'
@@ -11,13 +11,15 @@ USE_HTTPS=false
KEY_FILEPATH=''
CERTIFICATE_FILEPATH=''
-#### URL to whisper service
+# ### URL to whisper service
WHISPER_SERVICE_ENDPOINT="ws://127.0.0.1:8000/whisper?api_key=CHANGEME&model_key=faster-whisper:cpu-tiny-en"
WHISPER_RECONNECT_INTERVAL_SEC=1
-#### Authentication settings
+# ### Authentication settings
# Enable or disable authentication
REQUIRE_AUTH=true
+# Key used by frontend to connect as audio source
+SOURCE_TOKEN="CHANGEME"
# How many bytes of random data should be used to generate access token
ACCESS_TOKEN_BYTES=8
# How often access token should be refreshed in seconds
diff --git a/.env b/template.env
similarity index 92%
rename from .env
rename to template.env
index 59a3711..bf606d3 100644
--- a/.env
+++ b/template.env
@@ -16,6 +16,8 @@ WHISPER_RECONNECT_INTERVAL_SEC=1
#### Authentication settings
# Enable or disable authentication
REQUIRE_AUTH=false
+# Key used by frontend to connect as audio source
+SOURCE_TOKEN="CHANGEME"
# How many bytes of random data should be used to generate access token
ACCESS_TOKEN_BYTES=8
# How often access token should be refreshed in seconds