Skip to content

build: upgrade better-auth to 1.7.7 - #3

Merged
agentHits merged 8 commits into
AgentHits-Devfrom
feat/better-auth-1-7
Oct 6, 2026
Merged

agentHits merged 8 commits into
AgentHits-Devfrom
feat/better-auth-1-7

Conversation

@agentHits

Copy link
Copy Markdown
Collaborator

What is this PR about?

Moves the AgentHits fork from better-auth 1.6.23 to 1.7.7, which closes GHSA-j8v8-g9cx-5qf4 in @better-auth/scim (the last fixable bun audit finding; only braces 3.0.3 remains). Upstream Dokploy#5517 deferred this upgrade. Admin runbook: docs/better-auth-1.7-upgrade.md.

Commits

  • build: upgrade better-auth family to 1.7.7: bumps the family, better-call 1.4.0 and @better-fetch/fetch 1.3.2, and drops the global @xmldom/xmldom <0.9.0 override (@better-auth/sso needs ^0.9.12; samlify stays on 0.8.15). better-auth 1.7.3+ validates the Drizzle schema before every request, and user.ownerId and organization.ownerId made it reject every sign-in. Both are fixed, and a test runs the real check. Also covers 2FA enable narrowing and unlinkAccount by local account id.
  • fix(sso): gate sso email domains before account creation: 1.7 SSO callbacks only log after-hook errors, so the domain check moves to user.validateUserInfo.
  • feat(sso): migrate saml and oidc providers to better-auth 1.7 config: new ACS /api/auth/sso/saml2/sp/acs/<providerId>, no mapping.id, and idpMetadata.entityID. Migration 0199 rewrites stored provider configs; without it 1.7 answers 400 for SAML providers created in the Dokploy UI.
  • feat(scim): replace legacy scim providers with managed connections: per-organization connections with tokens shown once, a membership projection, relinking of legacy SCIM users, and migrations 0200–0202.
  • docs: add better-auth 1.7 upgrade notes for sso and scim admins.
  • fix: stop startup when database migrations fail: migration.ts used to exit 0 after a failed migration.
  • fix: handle fork databases where upstream migration 0173 was skipped: the drizzle migrator skipped upstream 0173/0174 on the dev database because fork migrations with later timestamps ran first. As a result there was no scim_provider and no two_factor columns, and enabling 2FA failed with column "verified" does not exist. 0201/0202 now tolerate the missing table, and 0203 re-adds the columns idempotently.
  • ci: let the upgrade integration test target a fork image: optional target_image, plus a post-upgrade sign-in check.

Admin actions after deploy

  • SAML: set the IdP ACS URL to https://<dokploy>/api/auth/sso/saml2/sp/acs/<providerId> (the SP entity ID is unchanged). IdP-initiated sign-in is disabled.
  • SCIM: create a connection in Manage SCIM, put the new token into the IdP, and run a full sync.
  • The dev database has no SSO providers and no SCIM data, so no IdP changes are needed there.

Checklist

  • Dedicated branch based on the AgentHits-Dev content (chore/deps-security-update).
  • bun run typecheck: 0 errors in packages/server; apps/dokploy has only the existing next.config.mjs error.
  • bun run test: 2050 passed. Only deploy/application.real.test.ts fails, because no local Docker socket is available.
  • bun run build (all workspaces, incl. Next) and bun run server:build succeed.
  • Migrations rehearsed on a copy of the dev schema (schema-only dump plus drizzle.__drizzle_migrations): the real migration.ts applied 0199–0203. Real lib/auth.ts then passed sign-up, sign-in, 2FA enable/verify/second factor, an API key session and SCIM POST /Users.
  • SAML rows migrated by 0199 were read by real better-auth 1.7.7: same SP entity ID, new ACS in metadata and AuthnRequest.

🤖 Generated with Claude Code

