* fix(security): scope the invoice number sequence to the tenant GHSA-57hw-jqpj-ww97 (medium): GET /invoices/highest ran an unscoped MAX over every tenant's invoices, so any authenticated user learned the highest invoice number in the installation, and numbering leaked business volume across tenants. The aggregate is now tenant-scoped in both ORM branches and fails closed without a tenant. Because scoping alone would collide with numbers other tenants already hold, the global unique on invoiceNumber becomes unique per (tenantId, invoiceNumber), with a per-dialect migration for postgres, mysql and sqlite. GHSA-w3mx-m5cr-3gxp residual (low): a tenant AI-provider credential with no baseUrl still reached the provider's built-in loopback default. Every tenant-sourced credential is now treated as tenant-controlled, so it is blocked unless private base URLs are explicitly allowed; only an operator-set environment base URL bypasses the flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(cspell): add the new vocabulary and use US spellings Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(invoice): make the MySQL uniqueness migration resumable and reject fractional numbers Review follow-up on the tenant-scoped invoice numbering change. MySQL commits every DDL statement on its own, so `migrationsTransactionMode: 'each'` does not roll the CREATE and the DROP of `mysqlUpQueryRunner` back together. If the process died between them — or the information_schema lookup failed — the new composite index survived while the migration row was never written, and the retry on the next boot died on `Duplicate key name`, taking the API down until someone repaired the schema by hand. Both MySQL branches now look the index up by its COLUMNS first (`GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX)`, so column order is part of the match and the primary key never matches), create only what is missing and drop every same-shaped leftover whatever it is named. That also fixes the reverse case the reviewers pointed out: `down()` used to drop only the index name this migration happens to use. `invoiceNumber` now requires a whole number. `numeric` (Postgres/SQLite) stores a fraction that MySQL's `bigint` truncates, so a fractional number made `MAX(invoiceNumber) + 1` mean different things per database; `@IsNumber()` accepted it. `@IsInt()` already implies a number, so it replaces rather than joins `@IsNumber()`. `up()`/`down()` dispatch through a table instead of the two switch statements every migration in this folder repeats verbatim. The behaviour is identical (including the `Unsupported database` throw); it is here because those ~40 boilerplate lines were counted as duplicated new code and failed the Sonar quality gate for this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(invoice): cover the MySQL branch of the uniqueness migration The MySQL resumability fix was argued from the MySQL manual, not exercised: no MySQL server is available in this environment. It does not need one — the migration only ever learns which unique indexes exist through `information_schema.STATISTICS`, so a stub query runner over a simulated index set drives the real branch end to end and records the DDL it issues. Five cases, including the two the review raised: a crash between the CREATE and the DROP (the index is found, no second CREATE is attempted, the old one is still dropped), a completed run re-entered (no statements at all), an index the migration chain did not name, and a revert that has to drop a composite index under an unexpected name. The primary key shares the `NON_UNIQUE = 0` filter and is asserted to survive all of them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@gauzy/plugin-ai-provider-whisper-cpp
Local speech-to-text provider plugin for the Ever Gauzy AI chat engine
(@gauzy/plugin-ai-chat) backed by
whisper.cpp's whisper-server — a
tiny, dependency-free HTTP server that runs OpenAI Whisper (ggml) models on CPU,
Apple Silicon or CUDA. No cloud account, no API key.
On bootstrap it registers the whisper-cpp provider with the chat engine's
AiProviderRegistry as a voice-only, local provider: it transcribes
dictation for the AI chat but has no chat models (chatCapable: false). It
needs no API key (requiresApiKey: false) — a tenant credential holding only
the server's base URL, or WHISPER_CPP_BASE_URL, is enough.
Dictation calls POST {baseUrl}/inference as multipart (file,
response_format=json, temperature=0, optional language) and reads text.
The server loads one model at start-up (-m), so the "Speech model" on the
settings page is a single fixed entry, whisper.cpp (server model).
Run the server
# Build once
git clone https://github.com/ggml-org/whisper.cpp && cd whisper.cpp
cmake -B build && cmake --build build -j --config Release
sh ./models/download-ggml-model.sh base.en # or small / medium / large-v3-turbo
# Serve on http://localhost:8080
./build/bin/whisper-server -m models/ggml-base.en.bin --host 0.0.0.0 --port 8080 --convert
(--convert lets the server accept the browser's WebM/Opus and MP4 recordings by
converting them with ffmpeg; without it whisper-server only accepts 16 kHz WAV.)
Or with Docker: docker run --rm -p 8080:8080 -v ./models:/models ghcr.io/ggml-org/whisper.cpp:main ./build/bin/whisper-server -m /models/ggml-base.en.bin --host 0.0.0.0 --port 8080 --convert.
Then point Gauzy at it: Settings → AI Providers → Add AI Voice Provider →
whisper.cpp, base URL http://localhost:8080 (prefilled), leave the API key
empty, tick "Use as default voice provider".
Environment variables
| Variable | Description |
|---|---|
WHISPER_CPP_BASE_URL |
Server-wide whisper-server base URL, e.g. http://whisper:8080. Setting it makes the provider configured for every tenant. |
WHISPER_CPP_API_KEY |
Optional — only if you put the server behind an authenticating proxy (Authorization: Bearer). |
BYOK
A tenant credential saved for the whisper-cpp provider via the AI chat
credentials API (/api/ai-chat/credentials) always takes precedence over the
WHISPER_CPP_* environment variables.