Depolar arası otomasyon + ikinci faktörde kilidi gerçekten uygula - #4
Merged
Conversation
Both MFA routes consulted the lock only on the FAILURE path: a wrong code called `registerFailure` and the response then said `account_locked`. A correct code never met the lock at all — it was verified, `clearFailures` ran, and `mfa_pending` was cleared. So a locked account still signed in, and the lock counted misses and announced a state it never enforced. The password step does check first (`auth/signin/route.ts:47`), so the gap was the second factor alone — precisely the step whose reason to exist is that a stolen password should not be enough. An attacker holding the password gets a session in `mfa_pending` and can then guess the code with no effective budget, which is what the comment above `POST` already promised would not happen. `readLockState()` existed in `panel/lib/auth/lockout.ts` for exactly this and was not called anywhere; both routes now call it immediately after the session check and refuse before spending a code. `ovurrsl/panel`'s copies of both routes already do this — the guard was dropped while vendoring, not designed away, so this restores the upstream behaviour rather than inventing a policy. Recovery codes matter more here than TOTP: they are the credential that survives losing the authenticator, so an unbounded guessing budget against them is the weakest point in the whole second factor. Not covered by a test: this repository has no harness for API routes (every existing test is a pure-function test — `scene-api-security`, `env-file`, `graph-schema`, `mail-template`), and these routes need a database and a live session. A real guard would assert that a locked account is refused even when the submitted code is correct. Verified here by reading the order of operations and by `tsc --noEmit` over `apps/editor` (no errors in either file). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The sync would have created `scenes-tab.tsx` and `guides-tab.tsx` upstream and
overwritten `console-tabs.ts` to register them, while `ROUTES` (create-new is
`false`) declined to push the endpoints behind them. The standalone console
would have gained two rail entries whose fetches 404.
The endpoints cannot exist upstream, which is why declining them is right and
the tabs are what is wrong:
/api/guides imports the editor's own `@/lib/guides-content` and
`@/lib/scene-api-security` — absent in the console repo,
so the route would not even build there.
/api/admin/scenes reads a `scenes` table. No migration in the synced set
creates one; `004_site_scene.sql` only adds
`sites.scene_id`. The console's schema has no scenes.
So all three files join `EDITOR_OWNED`. Verified with `--check` against a
checkout of ovurrsl/panel: 133 planned actions before, 130 after, and the
three named files gone from the list.
The cost, stated plainly: changes to the SHARED tabs' metadata in
`console-tabs.ts` no longer reach upstream either. That is what a file which
must legitimately differ between two deployments costs. Splitting the list
into a base the console owns and an extension the editor adds would remove the
conflict properly; the comment says so, so the next person does not read this
as the intended end state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
Verified by applying the sync to a real checkout of ovurrsl/panel and running
that repository's own checks. Its test suite passed with all of these broken —
`tsc --noEmit` is what caught them, so it is the check that matters when this
list changes.
`tab-content.tsx` imports both `guides-tab` and `scenes-tab` and switches on
their names. Holding the tabs back while pushing their only caller turned a
missing feature into a repository that does not compile:
tab-content.tsx(4,27): Cannot find module '@/components/console/guides-tab'
tab-content.tsx(32,10): Type '"scenes"' is not comparable to type
'"audit" | "integrations" | ... | "users"'
`api/health/route.ts` is the same shape one layer down — the editor's copy
imports `@/lib/auth/db` and `@/lib/scene-store-server`, neither of which exists
upstream. It is an existing upstream path, so `ROUTES` overwrote it.
Test files now never cross. The two repositories run different runners — this
one is on `bun:test`, the console on vitest — so `mail-template.test.ts` landed
upstream as a file vitest does not collect (its config globs `tests/**`) and
`tsc` cannot resolve (`Cannot find module 'bun:test'`). The console's own suite
lives in `tests/`, outside every mapping here, so nothing in this direction can
reach it either way.
After: 127 planned actions. In the panel checkout, `npm run typecheck` clean and
`npm test` 59/59.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The console repository is the home of the console now, so the direction that matters day to day is inward. `--pull` adds it, inverting the existing tables rather than declaring its own: a mapping added for one direction is a mapping the other gets for free, which is the only way two directions stay honest. Inverting is not a swap. The console's tree nests where the editor's does not — `src/app/api` and `src/app/globals.css` both live INSIDE `src/app` — so walking `src/app` naively drags the console's endpoints into `app/(panel)/api/` and its stylesheet into `app/(panel)/globals.css`, both wrong and both silent. Longest source path first, and the first pair to claim a file keeps it. `EDITOR_OWNED` needed no second list: it holds console-side paths, so it is the origin on pull and the target on push, and it means the same thing either way — the editor's copy is the authority. `create` is dropped on pull. Its `false` on `ROUTES` protects the CONSOLE from receiving editor-only routes; the mirror risk, the editor receiving a console route it lacks, is not a risk but the point. ## The asymmetry this surfaced A dry run against the real console reported one file, `console-shell.tsx`, while the push direction called the same pair in sync. That contradiction is the signature of a real defect: the forward rewrite is LOSSY. `@panel/x` and a literal `@/x` both leave as `@/x`, so the inverse cannot tell them apart. The file held a comment naming `@/lib/escape-layers`; push left it, pull would rewrite it, and the two directions would have "fixed" each other forever. Fixed at the source — in the editor that module IS `@panel/lib/escape-layers`, so the comment is now true as well as stable, and both directions report clean. The constraint it implies is written down where the rewrite lives: a synced file under `apps/editor/panel/` must never contain a bare `@/`, in an import or in prose, and the way that mistake shows up is exactly the contradiction above. ## Verified against the real repository Not a dry run only. With ovurrsl/panel checked out at its merged `main`: - both directions report in sync, exit 0 — the round trip is stable - a change to `src/lib/cn.ts` plus a NEW `src/lib/__pull-probe.ts` importing `@/lib/cn`: pull placed both under `apps/editor/panel/lib/`, and the new file's import arrived as `@panel/lib/cn` - probes removed, both repositories clean, both directions in sync again `biome check .` clean. `bun test` unchanged from base (the one failure is the pre-existing `graph-schema` plugin-kind case, which fails on the base branch identically). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The console repository is the home of the console, so the automatic direction is inward. `pull-panel.yml` runs the sync with `--pull` hourly (and on demand, and on a `panel-updated` repository dispatch the console can fire once it has a token), commits to the integration branch, and stops there. Gated on `bun run check-types`, the same command CI runs. Pushing straight to the branch is what makes this automatic, and automatic without a gate means one console commit that does not compile here quietly breaks the branch the bundle is built from. Fails red, pushes nothing. A scheduled run starts on the default branch, so the branch to update is named rather than inherited: `vars.INTEGRATION_BRANCH`, falling back to the branch it is today. Renaming it is a repository-variable edit, not a workflow edit. ## Why the outbound sync is now manual only Both directions on `push` would give one file two masters: a change made in the console flows here, the outbound sync fires on that very commit and pushes it straight back, and the two spend the day answering each other — whichever ran last looking right. It keeps `workflow_dispatch` because it is still the tool for seeding the console after a change had to be made here, which the comment now says. ## And a bug that would have started with the first merge `gh pr view <branch>` resolves closed and merged pull requests too, so the plain existence check would have edited the body of the pull request just merged and never opened a new one — while the force-push to `sync/from-editor` had already landed. Files would move; nothing would ask anyone to look. It now asks for the state and only edits when it is OPEN. Both files parse; `biome check .` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
The last manual link. The warehouse plugin is a git dependency pinned to an exact sha and it compiles into the app, so a plugin release reaches the site only when this repository moves the pin AND the bundle is rebuilt. Doing that by hand is what left a set of freeze fixes sitting unreleased while the plugin's main had moved on and production had not. `bump-plugin.yml` compares the pin with the plugin's head, moves it, relocks with a real `bun install` (the lockfile records the resolved tarball's sha512, which is why `relock.yml` exists at all), type checks, and commits. Needs no secret: the plugin repository is public, so both reading its head and resolving the dependency work with nothing configured. Gated on `bun run check-types` for the reason an exact pin exists — a plugin release that does not compile against this editor should stop here, visibly, rather than reach the branch the bundle is built from. ## The handoff, which is not optional Both this and `pull-panel.yml` end by dispatching `deploy-bundle.yml` explicitly. A push made with GITHUB_TOKEN starts no workflow runs — `relock.yml` already says so in its own header — so without the dispatch the pin moves, the console lands, and the site never rebuilds. The chain would look wired and stop one step short, which is the same shape as the failure this whole change set is about. Verified against the real files rather than by reading: the pin grep returns `49b2f16…` from `apps/editor/package.json`, `git ls-remote` returns the plugin's head, and the `sed` rewrites exactly the one occurrence. Both workflow files parse. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
ovurrsl
force-pushed
the
claude/mfa-lockout-and-panel-sync
branch
from
August 5, 2026 12:14
37df89d to
23d72ad
Compare
… mirror upstream into `main` Four workflows disagreed about which branch was the real one — `ci`, `mcp-ci` and `deploy-bundle` watched `main`, `relock` watched the working branch, `sync-panel` watched both. That disagreement is not cosmetic: it is why a pin bump merged to `main` never deployed (the branch it landed on could not build, because `next.config.ts` there carries no `output: 'standalone'`), and why a pull request against the branch all the work happens on gets no CI at all. They now all name `integration`, and `main` becomes what a fork's `main` should be: a clean mirror of `pascalorg/editor`, no local commits, ever. ## Why that split, and why `integration` is the default branch A mirror with nothing of ours on it can only fast-forward, so taking upstream never conflicts and never needs a decision. Every conflict then has exactly one place to happen — the pull request from `main` into `integration` — instead of being spread across whichever branch someone last merged into. `integration` has to be the DEFAULT branch for this to work at all, and that is not a preference: GitHub runs scheduled workflows only from the default branch. Leaving `main` as the default while emptying it of our workflows would have silently killed every schedule here — the console pull, the plugin bump, the upstream check — while the files sat on a branch no scheduler reads. ## mirror-upstream.yml Fast-forwards `main` to `upstream/main` daily, then opens one long-lived pull request into `integration` that accumulates whatever upstream has added. It refuses to force. A `main` that cannot fast-forward is a `main` somebody has committed to, and erasing that quietly is worse than a red job. ## relock.yml Its self-triggering hack existed because dispatch needs the file on the default branch and the working branch was not it. It is now, so the hack is gone and it is dispatched like anything else. The trailing marker comments stay as a record of which relocks were run that way, not as a mechanism. Routine plugin bumps no longer come here at all — `bump-plugin.yml` moves the pin and relocks in one job. All ten workflow files parse; `biome check .` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012VUVkZWKGN5B2oyEnAPjGg
ovurrsl
marked this pull request as ready for review
August 5, 2026 12:29
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Yedi commit. Biri güvenlik açığı, gerisi depolar arası otomasyonu kuruyor ve iş akışlarının hangi dalın asıl olduğuna dair anlaşmazlığını bitiriyor.
1. İkinci faktörde kilit sayıyor ama engellemiyordu
apps/editor/app/api/mfa/{verify,recovery}/route.tsHer iki route da kilide yalnız başarısızlık yolunda bakıyordu. Yanlış kod
registerFailureçağırıyor, cevapaccount_lockeddiyordu. Doğru kod kilide hiç uğramıyordu — doğrulanıyor,clearFailureskoşuyor, oturum açılıyordu. Yani kilitli hesap yine de içeri giriyordu; kilit, uygulamadığı bir durumu ilan ediyordu.Şifre adımında kontrol var (
auth/signin/route.ts:47), boşluk yalnız ikinci faktördeydi — var olma sebebi "şifre çalınsa bile yetmesin" olan adım.readLockState()bu iş için yazılmış ve hiçbir yerde çağrılmıyordu.ovurrsl/panel'deki karşılıkları zaten çağırıyor, yani kontrol tasarımla kaldırılmamış, vendoring sırasında düşmüş.Test yok, sebebi: bu depoda API route'ları için düzenek yok (mevcut testlerin hepsi saf fonksiyon testi). Gerçek bir koruma testi kod doğru olsa bile kilitli hesabın reddedildiğini doğrulardı. İşlem sırası okunarak ve
tscile doğrulandı.2–3. Panel aktarımını konsolun derlenebileceği hâle getirmek
Aktarımı gerçek
ovurrsl/paneldeposuna uygulayıp o deponun kendi kontrollerini koşturarak bulundu. Test paketi hepsi bozukken de geçiyordu — yakalayantsc --noEmitoldu.Konsola gitmemesi gereken dosyalar (
EDITOR_OWNED):scenes-tab.tsx,guides-tab.tsx/api/guideseditörün@/lib/guides-content'ini kullanıyor,/api/admin/sceneshiçbir migration'ın kurmadığıscenestablosunu okuyorconsole-tabs.tstab-content.tsxapi/health/route.ts@/lib/auth/dbve@/lib/scene-store-server'ını kullanıyorTest dosyaları da artık geçmiyor: iki depo farklı koşucu kullanıyor (
bun:test↔ vitest), yani gönderilen test ne koşulabiliyor ne çözülebiliyordu.Sonuç: 133 → 127 eylem. Panel deposunda
npm run typechecktemiz,npm test59/59.4. Ters yön — konsolu içeri çekmek
Konsolun evi kendi deposu olduğuna göre asıl yön içeri.
--pullbunu ekliyor, aynı tabloları tersine çevirerek — biri için eklenen eşleme diğerine bedava geliyor, yoksa ikisi zamanla ayrışır.Tersine çevirmek düz bir takas değil: konsolun ağacı editörünkinin iç içe olmadığı yerde iç içe (
src/app/apivesrc/app/globals.cssikisi desrc/app'in içinde). Naif yürüyüş konsolun servisleriniapp/(panel)/api/'ye sürüklerdi. En uzun kaynak yol önce, ilk sahiplenen tutar.Ve bu bir asimetri ortaya çıkardı: kuru çalıştırma bir dosyayı gösterirken ileri yön aynı çifti "senkron" diyordu. O çelişki gerçek bir kusurun imzası — ileri dönüşüm kayıplı:
@panel/xve düz@/xikisi de@/xoluyor. Bir yorum satırı@/lib/escape-layersdiyordu; iki yön sonsuza dek birbirini "düzeltecekti". Kaynağında düzeltildi.Gerçek depoya karşı doğrulandı: iki yön de senkron (çıkış 0); panelde bir dosya değiştirilip içe aktarımlı yeni bir dosya eklendi → ikisi de doğru klasöre indi, içe aktarım
@panel/'e dönüştü; denemeler silindi, iki depo temiz.5–6. Otomatik akış
pull-panel.ymlbump-plugin.ymlbun installile relock eder, tip denetiminden geçerse yazarsync-panel.ymlDevir açıkça yapılıyor, ve bu isteğe bağlı değil: GITHUB_TOKEN ile yapılan bir push hiçbir iş akışını tetiklemiyor —
relock.ymlbunu kendi başlığında zaten söylüyor. Devir olmadan pin taşınır, konsol iner, site hiç yeniden derlenmez; zincir kurulu görünüp bir adım eksik kalırdı, ki bu tam da bu değişiklik setinin konusu olan hatanın şekli.bump-pluginhiçbir yeni anahtar istemiyor — eklenti deposu açık.Ayrıca
sync-panel'de birleşmeyle başlayacak bir hata kapatıldı:gh pr view <dal>kapanmış ve birleşmiş önerileri de çözüyor, yani az önce birleşen önerinin metnini güncelleyip yenisini hiç açmayacaktı — force-push ise çoktan inmiş olacaktı. Artık durum soruluyor.7. Dal düzeni
Dört iş akışı hangi dalın asıl olduğunda anlaşamıyordu. Bu kozmetik değil:
maine birleşen bir pin bump'ının neden hiç yayınlanmadığının (o dal derlenemiyor —next.config.ts'indeoutput: 'standalone'yok) ve bütün işin yapıldığı dala açılan bir önerinin neden hiç test almadığının sebebi buydu.Hepsi artık
integrationdiyor.mainise bir fork'unmaini ne olmalıysa o oluyor:pascalorg/editor'ün temiz aynası, üzerinde bizim commit'imiz yok.Neden bu ayrım: üzerinde bizden hiçbir şey olmayan bir ayna yalnızca fast-forward olabilir, yani upstream almak hiç çakışmaz ve hiç karar gerektirmez. Çakışmaların tek bir yeri olur:
main→integrationönerisi.integrationvarsayılan dal olmak zorunda — bu tercih değil: GitHub zamanlanmış iş akışlarını yalnız varsayılan daldan koşturuyor.mainvarsayılan kalıp iş akışlarımızdan boşaltılsaydı buradaki her zamanlama sessizce ölürdü.mirror-upstream.ymlmaini günlük fast-forward eder veintegration'a tek bir uzun ömürlü öneri açar. Force etmeyi reddediyor: fast-forward olamayan bir ayna, birinin commit'lediği bir aynadır; onu sessizce silmek kırmızı bir işten kötüdür.Doğrulama
biome check .temiz (1742 dosya)bun run check-types— taban dalla aynı hata sayısı (4, ikisi de bu ortamın ödünçnode_modules'ından)bun test— taban dalla aynı (düşen tek testgraph-schema'nın eklenti kind vakası, taban dalda da düşüyor)typechecktemiz,npm test59/59Bilinen, bu PR'da olmayan
001_init.sqlve002_roles_and_requests.sqlgeçmişe dönük düzenlenmiş.migrate.tsmigration'ları yalnız dosya adına göre izliyor — zaten göç etmiş bir veritabanında bu düzenlemeler sessizce uygulanmaz. Yeni bir008_*.sqltercih edilebilirdi.mail.ts'tepool: truebilinçli kaldırılmış ama panelin HEAD commit'inin amacını tersine çeviriyor.Birleştikten sonra
integrationvarsayılan dal yapılmalı (Settings → Branches), yoksa zamanlanmış işlerin hiçbiri koşmaz. Ondan sonramainPascal'ın aynasına sıfırlanabilir — ölçüldü:mainde olupintegration'da olmayan 4 commit var, üçü tekrarlanmış pin commit'i, dördüncüsü yalnızdeploy-bundle.yml'ı ekliyor ve o dosyaintegration'da zaten var. Kaybolan bir şey yok.