agentHits and others added 8 commits October 6, 2026 09:28
Что:
- better-auth и @better-auth/{api-key,passkey,scim,sso} обновлены с 1.6.23 до 1.7.7; в root overrides better-call 1.4.0 и @better-fetch/fetch 1.3.2 по каталогу 1.7.7; override @xmldom/xmldom <0.9.0 удален, потому что @better-auth/sso 1.7 требует ^0.9.12, а samlify и xml-crypto остаются на 0.8.15 по своим ^0.8 диапазонам.
- Генерация схемы переведена на CLI-пакет auth 1.7.7 (скрипт generate:drizzle).
- Drizzle-адаптер работает с transaction: true; ownerId убран из user.additionalFields (колонки нет, validateRequest вычисляет его как владельца активной организации и возвращает явно собранного пользователя); NOT NULL organization.ownerId объявлен через schema.organization.additionalFields.
- Плагин SCIM временно отключен: API токенов 1.6 в 1.7 удалены, выдача токена в tRPC отвечает PRECONDITION_FAILED до перехода на managed connections.
- twoFactor.enable сужается по method, unlinkAccount вызывается с локальным account.id.
- Добавлен тест, который поднимает настоящий betterAuth и вызывает встроенную проверку схемы 1.7 для self-hosted и cloud.
Зачем:
- Закрыть GHSA-j8v8-g9cx-5qf4 в @better-auth/scim 1.6.23, исправленную только в 1.7.x.
- В 1.7.3+ каждый запрос и прямой вызов auth.api сначала проверяет Drizzle-схему; user.ownerId и organization.ownerId давали SchemaMismatchError и блокировали весь вход в дашборд.
Риски:
- SCIM-провижининг недоступен до коммита с managed connections.
- В транзакционном режиме хуки внутри транзакции не должны писать через глобальный db.
Проверки:
- Команды и результаты: bun install с minimumReleaseAge=259200 и исключением для уже зафиксированного http-cache-semantics 4.3.0 — успешно; bun audit — осталась только braces 3.0.3 без upstream-исправления, GHSA-j8v8-g9cx-5qf4 ушла; bun run typecheck в packages/server — 0 ошибок, в apps/dokploy — только существующая ошибка next.config.mjs (outputFileTracing); biome check по измененным файлам — без ошибок; bun run test -- --run — 229 файлов прошли, падает только deploy/application.real.test.ts из-за отсутствия локального Docker (ECONNREFUSED docker.sock); тест схемы падает с SCHEMA_MISMATCH user.ownerId, если вернуть поле (негативная проверка).
- Ограничения: SSO- и SCIM-флоу и миграции БД в этом коммите не проверялись; сборка Next и Docker-образа не запускалась.

What:
- Bumped better-auth and @better-auth/{api-key,passkey,scim,sso} from 1.6.23 to 1.7.7; root overrides now pin better-call 1.4.0 and @better-fetch/fetch 1.3.2 to match the 1.7.7 catalog; removed the @xmldom/xmldom <0.9.0 override because @better-auth/sso 1.7 needs ^0.9.12, while samlify and xml-crypto stay on 0.8.15 through their ^0.8 ranges.
- Schema generation now uses the auth 1.7.7 CLI package (generate:drizzle script).
- The Drizzle adapter runs with transaction: true; ownerId is removed from user.additionalFields (there is no column; validateRequest derives it as the active organization owner and returns an explicitly built user); the NOT NULL organization.ownerId column is declared through schema.organization.additionalFields.
- The SCIM plugin is disabled for now: 1.7 removed the 1.6 token APIs, and the tRPC token mutation answers PRECONDITION_FAILED until the move to managed connections.
- twoFactor.enable is narrowed on method, and unlinkAccount is called with the local account.id.
- Added a test that boots a real betterAuth instance and runs the built-in 1.7 schema check for self-hosted and cloud.
Why:
- Close GHSA-j8v8-g9cx-5qf4 in @better-auth/scim 1.6.23, which is fixed only in 1.7.x.
- From 1.7.3 every request and direct auth.api call first validates the Drizzle schema; user.ownerId and organization.ownerId raised SchemaMismatchError and blocked every dashboard sign-in.
Risks:
- SCIM provisioning is unavailable until the managed connections commit.
- In transaction mode, hooks that run inside a transaction must not write through the global db.
Checks:
- Commands and results: bun install with minimumReleaseAge=259200 and an exclusion for the already locked http-cache-semantics 4.3.0 — succeeded; bun audit — only braces 3.0.3 remains (no upstream fix), GHSA-j8v8-g9cx-5qf4 is gone; bun run typecheck in packages/server — 0 errors, in apps/dokploy — only the existing next.config.mjs (outputFileTracing) error; biome check on the changed files — no errors; bun run test -- --run — 229 files passed, only deploy/application.real.test.ts fails because no local Docker is running (ECONNREFUSED docker.sock); the schema test fails with SCHEMA_MISMATCH user.ownerId when the field is put back (negative check).
- Limitations: SSO and SCIM flows and DB migrations were not exercised in this commit; the Next build and Docker image were not built.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- Добавлен user.validateUserInfo: для sso-oidc и sso-saml при create-user и link-account провайдер ищется по source.sso.providerId, и вход отклоняется с sso_email_domain_not_allowed, если canProvisionSsoMembershipForEmail не проходит (домен не подтвержден или email вне доменов провайдера). Повторные входы (sign-in) не проверяются, как в 1.6.
- SSO-ветка databaseHooks.user.create.after не вставляет второе членство, если пользователь уже состоит в организации провайдера.
- organizationProvisioning.getRole возвращает роль организации по умолчанию (resolveOrganizationDefaultRole), как и хук создания пользователя, вместо жесткого "member".
- IdP-initiated SAML явно выключен (saml.allowIdpInitiated: false), это значение по умолчанию в 1.7.
Зачем:
- В better-auth 1.7 OIDC- и SAML-колбэки коммитят пользователя, аккаунт и сессию до after-хуков и только логируют их ошибки; отказ в user.create.after больше не прерывал вход, и для запрещенного домена оставались пользователь и сессия.
- Проверка на link-account не дает IdP одного тенанта привязать SSO-аккаунт к пользователю с чужим доменом.
Риски:
- SSO-вход нового пользователя через провайдера с неподтвержденным доменом отклоняется сразу, а не после создания пользователя; итоговое поведение для таких провайдеров не меняется.
Проверки:
- Команды и результаты: bun run test -- --run __test__/auth/ __test__/sso/ __test__/traefik/forward-auth.test.ts — 77 тестов прошли; bun run typecheck в packages/server — 0 ошибок, в apps/dokploy — только существующая ошибка next.config.mjs; biome check по измененным файлам — без ошибок.
- Ограничения: реальный вход через OIDC/SAML IdP не проверялся.

