Files
Ruslan KonviserandClaude Opus 5 60eacd8a92 fix(security): Tenant-scoped invoice numbering (GHSA-57hw, GHSA-w3mx) (#10244)
* 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>
2026-09-20 15:06:17 +02:00
..

@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.