точки входа переехали в cmd/, заведена заглушка OIDC для локального входа
- main.go и journal_route_test.go переехали в cmd/transcriber без правок содержимого; образ собирает ./cmd/transcriber поимённо - cmd/oidcstub отвечает на /authorize, /token и /userinfo, проверок не делает и слушает петлевой адрес: войти без Authelia стало чем - ступень сборки приложения переехала на node:24 с alpine — musl ждёт ответа на AAAA, которого нет, и npm ci висел вместо отказа
This commit is contained in:
+3
-2
@@ -64,8 +64,9 @@ linters:
|
||||
msg: 'пишем через slog, а не в stdout напрямую (docs/conventions/logging.md)'
|
||||
# Конфигурация приезжает из TOML. Перечислены все способы прочитать
|
||||
# окружение, а не один: `os.Getenv` без соседей обходится `os.LookupEnv`
|
||||
# одной правкой. Окружение читает только godotenv в main.go — он кладёт
|
||||
# .env в окружение процесса, а не в настройки.
|
||||
# одной правкой. Окружение читает только godotenv в точке входа
|
||||
# `cmd/transcriber` — он кладёт .env в окружение процесса, а не в
|
||||
# настройки.
|
||||
#
|
||||
# Чего правило не ловит: `fmt.Fprintln(os.Stdout, …)` и
|
||||
# `os.Stdout.WriteString` — первый аргумент по имени функции не судится.
|
||||
|
||||
@@ -119,7 +119,8 @@ go test ./... # в гейте идёт с -race, и там нуже
|
||||
go vet ./...
|
||||
gofmt -l .
|
||||
golangci-lint run
|
||||
go run . -c config.toml # флаг -c или --config, по умолчанию config.toml
|
||||
go run ./cmd/transcriber -c config.toml # флаг -c или --config, по умолчанию config.toml
|
||||
go run ./cmd/oidcstub # подставной провайдер OIDC для локального входа
|
||||
task front # приложение: зависимости, Biome, юнит-тесты, сборка
|
||||
task image # docker-образ; тег и раскладка — docs/architecture.md
|
||||
task gate # весь набор проверок разом
|
||||
@@ -234,6 +235,12 @@ Node на машину **не ставится**: шаг сборки прило
|
||||
ссылки. Расшифровка при выдуманных ключах не работает: её подменяют
|
||||
`internal/adapter/recognizer/memory.go`. Подробности строками в
|
||||
`config.example.toml`.
|
||||
**Войти при выдуманных адресах нельзя** — они никуда не ведут, а сервис без
|
||||
входа не отдаёт ничего. Вместо провайдера поднимается заглушка
|
||||
`cmd/oidcstub` — она отвечает на `/authorize`, `/token` и `/userinfo`, а
|
||||
проверок не делает никаких;
|
||||
значения `[auth]` под неё стоят строками в `config.example.toml`. Ключи
|
||||
боевого провайдера на машине разработчика при этом по-прежнему не нужны.
|
||||
- **Yandex Cloud за деньги.** Распознавание и хранение в Object Storage
|
||||
оплачиваются по факту. Прогон на реальных ключах ради проверки кода запрещён —
|
||||
подставляй `internal/adapter/recognizer/memory.go`.
|
||||
|
||||
+12
-2
@@ -3,7 +3,15 @@
|
||||
# Приложение собирается до бинарника: вшивание требует готового каталога.
|
||||
# Имя этого образа — единственное; шаг набора проверок берёт его отсюда же,
|
||||
# чтобы версия сборочного окружения не жила вторым числом в Taskfile.yml.
|
||||
FROM docker.io/library/node:24-alpine AS front-build
|
||||
#
|
||||
# Образ на glibc, а не на alpine, и разница здесь не в весе: musl шлёт запросы
|
||||
# `A` и `AAAA` разом и ждёт **оба** ответа, а DNS-сервер, который на `AAAA`
|
||||
# молчит, оставляет его без адреса вовсе — при живом `A`. `npm` на такой отказ
|
||||
# уходит в повторы с нарастающей паузой на каждом пакете, и сборка не краснеет,
|
||||
# а **висит**. Библиотека glibc довольствуется полученным `A` и собирает.
|
||||
# Ступень сборочная: в готовый образ её слои не едут, и лишний вес остаётся
|
||||
# ценой одной сборки, а не размером выкладки.
|
||||
FROM docker.io/library/node:24 AS front-build
|
||||
|
||||
WORKDIR /web
|
||||
|
||||
@@ -38,7 +46,9 @@ COPY . .
|
||||
COPY --from=front-build /web/embed/dist ./web/embed/dist
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=0 go build -o transcriber .
|
||||
# Собирается одна точка входа из cmd/, а не весь пакет: соседний cmd/oidcstub —
|
||||
# подставной провайдер для локального запуска, и в образе ему делать нечего.
|
||||
RUN CGO_ENABLED=0 go build -o transcriber ./cmd/transcriber
|
||||
|
||||
# ----------------
|
||||
# Production stage
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
```
|
||||
4. Запустите приложение:
|
||||
```bash
|
||||
go run . -c config.toml
|
||||
go run ./cmd/transcriber -c config.toml
|
||||
```
|
||||
|
||||
Сервер запустится на порту из `[server] port`, по умолчанию 8080. Нужен
|
||||
@@ -45,6 +45,15 @@
|
||||
[docs/security.md](docs/security.md), «Что разграничивает доступ»; известные
|
||||
прорехи образца конфига — [docs/conventions/config.md](docs/conventions/config.md).
|
||||
|
||||
Локально провайдера нет, и войти при выдуманных адресах нельзя — вместо него
|
||||
поднимается заглушка:
|
||||
|
||||
```bash
|
||||
go run ./cmd/oidcstub
|
||||
```
|
||||
|
||||
Значения `[auth]` под неё стоят строками в `config.example.toml`.
|
||||
|
||||
## Деплой
|
||||
|
||||
Деплой запускается из `pet-project-server`:
|
||||
@@ -82,7 +91,9 @@ inv pl -- transcriber
|
||||
|
||||
```
|
||||
transcriber/
|
||||
├── main.go # Точка входа: конфиг, миграции, сборка зависимостей, запуск
|
||||
├── cmd/
|
||||
│ ├── transcriber/ # Точка входа сервиса: конфиг, миграции, сборка зависимостей, запуск
|
||||
│ └── oidcstub/ # Подставной провайдер OIDC для локального входа
|
||||
├── internal/
|
||||
│ ├── entity/ # Модели: задача, файл, результат распознавания
|
||||
│ ├── contract/ # Интерфейсы адаптеров и репозиториев, типы ошибок
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// Команда oidcstub — подставной провайдер OIDC для локального запуска.
|
||||
//
|
||||
// Сервис держит вход через внешнего провайдера, и без него в приложение не
|
||||
// попасть вовсе: секция `[auth]` обязательна, а выдуманные адреса разбираются
|
||||
// как ссылки, но никуда не ведут. Настоящая Authelia — отдельный сервис, который
|
||||
// надо развернуть и настроить, а ключи боевого провайдера на машине разработчика
|
||||
// лежать не должны.
|
||||
//
|
||||
// Заглушка отвечает на `/authorize`, `/token` и `/userinfo`, и этого хватает:
|
||||
// `user_info_url` — обязательный ключ конфига, а при заполненном адресе сведений
|
||||
// библиотека хранилища берёт их обычным запросом с предъявленным токеном и
|
||||
// `id_token` не смотрит вовсе. Поэтому здесь нет ни ключей подписи, ни документа
|
||||
// обнаружения.
|
||||
//
|
||||
// Проверок она не делает никаких — ни секрета клиента, ни проверочного кода
|
||||
// PKCE, ни выданного токена. Вход у неё один и заранее известный: она нужна,
|
||||
// чтобы дойти до куки сессии, а не чтобы изображать провайдера. По той же
|
||||
// причине слушает она только петлевой адрес.
|
||||
//
|
||||
// Запуск:
|
||||
//
|
||||
// go run ./cmd/oidcstub
|
||||
// go run ./cmd/oidcstub -sub local-2
|
||||
//
|
||||
// Другое значение `-sub` заводит второго вошедшего, и почта переезжает вместе с
|
||||
// ним: без своей почты второй вход отвергается вовсе. Хранилище ищет учётную
|
||||
// запись сперва по признаку провайдера, а не найдя — по адресу почты, и на
|
||||
// найденной записи признак уникален. Второй `sub` при общей почте пришёл бы к той
|
||||
// же записи и получил бы отказ уникальности — вход отвечал бы 401, а причина
|
||||
// осталась бы в журнале хранилища строкой про связь.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// authCode — код, который заглушка отдаёт на возврате. Значение постоянное:
|
||||
// одноразовость кода держит провайдер, а здесь её изображать не для кого.
|
||||
const authCode = "local-code"
|
||||
|
||||
// accessToken — то, что уезжает в обмен и приходит обратно заголовком запроса
|
||||
// сведений о вошедшем. Заглушка его не сверяет.
|
||||
const accessToken = "local-token"
|
||||
|
||||
// readHeaderTimeout — потолок чтения заголовков. Заглушка стоит на петлевом
|
||||
// адресе, но сервер без единого срока держит подвисшее соединение вечно.
|
||||
const readHeaderTimeout = 5 * time.Second
|
||||
|
||||
func main() {
|
||||
port := flag.Int("port", 9000, "Порт заглушки")
|
||||
sub := flag.String("sub", "local-1", "Идентификатор вошедшего: разные значения дают разных владельцев записей")
|
||||
name := flag.String("name", "Локальный", "Имя вошедшего")
|
||||
email := flag.String("email", "", "Почта вошедшего; пустое значение даёт <sub>@example.com")
|
||||
flag.Parse()
|
||||
|
||||
// Почта выводится из `sub`, а не стоит своим умолчанием: общая почта у двух
|
||||
// разных `sub` приводит второй вход к первой записи, а признак провайдера на
|
||||
// ней уникален — второй вошедший получал бы 401 вместо своей учётной записи.
|
||||
// Обещание «разные `-sub` дают разных владельцев» держится этой строкой.
|
||||
if *email == "" {
|
||||
*email = *sub + "@example.com"
|
||||
}
|
||||
|
||||
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelInfo,
|
||||
}))
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/authorize", authorize(logger))
|
||||
mux.HandleFunc("/token", token(logger))
|
||||
mux.HandleFunc("/userinfo", userInfo(logger, *sub, *name, *email))
|
||||
|
||||
// Петлевой адрес, а не все — заглушка пускает кого угодно кем угодно, и в
|
||||
// чужой сети это дыра, а не удобство.
|
||||
addr := fmt.Sprintf("127.0.0.1:%d", *port)
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: readHeaderTimeout,
|
||||
}
|
||||
|
||||
logger.Info("oidc stub started", "addr", addr, "sub", *sub)
|
||||
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
logger.Error("oidc stub stopped", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// authorize уводит браузер обратно на адрес возврата с готовым кодом.
|
||||
//
|
||||
// Настоящий провайдер спросил бы здесь имя и пароль; заглушка не спрашивает
|
||||
// ничего и возвращает сразу — в этом вся её работа.
|
||||
func authorize(logger *slog.Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
|
||||
target, err := url.Parse(query.Get("redirect_uri"))
|
||||
if err != nil || target.Host == "" {
|
||||
logger.Error("authorize rejected", "reason", "redirect_uri is missing or malformed")
|
||||
http.Error(w, "redirect_uri не задан или не разбирается", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Состояние возвращается тем же значением, каким пришло: сервис сверяет
|
||||
// его со своей кукой и без совпадения отвергает возврат.
|
||||
back := target.Query()
|
||||
back.Set("code", authCode)
|
||||
if state := query.Get("state"); state != "" {
|
||||
back.Set("state", state)
|
||||
}
|
||||
target.RawQuery = back.Encode()
|
||||
|
||||
logger.Info("authorize passed", "redirect_host", target.Host)
|
||||
|
||||
http.Redirect(w, r, target.String(), http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
// token отдаёт токен в обмен на код. Ни код, ни секрет клиента, ни проверочный
|
||||
// код PKCE не сверяются.
|
||||
func token(logger *slog.Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "обмен идёт методом POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// `id_token` не отдаётся намеренно: подписать его нечем, а библиотека
|
||||
// хранилища его и не смотрит — `user_info_url` стоит в перечне
|
||||
// обязательных ключей конфига, и без него сервис не поднимается вовсе.
|
||||
writeJSON(logger, w, map[string]any{
|
||||
"access_token": accessToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// userInfo отдаёт сведения о вошедшем.
|
||||
//
|
||||
// `email_verified` обязано быть истинным: без него библиотека адрес почты в
|
||||
// учётную запись не запишет, и запись заведётся без него.
|
||||
func userInfo(logger *slog.Logger, sub, name, email string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeJSON(logger, w, map[string]any{
|
||||
"sub": sub,
|
||||
"name": name,
|
||||
"preferred_username": sub,
|
||||
"email": email,
|
||||
"email_verified": true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// writeJSON отвечает разметкой JSON. Отказ записи идёт в журнал: ответ к этому
|
||||
// моменту уже начат, и сказать о нём спрашивающему нечем.
|
||||
func writeJSON(logger *slog.Logger, w http.ResponseWriter, body map[string]any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(body); err != nil {
|
||||
logger.Error("response write failed", "error", err)
|
||||
}
|
||||
}
|
||||
@@ -85,3 +85,31 @@ redirect_url = "https://transcriber.example.com/auth/callback"
|
||||
# Признак `Secure` у куки сессии. Умолчание true; false только для локального
|
||||
# запуска по http://localhost, где браузер такую куку не сохранит
|
||||
secure_cookie = true
|
||||
|
||||
# Локальный вход без Authelia.
|
||||
#
|
||||
# Провайдер на машине разработчика не поднимается, а ключи боевого на ней лежать
|
||||
# не должны — вместо провайдера идёт заглушка `cmd/oidcstub`. Она отвечает на
|
||||
# `/authorize`, `/token` и `/userinfo` и не сверяет ни секрет клиента, ни
|
||||
# проверочный код PKCE: вход у неё один и заранее известный. Поднимается
|
||||
# отдельным процессом:
|
||||
#
|
||||
# go run ./cmd/oidcstub
|
||||
#
|
||||
# Замени секцию `[auth]` выше на эти значения — вход пойдёт настоящим путём,
|
||||
# вплоть до куки сессии и заведённой учётной записи в панели:
|
||||
#
|
||||
# auth_url = "http://localhost:9000/authorize"
|
||||
# token_url = "http://localhost:9000/token"
|
||||
# user_info_url = "http://localhost:9000/userinfo"
|
||||
# client_id = "transcriber"
|
||||
# client_secret = "local"
|
||||
# redirect_url = "http://localhost:8080/auth/callback"
|
||||
# secure_cookie = false
|
||||
#
|
||||
# Другое значение `-sub` заводит второго вошедшего: это неизменяемый признак,
|
||||
# по которому обмен ищет учётную запись. Почта переезжает вместе с ним сама —
|
||||
# заглушка выводит её из `-sub`, — и это обязательно: при общей почте второй вход
|
||||
# приходит к первой записи и получает отказ.
|
||||
#
|
||||
# go run ./cmd/oidcstub -sub local-2
|
||||
|
||||
+13
-2
@@ -77,8 +77,8 @@
|
||||
[pipeline](../openspec/specs/pipeline/spec.md), «Брошенная задача возвращается
|
||||
в работу»; здесь это принцип письма шага, а не описание поведения.
|
||||
- **Ядро зависит от интерфейсов.** `internal/service` знает только
|
||||
`internal/contract`; ffmpeg, Yandex и хранилище подставляются в
|
||||
`main.go`. Правило механизировано тестами-сканерами `internal/archrules`, и
|
||||
`internal/contract`; ffmpeg, Yandex и хранилище подставляются в точке входа
|
||||
`cmd/transcriber`. Правило механизировано тестами-сканерами `internal/archrules`, и
|
||||
они же держат обратные направления: транспорты не знают друг о друге, адаптер
|
||||
не знает ни ядра, ни транспортов.
|
||||
*Изъятие:* транспорт **вправе** знать адаптер хранилища — `controller/http`
|
||||
@@ -219,6 +219,17 @@
|
||||
не попадает. Финальный слой — alpine с `ca-certificates` и `ffmpeg`, процесс
|
||||
работает под непривилегированным пользователем `transcriber`.
|
||||
|
||||
Ступень бинарника собирает **одну** точку входа — `./cmd/transcriber`, а не весь
|
||||
пакет: рядом в `cmd/` живёт `oidcstub`, подставной провайдер OIDC для локального
|
||||
входа, и в образе ему делать нечего.
|
||||
|
||||
Ступень приложения стоит на образе с glibc, а не на alpine, и решает это не вес:
|
||||
у musl запрос имени идёт `A` и `AAAA` разом и ждёт **оба** ответа, поэтому
|
||||
DNS-сервер, молчащий на `AAAA`, оставляет установщика пакетов без адреса при
|
||||
живом `A`. Установщик уходит в повторы с нарастающей паузой на каждом пакете, и
|
||||
сборка не краснеет, а **висит** — исход хуже красного. Слои этой ступени в
|
||||
рабочий слой не едут, поэтому её вес остаётся ценой одной сборки.
|
||||
|
||||
**По весу финальный образ от ступени приложения не растёт вовсе:** она отдаёт
|
||||
следующей только собранное, а сама в рабочий слой не копируется. Вшитое
|
||||
приложение прибавляет к бинарнику 86 072 байта. Время сборки образа не
|
||||
|
||||
@@ -5,12 +5,12 @@
|
||||
|
||||
**Взято из проекта jellybit.** Расхождения с сегодняшним кодом названы по месту.
|
||||
Главные: комментариями снабжена половина полей; единого места проверки на старте
|
||||
нет: у секций `[auth]` и `[pipeline]` свой `Validate()` в `main.go`, а пустые
|
||||
нет: у секций `[auth]` и `[pipeline]` свой `Validate()` в точке входа, а пустые
|
||||
ключи `[yandex]` ловит конструктор распознавателя.
|
||||
|
||||
**Механизировано:** запрет `os.Getenv` — `forbidigo` в `.golangci.yml`
|
||||
([go-linters.md](go-linters.md), «Механизировано»). Он держит правило «настройки
|
||||
приезжают из TOML»; `godotenv` в `main.go` по-прежнему загружает `.env`, но кладёт
|
||||
приезжают из TOML»; `godotenv` в `cmd/transcriber` по-прежнему загружает `.env`, но кладёт
|
||||
его в окружение процесса, а не в настройки приложения.
|
||||
|
||||
## Принципы
|
||||
@@ -18,7 +18,7 @@
|
||||
- **Конфигурация — только TOML.** Переменные окружения для конфигурации **не
|
||||
используем**: окружение наследуется дочерними процессами и видно через
|
||||
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
|
||||
*Расхождение:* `main.go` зовёт `godotenv.Load()` и молча продолжает без файла.
|
||||
*Расхождение:* `cmd/transcriber` зовёт `godotenv.Load()` и молча продолжает без файла.
|
||||
- Грузим **один раз при старте** в одну типизированную структуру `Config`
|
||||
(под-структуры по секциям). Дальше по коду читаем только её — чтения файла в
|
||||
прикладном коде нет, только загрузчик `internal/config`.
|
||||
@@ -62,6 +62,12 @@ force_shutdown_timeout = <N> # ждать остановки ворке
|
||||
говорит, какой формы значение здесь ждут. Пустым оставлен только
|
||||
`client_secret` — он и есть секрет.
|
||||
|
||||
Там же, комментарием под секцией, стоит **второй набор значений `[auth]` — под
|
||||
подставной провайдер `cmd/oidcstub`**. Они — не живая форма, и это намеренно:
|
||||
образец описывает боевую выкладку, а локальный вход — способ до неё дойти, и два
|
||||
рабочих набора в одном файле читались бы как выбор без указания, какой из них
|
||||
чей.
|
||||
|
||||
## Поля по дискриминатору `type`
|
||||
|
||||
Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из
|
||||
@@ -130,7 +136,7 @@ TOML. Пустые ключи Yandex ловятся в конструкторе
|
||||
входом 2026-08-14: секции больше нет, и своей проверки у неё тоже.
|
||||
|
||||
Секция `[auth]` — первая, у которой проверка своя и стоит на старте:
|
||||
`AuthConfig.Validate()` зовётся из `main.go` сразу после загрузки и роняет
|
||||
`AuthConfig.Validate()` зовётся из `cmd/transcriber` сразу после загрузки и роняет
|
||||
процесс с перечнем незаполненных ключей. Причина в цене умолчания: поднявшись с
|
||||
молча выключенным входом, сервис остался бы открытым наружу, а узнать об этом
|
||||
было бы неоткуда. Сообщение называет **имена ключей**, а не значения — значение
|
||||
|
||||
@@ -31,7 +31,10 @@ OpenSpec.
|
||||
{"time":"2026-08-10T11:23:45.123456Z","level":"INFO","msg":"record accepted","capability":"intake","record_id":"…","source":"api","duration_seconds":137}
|
||||
```
|
||||
|
||||
*Расхождение:* `main.go` ставит `slog.NewTextHandler(os.Stdout, …)`.
|
||||
*Расхождение:* текстовый обработчик ставят **оба** пакета `cmd/` —
|
||||
`slog.NewTextHandler(os.Stdout, …)` и в `transcriber`, и в `oidcstub`. У второго
|
||||
это выбор, а не долг: его вывод читает человек в терминале, и разбирать его
|
||||
отбором никто не станет.
|
||||
|
||||
## Сообщение
|
||||
|
||||
@@ -78,7 +81,7 @@ OpenSpec.
|
||||
- `slog` не разделяет CRITICAL и FATAL — сбой на старте логируем `ERROR` и
|
||||
завершаем процесс с ненулевым кодом.
|
||||
|
||||
*Расхождение:* уровень зашит константой в `main.go`, `DEBUG` включить нечем.
|
||||
*Расхождение:* уровень зашит константой в `cmd/transcriber`, `DEBUG` включить нечем.
|
||||
Пустой прогон воркера не логируется вовсе — и это правилу не противоречит.
|
||||
|
||||
## Время
|
||||
@@ -208,7 +211,7 @@ Object Storage и опрос операции не логируются ника
|
||||
`INFO` они не пишутся.
|
||||
|
||||
Расхождения здесь больше нет: слой журналирования запросов свой,
|
||||
`main.go`, хук `OnServe` — вместе с gin ушёл и `sloggin`. `/health` и `/metrics`
|
||||
`cmd/transcriber`, хук `OnServe` — вместе с gin ушёл и `sloggin`. `/health` и `/metrics`
|
||||
идут на `DEBUG`, то есть при боевом `INFO` не пишутся вовсе.
|
||||
|
||||
Хранилище ведёт **свой** журнал запросов в собственной таблице, и он виден
|
||||
|
||||
@@ -169,6 +169,20 @@ pb_data/storage/<коллекция>/<запись>/<имя>_<10 случайн
|
||||
**Куки библиотека не читает вовсе** — сессию берёт только заголовком
|
||||
`Authorization` (`apis/middlewares.go`, `getAuthTokenFromRequest`).
|
||||
|
||||
**Учётную запись обмен ищет двумя способами подряд, а связь с провайдером
|
||||
уникальна.** Сперва — по неизменяемому признаку провайдера, а не найдя — по
|
||||
адресу почты (`apis/record_auth_with_oauth2.go`, ветка `case authUser.Email !=
|
||||
""` → `FindAuthRecordByEmail`). Найденной записи он пытается добавить связь, а на
|
||||
ней стоит уникальный индекс
|
||||
`idx_externalAuths_record_provider (collectionRef, recordRef, provider)`. Отсюда
|
||||
исход, обратный ожидаемому: два разных признака провайдера с **одной** почтой не
|
||||
сливаются в одного владельца молча — второй вход отвергается, обмен отдаёт `400`,
|
||||
сервис — `401` со строкой `Failed to exchange provider code`, а настоящая причина
|
||||
остаётся в журнале хранилища строкой `failed to save linked rel: … Value must be
|
||||
unique`. Дописано 2026-08-15 задачей про заглушку OIDC; получено прогоном против
|
||||
временного каталога — чтение исходников давало ту же цепочку, но противоположную
|
||||
развязку.
|
||||
|
||||
**Журнал запросов пишет строку запроса целиком.** `activityLogger` на корневом
|
||||
роутере кладёт `RequestURI` полем `url` в таблицу `_logs`, ретеншен по умолчанию
|
||||
`MaxDays: 5`. Значит всё, что пришло параметром адреса, оседает там на пять
|
||||
|
||||
@@ -119,6 +119,18 @@
|
||||
|
||||
### Типовые ложноположительные
|
||||
|
||||
- **«Хранилище молча сливает две учётные записи с одной почтой в одного
|
||||
владельца».** Для версии v0.39.10 неверно, и неверна именно развязка. Первая
|
||||
половина цепочки настоящая: обмен ищет запись по признаку провайдера, а не
|
||||
найдя — по адресу почты, и приходит к чужой записи. Но повесить на неё второй
|
||||
признак он не может — уникальный индекс
|
||||
`idx_externalAuths_record_provider (collectionRef, recordRef, provider)` связь
|
||||
отвергает, обмен отдаёт `400`, а сервис — `401` со строкой
|
||||
`Failed to exchange provider code`. Отказ **громкий**, тихого слияния владельцев
|
||||
не происходит, и ложно-зелёной проверки разграничения такой дефект не даёт.
|
||||
Проверено прогоном 2026-08-15 (задача про заглушку OIDC); найдено чтением
|
||||
исходников библиотеки, опровергнуто запуском — то есть цена гипотезы, добытой
|
||||
без прогона, здесь и измерена.
|
||||
- **«Воркер глотает ошибку `NoopJobError`».** Не дефект: этот тип означает «задач
|
||||
в этом состоянии нет», и `internal/controller/worker/worker.go` намеренно не
|
||||
логирует его и не считает в метрику. Норма записана требованием
|
||||
@@ -159,6 +171,14 @@
|
||||
|
||||
Форма: `<тема>: <вопрос> (<откуда>)`.
|
||||
|
||||
- `operations`: не завёл ли инструмент разработчика второй дом тому, что уже есть
|
||||
в проверках. Подставных провайдера OIDC в репозитории теперь два — `cmd/oidcstub`
|
||||
и `fakeProvider` в `internal/controller/http/login_test.go`, — с теми же
|
||||
адресами и той же посылкой про `email_verified`, и они уже разошлись в мелочи
|
||||
(`token_type` «bearer» против «Bearer»). Тем же вопросом судится подставной
|
||||
распознаватель. Записанной конвенции о единственном доме подставных внешних
|
||||
собеседников у проекта нет, поэтому спрашивать надо, а не считать нарушением
|
||||
(ревью задачи про заглушку OIDC, 2026-08-15).
|
||||
- `operations`: как шаг отвечает на отмену посреди работы — контекст доходит до
|
||||
внешнего собеседника и это держат правила `noctx` и `contextcheck`
|
||||
([conventions/go-linters.md](conventions/go-linters.md), «Отмена и внешний
|
||||
@@ -312,6 +332,27 @@ API и имя не откатываются обратной правкой по
|
||||
истории git 2026-08-10: поле «Чем воспроизведён» называет у них коммит, а не
|
||||
оракул, и выдумывать оракул задним числом нельзя.
|
||||
|
||||
## 2026-08-15 — короткая форма рецепта входа не работала, а проверяли длинную [пойман ревью]
|
||||
|
||||
- **Где:** `cmd/oidcstub` — подставной провайдер OIDC для локального входа;
|
||||
доккоммент пакета, подсказка флага `-sub` и проза `config.example.toml`
|
||||
- **Симптом:** рецепт «второй вошедший получается сменой `-sub`» записан в трёх
|
||||
местах и в короткой форме не работал вовсе. Заглушка отдавала обоим `sub` одну
|
||||
и ту же почту умолчанием, вход отвечал `401`, а причина оставалась строкой в
|
||||
журнале хранилища
|
||||
- **Причина:** обмен ищет учётную запись сперва по признаку провайдера, а не
|
||||
найдя — по адресу почты. Второй `sub` при общей почте приходил к первой записи,
|
||||
а признак провайдера на записи уникален — `idx_externalAuths_record_provider` —
|
||||
и связь отвергалась. Умолчание почты стояло своим значением вместо выведенного
|
||||
из `sub`
|
||||
- **Почему не поймали раньше:** рецепт проверяли **длинной** формой, где почта
|
||||
задана флагом явно. Короткую не гонял никто, хотя записана она первой и берут
|
||||
читатели именно её
|
||||
- **Что меняем:** проверять ту форму рецепта, которая записана **короче всех**.
|
||||
Оракул — прогон именно её: два входа подряд разными `-sub` без прочих флагов,
|
||||
затем счёт записей в коллекции пользователей. Само умолчание почты теперь
|
||||
выводится из `-sub`
|
||||
|
||||
## 2026-08-15 — своя раздача статики потеряла отказ от записи успеха [пойман ревью]
|
||||
|
||||
- **Где:** `internal/controller/http/webapp.go`, регистрация корневого маршрута;
|
||||
|
||||
@@ -335,6 +335,13 @@ Storage, оттуда его читает SpeechKit. Третий путь —
|
||||
|
||||
- **Атака на сам сервер и на контур.** Компрометация хоста, прокси, Docker и
|
||||
Ansible — не наша граница.
|
||||
- **Машина разработчика и то, что он на ней поднимает.** С 2026-08-15 в
|
||||
репозитории лежит `cmd/oidcstub` — подставной провайдер OIDC, который выдаёт
|
||||
сессию всякому спросившему и не сверяет ни секрета клиента, ни проверочного
|
||||
кода PKCE. Двух вещей это не отменяет, и обе проверяемы: в образ он не едет
|
||||
(ступень собирает `./cmd/transcriber` поимённо), а слушает петлевой адрес.
|
||||
Периметра выкладки заглушка поэтому не касается; кто поднял её у себя в чужой
|
||||
сети, отвечает за это сам.
|
||||
- **Злоупотребление со стороны пользователя из белого списка.** Приглашённому
|
||||
доверяем полностью.
|
||||
- **Достоверность расшифровки.** Подмена или искажение текста на стороне
|
||||
|
||||
@@ -28,7 +28,7 @@ const (
|
||||
)
|
||||
|
||||
// Ядро — `internal/service`: оно знает только интерфейсы `internal/contract`, а
|
||||
// ffmpeg, Yandex и хранилище подставляются в `main.go`
|
||||
// ffmpeg, Yandex и хранилище подставляются в точке входа `cmd/transcriber`
|
||||
// (docs/architecture.md, «Принципы»).
|
||||
const core = "internal/service"
|
||||
|
||||
@@ -78,7 +78,7 @@ func TestЯдроНеЗнаетОбАдаптерах(t *testing.T) {
|
||||
if strings.HasPrefix(imp, adapterPrefix) {
|
||||
t.Errorf(
|
||||
"%s импортирует адаптер %s: ядро зависит от интерфейсов "+
|
||||
"internal/contract, а реализацию подставляет main.go",
|
||||
"internal/contract, а реализацию подставляет cmd/transcriber",
|
||||
core, imp,
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@
|
||||
- [🔬 Четыре недоказанные гипотезы о поверхности входа](items/login-surface-hypotheses.md) — Ревью назвало четыре пути, которых не смогло ни подтвердить, ни опровергнуть: браузера и живого провайдера в прогоне не было.
|
||||
- [🐞 Починить срок сессии, который ставит откат шага входа](items/rollback-restores-wrong-session-duration.md) — Константа defaultAuthTokenDuration в шаге 202608120001 названа умолчанием библиотеки, но 1209600 — это 14 суток, а умолчание PocketBase 432000, пять суток: откат объявляет возврат к умолчанию и ставит срок вдвое больше выбранных владельцем семи.
|
||||
- [🔬 Адрес объекта в тексте отказа SpeechKit](items/speechkit-error-text-leak.md) — Текст отказа операции приходит от Yandex и уезжает в журнал и в колонку error_text: если он несёт URI объекта, из журнала снова собирается ссылка на чужую запись.
|
||||
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||
- [🧹 Разобрать мелочи http-транспорта](items/http-transport-nits.md) — Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||
- [🧹 Запретить обращаться к Bot API мимо клиента бота](items/bot-api-only-through-bot-client.md) — Чистка отказа от адреса с токеном живёт в клиенте; свой http.Client в транспорте вернёт утечку молча — правило noctx такую подмену не ловит, а класс уже стоил одного дефекта.
|
||||
- [🧹 Свести пять расхождений между документами канона](items/docs-consistency-2026-08-13.md) — Сверка 2026-08-13 нашла шесть мест, где два документа отвечают на один вопрос по-разному; одно сведено при повышении раскладки, а три из пяти оставшихся стоят в architecture.md, и по ним читатель строит решения о выкладке и о периметре.
|
||||
- [✨ Сопоставить пользователя Telegram с учётной записью](items/telegram-account-link.md) — Белый список сверяется с именем пользователя Telegram, которое владелец меняет в любой момент, а записи из бота ни с кем не связаны.
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
- **Тип:** chore
|
||||
- **Категория:** Очередь — Поднято до контракта API: маршруты объявлены дважды, и переименование пути проходит тесты зелёным — переписывать контроллер под слепыми проверками нельзя.
|
||||
- **Зачем:** Маршруты зарегистрированы дважды, и переименование пути в main.go проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||
- **Зачем:** Маршруты зарегистрированы дважды, и переименование пути в cmd/transcriber проходит проверки зелёным; обработчик пишет в журнал через стандартный log и дублирует запись, уже сделанную сервисом.
|
||||
- **Теги:** review-2026-08-11
|
||||
|
||||
Два дефекта, найденные ревью change `2026-08-11-fix-http-handler-tests`. Оба в
|
||||
`internal/controller/http`, мерджатся одним заходом.
|
||||
|
||||
**Маршруты зарегистрированы дважды.** `main.go` объявляет пути приёма и опроса,
|
||||
**Маршруты зарегистрированы дважды.** `cmd/transcriber` объявляет пути приёма и опроса,
|
||||
и сборка теста объявляет их же своей копией. Проверки ходят по копии, поэтому
|
||||
переименование пути в `main.go` их не роняет — а публичный контракт HTTP API
|
||||
переименование пути в `cmd/transcriber` их не роняет — а публичный контракт HTTP API
|
||||
проект объявил необратимым. Проверено мутацией на прогоне триажа: замена
|
||||
`api.POST("/audio", …)` на `api.POST("/upload", …)` оставляет тесты зелёными.
|
||||
Нашёл проход `review-specs`.
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- `internal/controller/http` и `main.go` — регистрация маршрутов приёма и
|
||||
- `internal/controller/http` и `cmd/transcriber` — регистрация маршрутов приёма и
|
||||
опроса, сегодня продублированная в обоих местах;
|
||||
- `internal/controller/http/transcribe.go` — журнальная строка обработчика;
|
||||
- `internal/controller/http/transcribe_test.go` — сборка теста, держащая свою
|
||||
|
||||
@@ -41,7 +41,7 @@ file name too long"`, и поля `file_ext` в ней нет вовсе.
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- слой журнала запроса в `main.go` и приведение пути `journalRoute`;
|
||||
- слой журнала запроса в `cmd/transcriber` и приведение пути `journalRoute`;
|
||||
- отказ подготовки рабочей копии в `internal/service`, шаг приёма записи;
|
||||
- `docs/conventions/logging.md` — правило о длине поля журнала;
|
||||
- умолчание `MaxHeaderBytes` у сервера: сегодня оно единственное неназванное
|
||||
|
||||
@@ -37,7 +37,7 @@ POST /app/audiorecords без сессии → 401 ... http.status_code=0
|
||||
|
||||
## Затрагивает
|
||||
|
||||
- слой журнала запроса в `main.go`: место, где снимается код ответа;
|
||||
- слой журнала запроса в `cmd/transcriber`: место, где снимается код ответа;
|
||||
- `docs/conventions/logging.md` — словарь полей запроса.
|
||||
|
||||
## Критерии приёмки
|
||||
|
||||
Reference in New Issue
Block a user