What:
- Added user.validateUserInfo: for sso-oidc and sso-saml on create-user and link-account it loads the provider by source.sso.providerId and rejects the sign-in with sso_email_domain_not_allowed unless canProvisionSsoMembershipForEmail passes (unverified domain or an email outside the provider domains). Returning sign-ins are not gated, as in 1.6.
- The SSO branch of databaseHooks.user.create.after no longer inserts a second membership when the user already belongs to the provider organization.
- organizationProvisioning.getRole returns the organization default role (resolveOrganizationDefaultRole), the same one the user creation hook uses, instead of a hard-coded "member".
- IdP-initiated SAML is disabled explicitly (saml.allowIdpInitiated: false), which is the 1.7 default.
Why:
- In better-auth 1.7 the OIDC and SAML callbacks commit the user, account and session before after-hooks run and only log their errors, so a rejection thrown in user.create.after no longer stopped the sign-in and left a user and session behind for a disallowed domain.
- Gating link-account stops one tenant's IdP from linking an SSO account to a user from another domain.
Risks:
- A new user signing in through a provider whose domain is not verified is rejected up front instead of after the user is created; the final outcome for such providers does not change.
Checks:
- Commands and results: bun run test -- --run __test__/auth/ __test__/sso/ __test__/traefik/forward-auth.test.ts — 77 tests passed; bun run typecheck in packages/server — 0 errors, in apps/dokploy — only the existing next.config.mjs error; biome check on the changed files — no errors.
- Limitations: a real sign-in through an OIDC/SAML IdP was not exercised.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- ssoProviderBodySchema приведен к API 1.7: из OIDC- и SAML-маппингов убран id (старые клиенты могут его присылать, он отбрасывается); SAML cert принимает строку или список; idpMetadata обязателен и требует XML метаданных IdP или entityID; spMetadata и callbackUrl необязательны; убраны decryptionPvk и additionalParams; добавлены idpInitiatedCallbackUrl и wantAssertionsSigned.
- Диалог SAML показывает новый ACS /api/auth/sso/saml2/sp/acs/<providerId>, entity ID SP и ссылку на SP-метаданные; больше не генерирует SP XML со старым ACS, отправляет spMetadata.entityID = base URL, idpMetadata.entityID = issuer без XML, callbackUrl = /dashboard/home и маппинг без id. Поле Issuer подписано как entity ID IdP.
- Диалог OIDC больше не предлагает маппинг "User ID": в 1.7 субъект всегда проверенный sub.
- Настройки SSO показывают новый ACS для SAML-провайдеров.
- Миграция 0199_sso_better_auth_1_7 переписывает сохраненные конфиги: удаляет mapping.id, decryptionPvk и additionalParams; удаляет SP XML со старым ACS и сохраняет его entityID в spMetadata.entityID; добавляет idpMetadata.entityID = issuer для ручных SAML-конфигов; меняет callbackUrl со старого ACS на <origin>/dashboard/home. Строки обрабатываются по одной, некорректный JSON дает WARNING и не прерывает миграцию.
Зачем:
- better-auth 1.7 удалил /sso/saml2/callback/:providerId, использует сохраненный SP XML как есть, отвергает mapping.id и отказывается работать с ручным SAML-конфигом без idpMetadata.entityID; без миграции все SAML-провайдеры, созданные через UI Dokploy, перестали бы работать.
Риски:
- Администраторы должны поменять ACS URL в своих IdP (или переимпортировать SP-метаданные); до этого SAML-вход для них не работает.
- Строки с некорректным JSON в saml_config/oidc_config остаются как есть и требуют ручной правки (видны по WARNING в логе миграции).
Проверки:
- Команды и результаты: миграции 0000-0198 применены psql к временному PostgreSQL 18, засеяны строки в формате 1.6 (ручной SAML из UI, SAML с XML метаданных IdP, OIDC с mapping.id, некорректный JSON, строка уже в формате 1.7), применена 0199 — результат совпал с ожидаемым, повторный прогон ничего не меняет, строка в формате 1.7 не изменилась; мигрированные строки прочитаны настоящим better-auth 1.7.7 (spMetadata и signInSSO) — entityID SP прежний, ACS в метаданных и AuthnRequest новый; немигрированная строка 1.6 в 1.7.7 дает 400 "SAML manual IdP configuration requires idpMetadata.entityID"; bun run test -- --run __test__/sso/ __test__/auth/ __test__/traefik/forward-auth.test.ts — 80 тестов прошли; bun run typecheck — 0 ошибок в packages/server, в apps/dokploy только существующая ошибка next.config.mjs; biome check по измененным файлам — без ошибок.
- Ограничения: вход через настоящий IdP (Okta/Entra/Keycloak) не проверялся; миграция не применялась к копии рабочей БД.

