Skip to content

Depolar arası otomasyon + ikinci faktörde kilidi gerçekten uygula - #4

Merged
ovurrsl merged 7 commits into
integrationfrom
claude/mfa-lockout-and-panel-sync
Aug 5, 2026
Merged

Depolar arası otomasyon + ikinci faktörde kilidi gerçekten uygula#4
ovurrsl merged 7 commits into
integrationfrom
claude/mfa-lockout-and-panel-sync

Conversation

@ovurrsl

@ovurrsl ovurrsl commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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

Her iki route da kilide yalnız başarısızlık yolunda bakıyordu. Yanlış kod registerFailure çağırıyor, cevap account_locked diyordu. Doğru kod kilide hiç uğramıyordu — doğrulanıyor, clearFailures koş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 tsc ile doğrulandı.

2–3. Panel aktarımını konsolun derlenebileceği hâle getirmek

Aktarımı gerçek ovurrsl/panel deposuna uygulayıp o deponun kendi kontrollerini koşturarak bulundu. Test paketi hepsi bozukken de geçiyordu — yakalayan tsc --noEmit oldu.

Konsola gitmemesi gereken dosyalar (EDITOR_OWNED):

dosya neden
scenes-tab.tsx, guides-tab.tsx Servisleri konsolda var olamaz: /api/guides editörün @/lib/guides-content'ini kullanıyor, /api/admin/scenes hiçbir migration'ın kurmadığı scenes tablosunu okuyor
console-tabs.ts O iki sekmeyi kaydediyor
tab-content.tsx O iki sekmeyi içe aktarıyor — sekmeleri tutup çağıranı göndermek, eksik özelliği derlenmeyen depoya çevirir
api/health/route.ts Editörün @/lib/auth/db ve @/lib/scene-store-server'ını kullanıyor

Test 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 typecheck temiz, npm test 59/59.

4. Ters yön — konsolu içeri çekmek

Konsolun evi kendi deposu olduğuna göre asıl yön içeri. --pull bunu 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/api ve src/app/globals.css ikisi de src/app'in içinde). Naif yürüyüş konsolun servislerini app/(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/x ve düz @/x ikisi de @/x oluyor. Bir yorum satırı @/lib/escape-layers diyordu; 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ış

iş akışı ne yapar
pull-panel.yml Konsolu saatte bir çeker, tip denetiminden geçerse entegrasyon dalına yazar
bump-plugin.yml Eklenti pin'ini karşılaştırır, taşır, gerçek bun install ile relock eder, tip denetiminden geçerse yazar
sync-panel.yml Artık yalnız elle. İki yön birden otomatik olsaydı biri diğerinin yazdığını geri yazardı

Devir 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.yml bunu 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-plugin hiç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'inde output: '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 integration diyor. main ise bir fork'un maini 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: mainintegration önerisi.

integration varsayılan dal olmak zorunda — bu tercih değil: GitHub zamanlanmış iş akışlarını yalnız varsayılan daldan koşturuyor. main varsayılan kalıp iş akışlarımızdan boşaltılsaydı buradaki her zamanlama sessizce ölürdü.

mirror-upstream.yml maini günlük fast-forward eder ve integration'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

  • On iş akışı dosyasının hepsi parse ediyor
  • 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 test graph-schema'nın eklenti kind vakası, taban dalda da düşüyor)
  • Panel aktarımı gerçek depoya karşı: typecheck temiz, npm test 59/59

Bilinen, bu PR'da olmayan

  1. 001_init.sql ve 002_roles_and_requests.sql geçmişe dönük düzenlenmiş. migrate.ts migration'ları yalnız dosya adına göre izliyor — zaten göç etmiş bir veritabanında bu düzenlemeler sessizce uygulanmaz. Yeni bir 008_*.sql tercih edilebilirdi.
  2. mail.ts'te pool: true bilinçli kaldırılmış ama panelin HEAD commit'inin amacını tersine çeviriyor.

Birleştikten sonra

integration varsayılan dal yapılmalı (Settings → Branches), yoksa zamanlanmış işlerin hiçbiri koşmaz. Ondan sonra main Pascal'ın aynasına sıfırlanabilir — ölçüldü: mainde olup integration'da olmayan 4 commit var, üçü tekrarlanmış pin commit'i, dördüncüsü yalnız deploy-bundle.yml'ı ekliyor ve o dosya integration'da zaten var. Kaybolan bir şey yok.

claude added 6 commits August 5, 2026 12:13
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
ovurrsl force-pushed the claude/mfa-lockout-and-panel-sync branch from 37df89d to 23d72ad Compare August 5, 2026 12:14
… 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 ovurrsl changed the title fix(auth): ikinci faktörde kilidi gerçekten uygula + senkronu iki bozuk sekmeden arındır Depolar arası otomasyon + ikinci faktörde kilidi gerçekten uygula Aug 5, 2026
@ovurrsl
ovurrsl changed the base branch from claude/incele-nq4heb to integration August 5, 2026 12:23
@ovurrsl
ovurrsl marked this pull request as ready for review August 5, 2026 12:29
@ovurrsl
ovurrsl merged commit 6232999 into integration Aug 5, 2026
1 check passed
@ovurrsl
ovurrsl deleted the claude/mfa-lockout-and-panel-sync branch August 5, 2026 13:24
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.

2 participants