What:
- ssoProviderBodySchema now follows the 1.7 API: id is gone from the OIDC and SAML mappings (older clients may still send it and it is stripped); SAML cert accepts a string or a list; idpMetadata is required and needs either the IdP metadata XML or an entityID; spMetadata and callbackUrl are optional; decryptionPvk and additionalParams are removed; idpInitiatedCallbackUrl and wantAssertionsSigned are accepted.
- The SAML dialog shows the new ACS /api/auth/sso/saml2/sp/acs/<providerId>, the SP entity ID and the SP metadata link; it no longer generates SP XML with the old ACS and sends spMetadata.entityID = base URL, idpMetadata.entityID = issuer when there is no XML, callbackUrl = /dashboard/home and a mapping without id. The Issuer field is labelled as the IdP entity ID.
- The OIDC dialog no longer offers a "User ID" mapping: in 1.7 the subject is always the verified sub.
- SSO settings show the new ACS for SAML providers.
- Migration 0199_sso_better_auth_1_7 rewrites stored configs: it removes mapping.id, decryptionPvk and additionalParams; drops SP XML that points at the old ACS and keeps its entityID in spMetadata.entityID; adds idpMetadata.entityID = issuer for manual SAML configs; rewrites an old-ACS callbackUrl to <origin>/dashboard/home. Rows are migrated one by one, and malformed JSON raises a WARNING without aborting the migration.
Why:
- better-auth 1.7 removed /sso/saml2/callback/:providerId, uses stored SP XML verbatim, rejects mapping.id and refuses manual SAML configs without idpMetadata.entityID; without the migration every SAML provider created through the Dokploy UI would stop working.
Risks:
- Admins must change the ACS URL in their IdPs (or re-import the SP metadata); SAML sign-in fails for them until then.
- Rows with malformed JSON in saml_config/oidc_config stay as they are and need a manual fix (reported as WARNING in the migration log).
Checks:
- Commands and results: migrations 0000-0198 applied with psql to a temporary PostgreSQL 18, seeded 1.6-shaped rows (manual SAML from the UI, SAML with IdP metadata XML, OIDC with mapping.id, malformed JSON, a row already in the 1.7 shape), applied 0199 — results matched expectations, a second run changes nothing, the 1.7-shaped row is untouched; migrated rows read through real better-auth 1.7.7 (spMetadata and signInSSO) — the SP entityID is unchanged and the ACS in metadata and AuthnRequest is the new one; an unmigrated 1.6 row fails in 1.7.7 with 400 "SAML manual IdP configuration requires idpMetadata.entityID"; bun run test -- --run __test__/sso/ __test__/auth/ __test__/traefik/forward-auth.test.ts — 80 tests passed; bun run typecheck — 0 errors in packages/server, only the existing next.config.mjs error in apps/dokploy; biome check on the changed files — no errors.
- Limitations: sign-in through a real IdP (Okta/Entra/Keycloak) was not exercised; the migration was not applied to a copy of the production database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- SCIM снова включен на @better-auth/scim 1.7.7 в режиме managedConnections: подключения создаются по одному на организацию (provisioningDomainId = id организации), Better Auth хранит только HMAC-дайджесты токенов; ключ выводится из BETTER_AUTH_SECRET (scimCredentialHashSecret, по образцу deriveCookieSecret).
- identity.resolveUser перепривязывает пользователей, заведенных SCIM-плагином 1.6, по новой таблице scim_legacy_identity (organization_id + externalId или userName), остальные создаются заново; по email плагин не связывает.
- projection.reconcileUser ведет членство: активный пользователь каталога состоит в организации с ролью по умолчанию, деактивированный или удаленный теряет членство; роли, назначенные в Dokploy, не меняются, владелец никогда не удаляется. SCIM-ветка user.create.after больше не трогает членство.
- tRPC-роутер scim переписан: listConnections, createConnection, rotateCredential, revokeCredential, decommissionConnection, events; только enterprise owner/admin, все вызовы привязаны к активной организации и текущему пользователю; токен возвращается только при создании и ротации с Cache-Control: no-store; NOT_FOUND и CONFLICT от Better Auth передаются как коды tRPC.
- Диалог SCIM показывает подключения и токены (срок, последнее использование, статус), выдает токен один раз, умеет ротацию с перекрытием, отзыв и необратимый вывод из эксплуатации.
- Схема: 10 таблиц плагина SCIM 1.7 (как их выдает auth generate) и scim_legacy_identity; миграции 0200 (создание таблиц), 0201 (перенос связок из legacy SCIM-аккаунтов и удаление этих аккаунтов, кроме совпадающих с SSO-провайдером), 0202 (удаление scim_provider). auth-cli.ts повторяет плагин и transaction: true, auth-schema2.ts перегенерирован CLI auth 1.7.7.
- migrate-auth-secret.ts напоминает, что после ротации секрета SCIM-токены нужно выпустить заново.
Зачем:
- SCIM 1.7 заменил схему, API и модель подключений; старые токены и scim_provider не переносятся, а без явной перепривязки повторный провижининг ранее заведенных пользователей давал бы 409.
- Обновление закрывает GHSA-j8v8-g9cx-5qf4: персональных SCIM-подключений без владельца больше нет.
Риски:
- После выкладки провижининг стоит, пока администратор не создаст подключение, не обновит токен в IdP и не запустит полную синхронизацию пользователей и групп.
- Деактивация в IdP теперь снимает членство в организации (раньше на self-hosted active=false давал ошибку).
- Ротация BETTER_AUTH_SECRET делает все SCIM-токены недействительными.
Проверки:
- Команды и результаты: миграции 0200-0202 применены psql к временному PostgreSQL 18 с данными SCIM в формате 1.6 — связка организационного провайдера перенесена, legacy SCIM-аккаунты удалены, аккаунт при совпадении id с SSO-провайдером, SSO- и парольный аккаунты и членство сохранены, scim_provider удалена; настоящий lib/auth.ts Dokploy против этой БД: managed-подключение создано, POST /scim/v2/Users — 201 и членство с ролью по умолчанию, legacy-пользователь с externalId привязан к прежнему пользователю без 409, PATCH active=false снимает членство, active=true возвращает, чужая организация — NOT_FOUND, отозванный токен — 401; таблицы SCIM в scim.ts совпадают с выводом auth generate; bun run test -- --run — 231 файл, 2050 тестов прошли, падает только deploy/application.real.test.ts (ECONNREFUSED docker.sock); bun run typecheck — 0 ошибок в packages/server, в apps/dokploy только существующая ошибка next.config.mjs; biome check по измененным файлам — без ошибок.
- Ограничения: провижининг из настоящего IdP (Okta/Entra) и Groups не проверялись; UI-диалог проверен только типизацией; schema.dbml не обновлялся (он сильно отстал от схемы еще до этой задачи).

What:
- SCIM is enabled again on @better-auth/scim 1.7.7 with managedConnections: one connection per organization (provisioningDomainId = organization id), and Better Auth stores only HMAC digests of tokens; the key is derived from BETTER_AUTH_SECRET (scimCredentialHashSecret, same pattern as deriveCookieSecret).
- identity.resolveUser relinks users provisioned by the 1.6 SCIM plugin through the new scim_legacy_identity table (organization_id + externalId or userName); everyone else is created; the plugin never links by email.
- projection.reconcileUser maintains membership: an active directory user is a member with the organization default role, a deactivated or deleted one loses the membership; roles assigned in Dokploy are kept and the owner is never removed. The SCIM branch of user.create.after no longer touches membership.
- The scim tRPC router is rewritten: listConnections, createConnection, rotateCredential, revokeCredential, decommissionConnection, events; enterprise owner/admin only, every call bound to the active organization and the current user; the token is returned only on create and rotate with Cache-Control: no-store; NOT_FOUND and CONFLICT from Better Auth surface as tRPC codes.
- The SCIM dialog lists connections and tokens (expiry, last use, status), shows a token once, and supports overlapping rotation, revocation and irreversible decommissioning.
- Schema: the 10 SCIM 1.7 plugin tables (as emitted by auth generate) plus scim_legacy_identity; migrations 0200 (create tables), 0201 (copy links from legacy SCIM accounts and delete those accounts, except ones whose id matches an SSO provider), 0202 (drop scim_provider). auth-cli.ts mirrors the plugin and transaction: true, and auth-schema2.ts is regenerated with the auth 1.7.7 CLI.
- migrate-auth-secret.ts reminds that SCIM tokens must be issued again after rotating the secret.
Why:
- SCIM 1.7 replaced the schema, the API and the connection model; legacy tokens and scim_provider are not migrated, and without explicit relinking reprovisioning previously provisioned users would fail with 409.
- The upgrade closes GHSA-j8v8-g9cx-5qf4: ownerless personal SCIM connections no longer exist.
Risks:
- After deployment provisioning is paused until an admin creates a connection, updates the token in the IdP and runs a full user and group sync.
- Deactivating a user in the IdP now removes the organization membership (on self-hosted, active=false used to fail).
- Rotating BETTER_AUTH_SECRET invalidates every SCIM token.
Checks:
- Commands and results: migrations 0200-0202 applied with psql to a temporary PostgreSQL 18 with 1.6-shaped SCIM data — the organization provider link was copied, legacy SCIM accounts were deleted, the account whose id matches an SSO provider, the SSO and password accounts and the membership were kept, scim_provider was dropped; Dokploy's real lib/auth.ts against that database: managed connection created, POST /scim/v2/Users — 201 and a membership with the default role, a legacy user with externalId linked to its existing user without 409, PATCH active=false removes the membership and active=true restores it, a foreign organization gets NOT_FOUND, a revoked token gets 401; the SCIM tables in scim.ts match the auth generate output; bun run test -- --run — 231 files, 2050 tests passed, only deploy/application.real.test.ts fails (ECONNREFUSED docker.sock); bun run typecheck — 0 errors in packages/server, only the existing next.config.mjs error in apps/dokploy; biome check on the changed files — no errors.
- Limitations: provisioning from a real IdP (Okta/Entra) and Groups were not exercised; the UI dialog was only type-checked; schema.dbml was not updated (it was already far behind the schema before this work).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- Добавлен docs/better-auth-1.7-upgrade.md на русском и английском: подготовка (резервная копия, пауза SCIM, проверка дубликатов аккаунтов), новый SAML ACS URL и ссылка на SP-метаданные, отключенный IdP-initiated вход, изменения маппинга OIDC, переход SCIM на подключения с выпуском новых токенов и полной синхронизацией, поведение деактивации и ротация секрета.
Зачем:
- Обновление требует действий администраторов SAML и SCIM в их IdP; без инструкции SAML-вход и провижининг останутся сломанными после выкладки.
Риски:
- Не выявлены: изменение только в документации.
Проверки:
- Команды и результаты: не запускались (только документация); адреса и шаги сверены с кодом и проверками предыдущих коммитов.
- Ограничения: шаги для конкретных IdP (Okta, Entra ID) не проверялись на настоящих тенантах.

What:
- Added docs/better-auth-1.7-upgrade.md in Russian and English: preparation (backup, pausing SCIM, the duplicate account check), the new SAML ACS URL and SP metadata link, disabled IdP-initiated sign-in, OIDC mapping changes, the move of SCIM to connections with new tokens and a full sync, deactivation behavior, and secret rotation.
Why:
- The upgrade needs SAML and SCIM admins to act in their IdPs; without instructions SAML sign-in and provisioning stay broken after deployment.
Risks:
- None identified: documentation only.
Checks:
- Commands and results: not run (documentation only); URLs and steps were checked against the code and the checks of the previous commits.
- Limitations: steps for specific IdPs (Okta, Entra ID) were not verified on real tenants.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- apps/dokploy/migration.ts выставляет process.exitCode = 1, если миграция упала; раньше ошибка только логировалась и процесс завершался с кодом 0.
- Добавлен тест __test__/setup/migration-exit-code.test.ts: при ошибке миграции код выхода 1 и соединение закрыто, при успехе код не меняется.
Зачем:
- Контейнер стартует через `node dist/migration.mjs && node dist/server.mjs`; с better-auth 1.7 сервер на недомигрированной схеме отклоняет любой вход, а ненулевой код не дает ему стартовать и оставляет повтор оркестратору.
Риски:
- При постоянной ошибке миграции сервис уходит в цикл перезапусков вместо работы с битой схемой; это видно в docker service ps и логах.
Проверки:
- Команды и результаты: bun run test -- --run __test__/setup/migration-exit-code.test.ts — 2 теста прошли; без исправления первый тест падает (негативная проверка); bun run typecheck в apps/dokploy — только существующая ошибка next.config.mjs; biome check по измененным файлам — без ошибок.
- Ограничения: перезапуск контейнера после упавшей миграции на сервере не воспроизводился.

What:
- apps/dokploy/migration.ts sets process.exitCode = 1 when a migration fails; before, the error was only logged and the process exited with code 0.
- Added __test__/setup/migration-exit-code.test.ts: a failed migration exits with code 1 and closes the connection, a successful one leaves the code untouched.
Why:
- The container starts with `node dist/migration.mjs && node dist/server.mjs`; with better-auth 1.7 a server on a partially migrated schema rejects every sign-in, while a non-zero code keeps it from starting and leaves the retry to the orchestrator.
Risks:
- A persistently failing migration makes the service restart in a loop instead of running on a broken schema; this shows in docker service ps and the logs.
Checks:
- Commands and results: bun run test -- --run __test__/setup/migration-exit-code.test.ts — 2 tests passed; without the fix the first test fails (negative check); bun run typecheck in apps/dokploy — only the existing next.config.mjs error; biome check on the changed files — no errors.
- Limitations: a container restart after a failed migration was not reproduced on a server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- Миграция 0201 переносит legacy SCIM-связки только если таблица scim_provider существует, 0202 удаляет ее через DROP TABLE IF EXISTS.
- Новая миграция 0203_repair_skipped_upstream_columns идемпотентно (ADD COLUMN IF NOT EXISTS) добавляет колонки, которые создавали пропущенные upstream-миграции: two_factor.verified, failed_verification_count, locked_until (0173) и backup.includeEncryptionKey (0174).
Зачем:
- Мигратор drizzle применяет только записи журнала новее последней примененной. На dev-базе форка миграции с более поздними метками (1781045439163/4 и 1784500000000) применились раньше, чем пришли upstream 0173 и 0174, поэтому те были молча пропущены: нет таблицы scim_provider и колонок two_factor. Без этого 0201/0202 падали бы на таких базах, а включение 2FA уже сейчас падает с "column verified does not exist".
Риски:
- Не выявлены: все операторы ничего не делают там, где 0173 и 0174 уже применились.
Проверки:
- Команды и результаты: на dev-базе (timeweb) только чтение — по хешам журнала не применены 0173 и 0174, 0169 применена в старой версии; scim_provider отсутствует, у two_factor 4 колонки, backup.includeEncryptionKey есть. Копия схемы dev (pg_dump --schema-only + drizzle.__drizzle_migrations) восстановлена в локальный PostgreSQL 18, настоящий migration.ts применил 0199-0203 (205 записей, только NOTICE, "Migration complete"); на этой копии настоящий lib/auth.ts: регистрация владельца, вход, validateRequest, включение и подтверждение 2FA, повторный вход со вторым фактором, сессия по API-ключу и SCIM POST /Users прошли; без колонок 0203 включение 2FA падает с "column verified does not exist".
- Ограничения: другие базы форка с иным набором пропущенных миграций не проверялись.

What:
- Migration 0201 moves legacy SCIM links only when the scim_provider table exists, and 0202 drops it with DROP TABLE IF EXISTS.
- New migration 0203_repair_skipped_upstream_columns idempotently (ADD COLUMN IF NOT EXISTS) adds the columns the skipped upstream migrations created: two_factor.verified, failed_verification_count, locked_until (0173) and backup.includeEncryptionKey (0174).
Why:
- The drizzle migrator applies only journal entries newer than the last applied one. On the fork's dev database, migrations with later timestamps (1781045439163/4 and 1784500000000) were applied before upstream 0173 and 0174 arrived, so those were silently skipped: there is no scim_provider table and no two_factor columns. Without this, 0201/0202 would fail on such databases, and enabling 2FA already fails there with "column verified does not exist".
Risks:
- None identified: every statement is a no-op where 0173 and 0174 were applied.
Checks:
- Commands and results: read-only on the dev database (timeweb) — by journal hashes 0173 and 0174 were never applied and 0169 was applied in an older version; scim_provider is missing, two_factor has 4 columns, backup.includeEncryptionKey exists. A copy of the dev schema (pg_dump --schema-only plus drizzle.__drizzle_migrations) was restored into a local PostgreSQL 18, the real migration.ts applied 0199-0203 (205 rows, only NOTICEs, "Migration complete"); on that copy the real lib/auth.ts passed owner sign-up, sign-in, validateRequest, enabling and confirming 2FA, a second sign-in with the second factor, an API key session and SCIM POST /Users; without the 0203 columns enabling 2FA fails with "column verified does not exist".
- Limitations: other fork databases with a different set of skipped migrations were not checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Что:
- В upgrade-integration-test добавлен необязательный вход target_image: версия B берется из него вместо dokploy/dokploy:<target>, пары по-прежнему строятся по официальным тегам ниже target_version.
- Для образа из ghcr.io выполняется docker login через GITHUB_TOKEN (permissions: packages: read), docker service update запускается с --with-registry-auth.
- После обновления добавлен шаг: админ, созданный до обновления, входит через /api/auth/sign-in/email, и /api/auth/get-session возвращает его email.
Зачем:
- Проверить образ форка (better-auth 1.7 и его миграции) обновлением с официальных версий; раньше workflow умел обновлять только на официальный образ.
- Схема, которую отвергает библиотека аутентификации, по-прежнему отдает страницы, но ломает любой вход, поэтому проверки "сервис отвечает" недостаточно.
Риски:
- Не выявлены: без target_image поведение прежнее, кроме нового шага входа.
Проверки:
- Команды и результаты: YAML разобран через python yaml.safe_load; сам workflow запускается после push.
- Ограничения: actionlint локально не запускался.

What:
- upgrade-integration-test gains an optional target_image input: version B comes from it instead of dokploy/dokploy:<target>, while pairs are still built from official tags below target_version.
- For a ghcr.io image the job logs in with GITHUB_TOKEN (permissions: packages: read), and docker service update runs with --with-registry-auth.
- A post-upgrade step signs in the admin created before the upgrade through /api/auth/sign-in/email and checks that /api/auth/get-session returns its email.
Why:
- Test the fork image (better-auth 1.7 and its migrations) by upgrading from official versions; the workflow could only upgrade to the official image.
- A schema the auth library rejects still serves pages but breaks every sign-in, so "the service responds" is not enough.
Risks:
- None identified: without target_image the behavior is unchanged apart from the new sign-in step.
Checks:
- Commands and results: YAML parsed with python yaml.safe_load; the workflow itself runs after the push.
- Limitations: actionlint was not run locally.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant