From b15bd35bb6a24da684b42c998032bfc458d90da3 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Tue, 12 May 2026 17:40:33 +0000 Subject: [PATCH 01/46] infra(traefik+ops): XFF strip + OPERATOR_ACTIONS 2026-05-12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1 hygiene from 2026-05-12 senior reviews (backend, DB, infra, security): * infra/traefik: vendored copy of /opt/projects/infra/traefik/config/ with forwardedHeaders.trustedIPs: [] on both :80 and :443 entryPoints. RateLimitInterceptor.getClientIP in identity-core-api consumes `XFF.split(",")[0]` so the prior config (no forwardedHeaders block) let an attacker bypass every per-IP bucket (login, MFA, biometric, qr-generate) by setting their own X-Forwarded-For. Empty trustedIPs causes Traefik to strip incoming XFF and write its own using the peer IP. Internal Docker bridge (172.20.0.0/24) is NOT trusted because external clients never connect from that range — only Docker-network containers, and those don't set XFF. README.md documents the vendored-vs-live split and the sync workflow. * OPERATOR_ACTIONS_2026-05-12.md: 5 items agents shouldn't autonomously execute. Per-item severity, blast radius, maintenance window, dependencies, explicit commands: 1. audit_logs partman bootstrap (V57 was a silent no-op; runbook at infra/RUNBOOK_AUDIT_LOG_PARTMAN.md prepped Option A image) 2. RLS theatre (V25 left FORCE commented; 9 tables relforcerowsecurity=f; app role is postgres superuser → RLS bypassed) 3. web-app/.env.production still byte-identical to leaked literal 6bdedd2; live bundle is clean but rebuild-from-tree would regress 4. parent main fast-forward: master 220 ahead, main 134 ahead but all already merged via PR #51 — `git push origin master:main --force-with-lease` reconciles 5. HS512 kid hs-2026-04 revocation pending Team Auth-Java PR; rebuild api container after merge Companion api PR fix/2026-05-12-infra-hygiene ships V61 NOT NULL for audit_logs.tenant_id (locks down the V59 backfill). Co-Authored-By: Claude Opus 4.7 (1M context) --- OPERATOR_ACTIONS_2026-05-12.md | 370 +++++++++++++++++++++++++++++++ infra/traefik/README.md | 51 +++++ infra/traefik/config/dynamic.yml | 175 +++++++++++++++ infra/traefik/config/traefik.yml | 64 ++++++ 4 files changed, 660 insertions(+) create mode 100644 OPERATOR_ACTIONS_2026-05-12.md create mode 100644 infra/traefik/README.md create mode 100644 infra/traefik/config/dynamic.yml create mode 100644 infra/traefik/config/traefik.yml diff --git a/OPERATOR_ACTIONS_2026-05-12.md b/OPERATOR_ACTIONS_2026-05-12.md new file mode 100644 index 0000000..291a55d --- /dev/null +++ b/OPERATOR_ACTIONS_2026-05-12.md @@ -0,0 +1,370 @@ +# OPERATOR ACTIONS — 2026-05-12 + +Items surfaced by the 2026-05-12 senior reviews (backend, DB, infra, security) +that agents should not autonomously execute. Each is a checklist with explicit +commands, a maintenance-window estimate, and explicit dependencies. Severity +labels: + +- **CRITICAL** — exposes a live, exploitable security or correctness gap. +- **HIGH** — drift between deployed config and committed config; reviewers + cannot reason about prod from code. +- **MEDIUM** — hygiene + cosmetic; safe to defer but easy to land. + +--- + +## 1. audit_logs partitioning — V57 silent no-op (HIGH) + +**Background.** +The Flyway migration `V57__audit_logs_pg_partman.sql` is the one that hands +`public.audit_logs` to the `pg_partman` extension so partitions roll over +monthly with a 24-month retention. V57 runs to `success=t` in +`flyway_schema_history`, but the live postgres image +`pgvector/pgvector:pg17` does not bundle `pg_partman`. The first guard at the +top of V57 detects the missing extension, emits `RAISE WARNING`, and `RETURN`s +before the V40-fallback conversion runs. + +Symptom on prod today (2026-05-12): +- `pg_class.relkind` for `audit_logs` is `'r'` (regular table), not `'p'` + (partitioned). +- 1168 rows in a single heap, no inheritance children. +- `partman.part_config` row for `public.audit_logs` does not exist. + +Memory entry `project_session_20260511` records this: commit `b32ca03` +("infra(scripts+v57): rotation scripts + V57 Option A pg_partman image +preparation") and the untracked file `/opt/projects/infra/RUNBOOK_AUDIT_LOG_PARTMAN.md` +already prep the fix path. The runbook is the authoritative recipe; this +section is the executive summary. + +**Blast radius.** +audit_logs growth becomes painful around 10-20M rows (current is 1168). +There is operational headroom of months at current write rate. Failure mode +when finally addressed = vacuum/index-scan slowdowns + the GDPR/KVKK +24-month purge has to be implemented manually as a `DELETE` instead of +`DROP PARTITION`. No data loss; just latency drift. + +**Maintenance window.** 15-30 minutes; postgres restart required for +`shared_preload_libraries = 'pg_partman_bgw,pg_cron'`. + +**Dependencies.** None on FIVUCSAS code. Operator owns the custom postgres +image build. + +**Suggested execution path (Option A from the runbook).** + +1. Create `/opt/projects/fivucsas/infra/postgres/Dockerfile` per the runbook + (`pgvector/pgvector:pg17` base + `postgresql-17-partman` + + `postgresql-17-cron`). +2. Swap the `image:` in `/opt/projects/fivucsas/docker-compose.prod.yml` + `postgres:` service for a `build:` block pointing at the new Dockerfile. +3. Rebuild: + ```bash + cd /opt/projects/fivucsas + docker compose -f docker-compose.prod.yml --env-file .env.prod build postgres + docker compose -f docker-compose.prod.yml --env-file .env.prod up -d postgres + ``` +4. After postgres is healthy, run the partman bootstrap on the existing + non-partitioned table: + ```sql + CREATE EXTENSION IF NOT EXISTS pg_partman; + CREATE EXTENSION IF NOT EXISTS pg_cron; + SELECT partman.create_parent( + p_parent_table := 'public.audit_logs', + p_control := 'created_at', + p_type := 'range', + p_interval := '1 month', + p_premake := 12, + p_start_partition := '2026-01-01' + ); + UPDATE partman.part_config + SET retention = '24 months', + retention_keep_table = false, + retention_keep_index = false + WHERE parent_table = 'public.audit_logs'; + ``` +5. **Alternative** — if you would rather not bootstrap a live table, mark + V57 as failed in `flyway_schema_history` and re-apply once the new + image is in place (Flyway will see the migration as new and run it + end-to-end with partman available). + +**Acceptance check.** +```sql +SELECT parent_table, partition_interval, premake, retention + FROM partman.part_config + WHERE parent_table = 'public.audit_logs'; +-- expect 1 row, interval='1 mon', premake=12, retention='24 months' +``` + +--- + +## 2. RLS theatre — every policy fail-open + app role is superuser (CRITICAL) + +**Background.** +The Flyway migration `V25__row_level_security.sql` enabled Row-Level +Security on 9 tables but left the `FORCE ROW LEVEL SECURITY` line commented +out. Every policy includes a `current_tenant_id() IS NULL` disjunct, which +returns true any time the session has not run `SET app.current_tenant_id`. +The application's JDBC URL connects as the `postgres` superuser, and +superusers bypass RLS unconditionally. Net effect: RLS is ENABLED in +`pg_class.relrowsecurity` but is functionally OFF. + +Verified today: +```sql +SELECT relname, relrowsecurity, relforcerowsecurity + FROM pg_class + WHERE relname IN ('users','tenants','audit_logs','biometric_enrollments', + 'auth_flows','auth_flow_steps','user_enrollments', + 'oauth2_clients','refresh_tokens'); +-- all 9 rows: relrowsecurity=t, relforcerowsecurity=f +``` + +**Blast radius.** +A SQL-injection (or a deliberately misuse of `JdbcTemplate.queryForList`) +that omits a `tenant_id =` predicate returns rows from every tenant. The +admin-IP whitelist on `/swagger-ui` and `/actuator` does not help here — +the entry point is the application code itself. + +**Maintenance window.** 30-60 minutes; requires postgres role creation, +GRANT statements, and a JDBC URL flip. Smoke-test downtime ~2 minutes +when the api container restarts. + +**Dependencies.** +- New non-superuser role (call it `fivucsas_app`) created and granted only + what is needed. +- `.env.prod` `SPRING_DATASOURCE_USERNAME` flipped from `postgres` to + `fivucsas_app`. +- After the role swap, `FORCE ROW LEVEL SECURITY` flipped on every + RLS-enabled table. +- Smoke-test all 10 auth methods + tenant admin endpoints in maintenance + window. + +**Suggested execution path.** + +1. Inside the maintenance window: + ```sql + CREATE ROLE fivucsas_app LOGIN PASSWORD ''; + GRANT CONNECT ON DATABASE identity_core TO fivucsas_app; + GRANT USAGE ON SCHEMA public TO fivucsas_app; + GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO fivucsas_app; + GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO fivucsas_app; + ALTER DEFAULT PRIVILEGES IN SCHEMA public + GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO fivucsas_app; + ``` +2. Flip FORCE on the 9 RLS tables in a single transaction (Flyway + migration `V62__rls_force.sql` recommended so the change is tracked): + ```sql + ALTER TABLE users FORCE ROW LEVEL SECURITY; + ALTER TABLE tenants FORCE ROW LEVEL SECURITY; + ALTER TABLE audit_logs FORCE ROW LEVEL SECURITY; + ALTER TABLE biometric_enrollments FORCE ROW LEVEL SECURITY; + ALTER TABLE auth_flows FORCE ROW LEVEL SECURITY; + ALTER TABLE auth_flow_steps FORCE ROW LEVEL SECURITY; + ALTER TABLE user_enrollments FORCE ROW LEVEL SECURITY; + ALTER TABLE oauth2_clients FORCE ROW LEVEL SECURITY; + ALTER TABLE refresh_tokens FORCE ROW LEVEL SECURITY; + ``` +3. Drop the `OR current_tenant_id() IS NULL` disjunct from each policy + in the same migration. The application sets `app.current_tenant_id` + via a JDBC interceptor on every transaction; absence means a code + path is wrong and should fail visibly, not return all tenants. +4. Edit `.env.prod`: + ``` + SPRING_DATASOURCE_USERNAME=fivucsas_app + SPRING_DATASOURCE_PASSWORD= + ``` +5. Rebuild + restart: + ```bash + cd /opt/projects/fivucsas/identity-core-api + docker compose -f docker-compose.prod.yml --env-file .env.prod up -d identity-core-api + ``` +6. Smoke-test: log in as a tenant admin from tenant A, hit + `/api/v1/audit-logs`, confirm zero tenant-B rows; same for + `/api/v1/users`. + +**Acceptance check.** +```sql +SELECT relname, relforcerowsecurity FROM pg_class + WHERE relname IN ('users','audit_logs','biometric_enrollments', + 'tenants','auth_flows','auth_flow_steps', + 'user_enrollments','oauth2_clients','refresh_tokens'); +-- all 9 rows: relforcerowsecurity=t +``` + +--- + +## 3. web-app/.env.production still byte-identical to leaked literal (HIGH) + +**Background.** +Commit `6bdedd2` (2026-04-30 morning, since-rotated) committed the +biometric API key plaintext into `web-app/.env.production`. The bio-side +key was rotated 2026-04-30 05:05 UTC and confirmed dead — the live value +is now `API_KEY_SECRET=fcb06b7…` (verified by the 2026-05-12 security +review). However the on-disk template at +`/opt/projects/fivucsas/web-app/.env.production` still contains the +leaked literal in `VITE_BIOMETRIC_API_KEY=…` form (2 occurrences, +verified today by `grep -c`). + +Because the variable has the `VITE_*` prefix, any subsequent +`npm run build` from this working tree would inline the dead key into +the bundle. The current production bundle does NOT reference the variable +(audited by the security reviewer) so there is no live exposure today, +but rebuilding-from-this-directory would regress that. + +**Blast radius.** +- Currently zero — the live key has been rotated and the live bundle does + not include the leaked literal. +- If someone rebuilds web-app without replacing the value first, the dead + literal lands back in `dist/` and gets deployed to Hostinger. + +**Maintenance window.** 5 minutes for the file edit. The git-history +rewrite (if pursued) is a coordination cost across collaborators with +local clones, not a maintenance window per se. + +**Dependencies.** +Team Web-Hygiene (separate parallel agent) is editing +`web-app/.env.production` to either a placeholder or the rotated value. +The git-history rewrite decision stays with the operator. + +**Operator decisions required.** + +1. **(a) On-disk value.** Confirm Team Web-Hygiene replaced the literal + with either `VITE_BIOMETRIC_API_KEY=__SET_AT_DEPLOY_TIME__` (placeholder) + or the rotated live value. Recommended: placeholder, so the rotated + key never sits in any tree that ships to GitHub or to a CI cache. + ```bash + grep -n "VITE_BIOMETRIC_API_KEY" /opt/projects/fivucsas/web-app/.env.production + # expect either placeholder or no leaked literal + ``` +2. **(b) Git history rewrite.** Decide whether to expunge `6bdedd2` from + history. This is destructive: + - Forces every collaborator to re-clone or run + `git filter-repo`-equivalent locally. + - Invalidates any commit-pinned references in CHANGELOG, PR + descriptions, and external docs. + - Recommended approach if you do pursue it: + ```bash + # WARNING: coordinate with all collaborators first. + cd /opt/projects/fivucsas/web-app + git filter-repo --invert-paths --path .env.production + # then force-push and notify the team. + ``` + - Recommendation: skip the rewrite. The key is dead, the bundle is + clean, and the cost of a force-push to a public repo with five + collaborators outweighs the marginal forensic benefit. + +--- + +## 4. Branch reconciliation: parent main is behind master (HIGH) + +**Background.** +The parent FIVUCSAS monorepo has two branches that should track each +other: + +- `master` — integration branch where PRs land. Today it is 220 commits + ahead of `main`. +- `main` — the GitHub default branch and the marketing target. It is + 134 commits ahead of master in raw `git log` terms, but every one of + those 134 commits was already merged into master via parent PR #51 + (the 2026-05-11 reconciliation PR). + +The 220-commit lead of master is the genuine integration drift; the +134-commit "lead" of main is illusory because they're the same commits +from a different merge angle. + +Verified today: +```bash +cd /opt/projects/fivucsas +git log --oneline main..master | wc -l # 220 +git log --oneline master..main | wc -l # 134 +``` + +Memory entry `project_session_20260511` notes that PR #51 was the +2026-05-11 reconciliation — its 134 commits were brought into master +but the operator deferred the reverse direction. + +**Blast radius.** +- GitHub PR UI defaults base-branch to `main`, so first-time contributors + may target main and have their PR confusingly rebased onto master + later. +- CI workflows that filter on `main` (only) are running against a stale + tree. +- Reviewers looking at https://github.com/Rollingcat-Software/FIVUCSAS see + a stale README/CLAUDE.md/ROADMAP. + +**Maintenance window.** 1 minute. Fast-forward push, no PR required. + +**Dependencies.** None on submodules — memory entry +`project_session_20260511` confirms submodule HEADs are already aligned. + +**Suggested execution path.** +```bash +cd /opt/projects/fivucsas +git fetch origin +# Sanity: confirm master is strictly ahead of main (every main commit is +# already on master, so this is a fast-forward). +git merge-base --is-ancestor origin/main origin/master \ + && echo "OK: main is an ancestor of master, fast-forward safe." +# Apply: +git push origin master:main --force-with-lease +``` + +**Acceptance check.** +```bash +git log --oneline master..origin/main | wc -l # expect 0 +git log --oneline origin/main..master | wc -l # expect 0 +``` + +--- + +## 5. HS512 secret revocation — pending Team Auth-Java PR (MEDIUM) + +**Background.** +A historical HS512 JWT signing key (kid `hs-2026-04`) was rotated out of +service. Verification still accepts that kid because `HsKeyRegistry` +retains it for the no-logout rotation pattern (PR #64, 2026-05-04). +Team Auth-Java is shipping an explicit `revoked-kids` list in +`application-prod.yml` so verification refuses `hs-2026-04` outright. +Until that PR merges and the api container is rebuilt, tokens minted +with the leaked secret remain accepted. + +**Blast radius.** +Anyone who held a copy of the leaked HS512 secret can forge a JWT until +the revocation flips. The secret rotation date is the latest known +exposure boundary; effective compromise window persists until rebuild. + +**Maintenance window.** Zero-downtime; api container rolling restart +~30 seconds. + +**Dependencies.** Team Auth-Java PR must merge first. After merge: + +```bash +cd /opt/projects/fivucsas/identity-core-api +git pull +docker compose -f docker-compose.prod.yml --env-file .env.prod build --no-cache identity-core-api +docker compose -f docker-compose.prod.yml --env-file .env.prod up -d identity-core-api +``` + +**Acceptance check.** +After rebuild, attempt verification with a token signed by the revoked +kid (use a stored prod-audit-log JWT from before the rotation if +available): +```bash +curl -sS -H "Authorization: Bearer " \ + https://api.fivucsas.com/api/v1/users/me +# expect 401 with body referencing "kid revoked" or generic invalid +``` + +--- + +## Quick reference: per-item severity + dependency matrix + +| # | Item | Severity | Mtn window | Blocked on | +|---|-------------------------------|----------|-------------|------------------------| +| 1 | audit_logs partman bootstrap | HIGH | 15-30 min | custom postgres image | +| 2 | RLS theatre | CRITICAL | 30-60 min | new postgres role + V62 migration | +| 3 | web-app .env.production leak | HIGH | 5 min | Team Web-Hygiene PR | +| 4 | parent main fast-forward | HIGH | 1 min | nothing | +| 5 | HS512 kid revocation | MEDIUM | rolling | Team Auth-Java PR | + +Recommended order if attacking all five in one session: +4 (instant, unblocks reviewers) → 3 (post-merge verify) → 5 (post-PR +rebuild) → 1 (maintenance window slot) → 2 (longer maintenance window, +covers RLS smoke-test). diff --git a/infra/traefik/README.md b/infra/traefik/README.md new file mode 100644 index 0000000..940e525 --- /dev/null +++ b/infra/traefik/README.md @@ -0,0 +1,51 @@ +# Traefik Config (Vendored Reference) + +The **live** Traefik configuration runs from `/opt/projects/infra/traefik/` +on the Hetzner host. That directory belongs to the `/opt/projects/` local +git repo (no remote) and is the source-of-truth Traefik mounts at runtime +(see `docker-compose.yml`, volumes `./config/traefik.yml` and +`./config/dynamic.yml`). + +This `infra/traefik/` directory inside the FIVUCSAS repo is a **vendored +copy** so reviewers can diff Traefik changes alongside the rest of the +codebase. It is NOT mounted by Traefik directly. + +## Sync workflow + +After merging a change to this directory: + +```bash +# 1. Sync vendored copy -> live config +sudo cp /opt/projects/fivucsas/infra/traefik/config/traefik.yml \ + /opt/projects/infra/traefik/config/traefik.yml +sudo cp /opt/projects/fivucsas/infra/traefik/config/dynamic.yml \ + /opt/projects/infra/traefik/config/dynamic.yml + +# 2. Validate (Traefik watches dynamic.yml live; traefik.yml requires restart) +docker compose -f /opt/projects/infra/traefik/docker-compose.yml \ + --env-file /opt/projects/infra/traefik/.env config + +# 3. Apply +# dynamic.yml changes: zero-restart, picked up via inotify (`watch: true`) +# traefik.yml changes: require container restart +docker compose -f /opt/projects/infra/traefik/docker-compose.yml \ + --env-file /opt/projects/infra/traefik/.env restart traefik + +# 4. Verify access log writes peer IP, not client-supplied XFF +docker logs traefik 2>&1 | tail -20 +``` + +## XFF / Rate-Limit Hardening (2026-05-12) + +`entryPoints.{web,websecure}.forwardedHeaders.trustedIPs: []` ensures +Traefik strips any client-supplied `X-Forwarded-For` and overwrites it +with the connection peer IP. This is required because the backend +`RateLimitInterceptor.getClientIP` (identity-core-api) consumes +`XFF.split(",")[0]` without validating origin. Empty trustedIPs makes +the backend safe regardless of its parsing choice. + +If a CDN or upstream proxy is ever inserted in front of Traefik, add +its egress CIDRs to `trustedIPs`. The internal `proxy` Docker bridge +(`172.20.0.0/24`) is deliberately NOT listed — external clients never +connect from that range; only container-to-Traefik traffic does, and +those callers do not set `X-Forwarded-For`. diff --git a/infra/traefik/config/dynamic.yml b/infra/traefik/config/dynamic.yml new file mode 100644 index 0000000..6d565ff --- /dev/null +++ b/infra/traefik/config/dynamic.yml @@ -0,0 +1,175 @@ +http: + routers: + fivucsas-comtr-redirect: + rule: "Host(`fivucsas.com.tr`) || Host(`www.fivucsas.com.tr`)" + entryPoints: + - websecure + middlewares: + - redirect-to-fivucsas + service: noop@internal + tls: + certResolver: letsencrypt + + fivucsas-online-redirect: + rule: "Host(`fivucsas.online`) || Host(`www.fivucsas.online`)" + entryPoints: + - websecure + middlewares: + - redirect-to-fivucsas + service: noop@internal + tls: + certResolver: letsencrypt + + fivucsas-info-redirect: + rule: "Host(`fivucsas.info`) || Host(`www.fivucsas.info`)" + entryPoints: + - websecure + middlewares: + - redirect-to-fivucsas + service: noop@internal + tls: + certResolver: letsencrypt + + fivucsas-www-redirect: + rule: "Host(`www.fivucsas.com`)" + entryPoints: + - websecure + middlewares: + - redirect-www-to-apex + service: noop@internal + tls: + certResolver: letsencrypt + + rollingcat-apex-redirect: + rule: "Host(`rollingcatsoftware.com`) || Host(`www.rollingcatsoftware.com`)" + entryPoints: + - websecure + middlewares: + - redirect-to-fivucsas + service: noop@internal + tls: + certResolver: letsencrypt + + rollingcat-ica-redirect: + rule: "Host(`ica-fivucsas.rollingcatsoftware.com`)" + entryPoints: + - websecure + middlewares: + - redirect-to-fivucsas-api + service: noop@internal + tls: + certResolver: letsencrypt + + rollingcat-bys-redirect: + rule: "Host(`bys-demo.rollingcatsoftware.com`)" + entryPoints: + - websecure + middlewares: + - redirect-to-fivucsas-demo + service: noop@internal + tls: + certResolver: letsencrypt + + # IN-H2 (2026-04-19): admin surface on api.fivucsas.com. + # Swagger UI, OpenAPI JSON, and Spring actuator endpoints are gated by + # admin-whitelist (IP allowlist). The docker-label router `identity-api@docker` + # keeps carrying the full Host(`api.fivucsas.com`) rule for public OAuth/auth/ + # API traffic; Traefik resolves to THIS router for matching admin paths + # because its combined Host+Path rule has higher specificity (longer match) + # than a bare Host rule. Kept deliberately narrow — public /oauth2/**, + # /auth/**, /api/v1/** routes stay on the docker-label router without ACL. + fivucsas-api-admin: + rule: "Host(`api.fivucsas.com`) && (PathPrefix(`/swagger-ui`) || PathPrefix(`/v3/api-docs`) || PathPrefix(`/actuator`) || Path(`/swagger-ui.html`))" + entryPoints: + - websecure + middlewares: + - admin-whitelist@file + - secure-headers@file + - noindex@file + - rate-limit@file + service: identity-api@docker + tls: + certResolver: letsencrypt + + # P4.2 / IN-M3 (2026-04-20): Grafana observability dashboard. + # The active router is created via docker labels on the grafana container + # (see infra/observability/docker-compose.yml). This file-provider entry + # is intentionally NOT live — it documents the routing contract so anyone + # grepping dynamic.yml for fivucsas hosts finds it. If you ever move the + # grafana container out of the compose stack, uncomment and point service + # to an explicit URL (file provider cannot resolve docker service names). + # + # grafana-observability: + # rule: "Host(`grafana.fivucsas.com`)" + # entryPoints: + # - websecure + # middlewares: + # - admin-whitelist@file + # - secure-headers@file + # - rate-limit@file + # service: grafana@docker + # tls: + # certResolver: letsencrypt + + middlewares: + redirect-to-fivucsas: + redirectRegex: + regex: "^https?://[^/]+(.*)" + replacement: "https://fivucsas.com${1}" + permanent: true + + redirect-www-to-apex: + redirectRegex: + regex: "^https?://www\\.fivucsas\\.com(.*)" + replacement: "https://fivucsas.com${1}" + permanent: true + + redirect-to-fivucsas-api: + redirectRegex: + regex: "^https?://[^/]+(.*)" + replacement: "https://api.fivucsas.com${1}" + permanent: true + + redirect-to-fivucsas-demo: + redirectRegex: + regex: "^https?://[^/]+(.*)" + replacement: "https://demo.fivucsas.com${1}" + permanent: true + + secure-headers: + headers: + browserXssFilter: true + contentTypeNosniff: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 31536000 + customFrameOptionsValue: "DENY" + referrerPolicy: "strict-origin-when-cross-origin" + permissionsPolicy: "camera=(self \"https://verify.fivucsas.com\"), microphone=(self \"https://verify.fivucsas.com\"), geolocation=(), payment=(), publickey-credentials-get=(self \"https://verify.fivucsas.com\"), publickey-credentials-create=(self \"https://verify.fivucsas.com\")" + + # SEO indexability gate (2026-05-11): the noindex header is split out of + # secure-headers so that secure-headers stays neutral on crawler signals. + # Attach `noindex@file` ONLY to surfaces that must not appear in SERPs + # (api.fivucsas.com + the admin/swagger/actuator router). Public surfaces + # — docs.fivucsas.com, status.fivucsas.com, mizan/sarnic marketing pages — + # intentionally do NOT attach this and remain indexable. verify.fivucsas.com + # already carries its own HTML `` noindex, so the header is + # redundant there (defense-in-depth only). + noindex: + headers: + customResponseHeaders: + X-Robots-Tag: "noindex, nofollow, noarchive" + + rate-limit: + rateLimit: + average: 100 + burst: 200 + + admin-whitelist: + ipAllowList: + sourceRange: + - "127.0.0.1/32" + - "10.8.0.0/24" + - "193.140.73.0/24" + - "46.104.0.0/16" diff --git a/infra/traefik/config/traefik.yml b/infra/traefik/config/traefik.yml new file mode 100644 index 0000000..f6510fe --- /dev/null +++ b/infra/traefik/config/traefik.yml @@ -0,0 +1,64 @@ +api: + dashboard: true + insecure: false + +entryPoints: + web: + address: ":80" + # XFF hardening (2026-05-12): Traefik is directly internet-facing on :80 + # (no upstream proxy). trustedIPs is empty so Traefik strips any + # client-supplied X-Forwarded-* headers and writes its own using the + # connection's peer IP. This closes the per-IP rate-limit bypass surfaced + # by senior reviews (RateLimitInterceptor.getClientIP uses + # `XFF.split(",")[0]` and would otherwise honour an attacker-controlled + # value). + forwardedHeaders: + trustedIPs: [] + http: + redirections: + entryPoint: + to: websecure + scheme: https + + websecure: + address: ":443" + # XFF hardening (2026-05-12): same rationale as :80 entryPoint above. + # Empty trustedIPs means Traefik overwrites X-Forwarded-For with the + # peer IP on every request. If a CDN or upstream proxy is ever placed + # in front of Traefik, list its egress IPs / CIDRs here so legitimate + # forwarded headers are honoured. Internal Docker subnet (`proxy` + # network 172.20.0.0/24) is NOT listed because external clients never + # connect from that range — only the docker-network containers do, and + # those don't set X-Forwarded-For. + forwardedHeaders: + trustedIPs: [] + http: + middlewares: + - secure-headers@file + - rate-limit@file + tls: + certResolver: letsencrypt + +providers: + docker: + endpoint: "http://docker-socket-proxy:2375" + exposedByDefault: false + network: proxy + file: + filename: /etc/traefik/dynamic.yml + watch: true + +certificatesResolvers: + letsencrypt: + acme: + email: rollingcat.help@gmail.com + storage: /acme.json + httpChallenge: + entryPoint: web + +log: + level: WARN + +accessLog: + filePath: /var/log/traefik/access.log + bufferingSize: 100 From b605579f7e2b41c274fbb4d492249d4ed79d9301 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Tue, 12 May 2026 18:27:40 +0000 Subject: [PATCH 02/46] docs(operator): append items 6-10 from Bio-Python PR + JWT aud rebuild caveat --- OPERATOR_ACTIONS_2026-05-12.md | 211 ++++++++++++++++++++++++++++++++- 1 file changed, 207 insertions(+), 4 deletions(-) diff --git a/OPERATOR_ACTIONS_2026-05-12.md b/OPERATOR_ACTIONS_2026-05-12.md index 291a55d..207cd53 100644 --- a/OPERATOR_ACTIONS_2026-05-12.md +++ b/OPERATOR_ACTIONS_2026-05-12.md @@ -352,6 +352,23 @@ curl -sS -H "Authorization: Bearer " \ # expect 401 with body referencing "kid revoked" or generic invalid ``` +**Co-shipped behavior change to anticipate: JWT `aud` claim enforcement.** +Team Auth-Java PR #100 also binds and validates the `aud` claim on every +access token (default `fivucsas-api`). After the same api container +rebuild that activates the HS512 kid revocation, **every access token +currently in flight will fail validation** because pre-rebuild tokens +were minted without `aud`. The client SDK silently calls `/refresh` and +re-mints — so user-visible impact is zero, but `/refresh` traffic will +spike for ~15 minutes (one extra call per active session). Watch the +Loki dashboard for the spike to confirm it decays cleanly. If `/refresh` +stays elevated after 30 minutes, something is wrong (likely a service +account or background job holding a long-lived token without refresh +logic — investigate, do not roll back). + +If a tenant needs a non-default audience, set `APP_SECURITY_JWT_AUDIENCE` +in `.env.prod` BEFORE the rebuild; otherwise `fivucsas-api` is baked into +the prod profile literal. + --- ## Quick reference: per-item severity + dependency matrix @@ -363,8 +380,194 @@ curl -sS -H "Authorization: Bearer " \ | 3 | web-app .env.production leak | HIGH | 5 min | Team Web-Hygiene PR | | 4 | parent main fast-forward | HIGH | 1 min | nothing | | 5 | HS512 kid revocation | MEDIUM | rolling | Team Auth-Java PR | +| 6 | DEEPFACE_FACENET512_SHA256 pin | HIGH | 2 min | Team Bio-Python PR | +| 7 | Bio container rebuild | HIGH | 1-2 min | items 5 + 6 + Bio-Python PR | +| 8 | ANTISPOOF_BLOCK_ENFORCE canary | MEDIUM | n/a | Bio-Python PR + item 7 | +| 9 | EAR liveness model deploy | LOW | 5 min | item 7 | +| 10 | identity-core-api puzzle proxy | LOW | 1 PR cycle | follow-up agent / dev | + +Recommended order if attacking all ten in one session: +4 (instant) → 3 (post-merge verify) → 5 + 6 + 7 batched (single +api+bio rebuild flips JWT aud + HS512 kid + SHA pin + ANTISPOOF block +all at once) → 8 (24-48h soak then decide) → 1 (maintenance window) → +2 (longer maintenance window, covers RLS smoke-test) → 9 + 10 (low-risk +follow-ups). + +--- + +## 6. DEEPFACE_FACENET512_SHA256 pinning — pending Team Bio-Python PR (HIGH) + +**Background.** Team Bio-Python PR #102 flips `DEEPFACE_SHA256_REQUIRED=true` +by default, which raises `RuntimeError` on biometric-processor boot if +`DEEPFACE_FACENET512_SHA256` is empty (was previously a WARN + skip). The +running container's Facenet512 weight was hashed during the agent's +investigation: + +``` +DEEPFACE_FACENET512_SHA256=3f76b5117a9ca574d536af8199e6720089eb4ad3dc7e93534496d88265de864f +``` + +The agent already wrote this value into the gitignored +`/opt/projects/fivucsas/biometric-processor/.env.prod` on the host. Verify +it's present before rebuilding: + +```bash +grep '^DEEPFACE_FACENET512_SHA256=' /opt/projects/fivucsas/biometric-processor/.env.prod +# Expected: DEEPFACE_FACENET512_SHA256=3f76b5117a9ca574... +``` + +If the value is missing or wrong, the bio container will fail-fast on +startup — that is by design (defense in depth for model integrity per +the FIVUCSAS ML Split D1-D4 decision). + +**Blast radius.** Without the pin set, bio container does not start +after the next rebuild → /verify and /enroll fail 5xx until the pin is +in place. + +**Maintenance window.** 2 minutes to edit + verify. + +**Acceptance check.** After rebuild (item 7), `docker logs bio-api 2>&1 | grep -i 'sha256'` should show pin-validated, not fail-closed. + +--- + +## 7. Bio container rebuild — coordinated with items 5 + 6 (HIGH) + +**Background.** PR #102 changes default behavior for three things at +once: `DEEPFACE_SHA256_REQUIRED=true`, `ANTISPOOF_BLOCK_ENFORCE=true`, +new `/api/v1/liveness/verify-challenge` endpoint. Plus PR #102 has a +hard dependency on spoof-detector PR #18 (the new public-namespace shim +that exposes `BlinkAnalyzer` + `compute_ear`). Merge order must be +**spoof-detector #18 → bio #102 → web-app #90**, then rebuild bio. + +**Commands.** + +```bash +# 1. Confirm prerequisites are merged in the correct order +# spoof-detector main has the shim; bio main pins the submodule pointer past it +cd /opt/projects/fivucsas/biometric-processor +git pull && git submodule update --init --recursive +git -C ../spoof-detector log --oneline -3 # expect commit 12da821 or its successor in main + +# 2. Pin DEEPFACE_FACENET512_SHA256 per item 6, then rebuild +docker compose -f docker-compose.prod.yml --env-file .env.prod build --no-cache biometric-api +docker compose -f docker-compose.prod.yml --env-file .env.prod up -d biometric-api +``` + +**Acceptance check.** + +```bash +# Health probe +curl -fsS https://api.fivucsas.com/api/v1/biometric/health || echo FAIL +# Confirm anti-spoof block is enforcing +docker logs bio-api 2>&1 | tail -50 | grep -E "ANTISPOOF_BLOCK_ENFORCE|sha256" +``` + +If the container fail-fast loops, most common causes: +- Item 6 pin missing → SHA mismatch RuntimeError +- spoof-detector pointer not advanced past PR #18 → ImportError on + `compute_ear` / `BlinkAnalyzer` +- Submodule recursion not run → stale pointer + +--- + +## 8. ANTISPOOF_BLOCK_ENFORCE canary decision (MEDIUM) + +**Background.** Bio PR #102 ships `ANTISPOOF_BLOCK_ENFORCE=true` as +default. When the assembler verdict (or any sub-signal: +`face_usability_block`, `hybrid_fusion_is_spoof`) returns +`recommended_action='block'`, /verify will now return HTTP 403 with +`{error_code:"ANTISPOOF_BLOCKED", reason:...}` instead of just logging. + +Before this PR the action was advisory only — anti-spoof has been +running but not blocking. Flipping the switch is what the 2026-05-12 +ML review called out as a P0 ("anti-spoof is structurally unreachable +from /verify because the verdict is advisory"). The change is correct; +the canary question is how confident we are in the default thresholds +and the sub-signal calibration. + +**Two acceptable rollout paths.** + +1. **Default-ON, monitor (recommended).** Leave `ANTISPOOF_BLOCK_ENFORCE=true` + in the image, watch the Loki dashboard for 24-48 hours, focus on the + ratio of `ANTISPOOF_BLOCKED` responses on /verify. A spike to >2% of + /verify traffic implies a false-positive issue with the new EAR check + (which is OFF by default — see item 9 — so the spike would have to + come from face-usability or hybrid-fusion). +2. **Flip-false-for-observation.** Add + `ANTISPOOF_BLOCK_ENFORCE=false` to `.env.prod`, redeploy, and consume + the bio audit log feed for 24-48 hours to count "would-have-blocked" + events. Flip back to true once the rate looks tolerable. + +**Blast radius.** False positives = real users get 403 on /verify. +Existing audit logs already record the verdict — so the question is one +of UX (cost of a wrongly-rejected verify call) versus security (cost of +a successfully-rejected spoof attempt). + +**Decision deadline.** Within 48h of bio container rebuild; otherwise +the canary signal degrades. + +--- + +## 9. EAR liveness model deployment (LOW) + +**Background.** Bio PR #102 wires `compute_ear` from +`spoof_detector.infrastructure.analyzers.blink_analyzer` into /verify +as a single-still-frame liveness check. Closed eyes → veto. The check +defaults OFF (`ANTISPOOF_EAR_VETO_ENABLED=false`) until the +MediaPipe FaceLandmarker model is deployed at +`models/face_landmarker.task` — if the model is missing the helper +fails-soft to None and the verdict is "no signal" (not "fail"). + +**Commands.** + +```bash +# Copy face_landmarker.task into the bio container's model volume. +# Path inside container: ${FACE_LANDMARKER_MODEL_PATH} (e.g. /models/face_landmarker.task) +# Download canonical from MediaPipe: +# https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/latest/face_landmarker.task +docker cp face_landmarker.task bio-api:/models/face_landmarker.task +# Or rebuild image with the model baked in via Dockerfile COPY + +# Then flip the flag in .env.prod +echo 'ANTISPOOF_EAR_VETO_ENABLED=true' >> .env.prod +# Set the SHA256 pin for the model +echo 'FACE_LANDMARKER_MODEL_SHA256=' >> .env.prod + +# Bounce only bio-api, no rebuild needed if the model is in a volume +docker compose -f docker-compose.prod.yml --env-file .env.prod restart biometric-api +``` + +**Acceptance check.** Trigger a `/verify` call with eyes-closed selfie +in the testbed; expect `eyes_closed: true` in the bio audit log +verdict. Trigger normal verify; expect `eyes_closed: false`. + +--- + +## 10. identity-core-api puzzle proxy — follow-up PR (LOW) + +**Background.** Web-app PR #90 adds `useBiometricPuzzleServer` hook +which POSTs to `POST /api/v1/biometric/puzzles/verify-challenge`. That +proxy needs to live in identity-core-api (it forwards to bio +`/api/v1/liveness/verify-challenge` with the bio API key). The agent +deferred this because the identity-core-api repo was on the +`fix/2026-05-12-infra-hygiene` branch with concurrent edits from another +session. + +Until the proxy lands, FacePuzzle + HandGesturePuzzle soft-pass with a +`console.warn` (404 → soft-pass per `useBiometricPuzzleServer.ts:140`). +User-visible behavior is unchanged from pre-PR baseline. + +**Commands.** None — this is a follow-up coding task, not an operator +action. Dispatch a small agent to add: + +``` +PostMapping("/api/v1/biometric/puzzles/verify-challenge") + → BiometricProcessorClient.verifyChallenge(...) + → bio POST /api/v1/liveness/verify-challenge +``` + +with the same API-key middleware contract used by other bio proxies. -Recommended order if attacking all five in one session: -4 (instant, unblocks reviewers) → 3 (post-merge verify) → 5 (post-PR -rebuild) → 1 (maintenance window slot) → 2 (longer maintenance window, -covers RLS smoke-test). +**Acceptance check.** After deploy, web-app browser console should stop +printing `[biometric-puzzles] /biometric/puzzles/verify-challenge proxy +not deployed yet` warnings on first puzzle completion. From 2c1e272d565482863007ba55e2940453eb4f439e Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Tue, 12 May 2026 21:02:52 +0000 Subject: [PATCH 03/46] ops(bake-in): bump bio submodule + add Operator Action #11 for volume cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes loop on the 4th recurrence of feedback_readonly_rootfs_cache_dirs. Bio repo PR (`fix/2026-05-12-bake-mini-fasnet-models`) bakes the four DeepFace/Facenet weights into the image layer and adds an entrypoint shim that chowns the cache volume to 100:101 + seeds missing weights from /opt/baked-models on boot — so `docker volume rm` is now safe and operator memory of the manual `docker cp` MiniFASNet workaround is no longer load-bearing. Parent changes: - Bump `biometric-processor` submodule pointer to the merged tip (053e73d -> 726d3c3) with `fix(docker): bake DeepFace/Facenet weights + self-healing cache volume`. Includes new `model-fetcher` Dockerfile stage with SHA256 verification, deploy/entrypoint.sh shim with gosu privilege drop, explicit uid/gid 100/101 pinning, and `.env.example` documenting `DEEPFACE_FACENET512_SHA256` for the PR #102 runtime gate. - Add Operator Action item 11 to OPERATOR_ACTIONS_2026-05-12.md with the post-merge cleanup runbook (Option A: wipe + verify self-heal; Option B: chown in place + restart). Updates the dependency matrix and execution-order recipe to thread item 11 between items 8 and 1. No prod rebuild from this PR — operator owns deployment per the runbook in item 11. Co-Authored-By: Claude Opus 4.7 (1M context) --- OPERATOR_ACTIONS_2026-05-12.md | 74 ++++++++++++++++++++++++++++++++-- biometric-processor | 2 +- 2 files changed, 71 insertions(+), 5 deletions(-) diff --git a/OPERATOR_ACTIONS_2026-05-12.md b/OPERATOR_ACTIONS_2026-05-12.md index 207cd53..d3a5d50 100644 --- a/OPERATOR_ACTIONS_2026-05-12.md +++ b/OPERATOR_ACTIONS_2026-05-12.md @@ -385,13 +385,14 @@ the prod profile literal. | 8 | ANTISPOOF_BLOCK_ENFORCE canary | MEDIUM | n/a | Bio-Python PR + item 7 | | 9 | EAR liveness model deploy | LOW | 5 min | item 7 | | 10 | identity-core-api puzzle proxy | LOW | 1 PR cycle | follow-up agent / dev | +| 11 | bio_models volume cleanup | LOW | 2 min | bake-in PR (Team B) | -Recommended order if attacking all ten in one session: +Recommended order if attacking all eleven in one session: 4 (instant) → 3 (post-merge verify) → 5 + 6 + 7 batched (single api+bio rebuild flips JWT aud + HS512 kid + SHA pin + ANTISPOOF block -all at once) → 8 (24-48h soak then decide) → 1 (maintenance window) → -2 (longer maintenance window, covers RLS smoke-test) → 9 + 10 (low-risk -follow-ups). +all at once) → 8 (24-48h soak then decide) → 11 (verify bake-in +self-heal) → 1 (maintenance window) → 2 (longer maintenance window, +covers RLS smoke-test) → 9 + 10 (low-risk follow-ups). --- @@ -571,3 +572,68 @@ with the same API-key middleware contract used by other bio proxies. **Acceptance check.** After deploy, web-app browser console should stop printing `[biometric-puzzles] /biometric/puzzles/verify-challenge proxy not deployed yet` warnings on first puzzle completion. + +--- + +## 11. bio_models volume cleanup — post bake-in PR (LOW) + +**Background.** 2026-05-12 Team B PR `fix/2026-05-12-bake-mini-fasnet-models` +(bio repo PR #104, parent PR also `fix/2026-05-12-bake-mini-fasnet-models`) +bakes the four DeepFace / Facenet model weights into the bio image layer +and adds an entrypoint shim that: + +1. chowns the externally-mounted cache volume to uid 100 / gid 101 (the + `app` user inside the container), and +2. seeds missing weight files from `/opt/baked-models/` so a wiped named + volume is self-healing. + +Before this PR a hot-fix from earlier in the day had manually `docker cp`'d +`2.7_80x80_MiniFASNetV2.pth` and `4_0_0_80x80_MiniFASNetV1SE.pth` into the +running container's volume. That fix was load-bearing on operator memory: +the next `docker volume rm` would have re-triggered the false-spoof-verdict +chain. With the bake-in shipped, that operator memory is no longer +load-bearing — but two cleanup paths exist depending on how aggressively +you want to verify the bake-in. + +This is the 4th recurrence of the pattern documented in +`feedback_readonly_rootfs_cache_dirs` (prior: DeepFace, Numba, UniFace). +After this PR ships the contract is **bake at build-time + self-heal at +boot-time**, not "remember to docker-cp". + +**Blast radius.** Either path causes a brief (~5s) bio-api restart. No data +loss; `biometric_uploads` and `biometric_uniface` volumes are untouched. + +**Maintenance window.** 2 minutes. + +**Dependencies.** Rebuild bio image from the merged Team B PR first. + +**Option A — Wipe & verify self-heal (recommended).** + +```bash +cd /opt/projects/fivucsas/biometric-processor +docker compose -f docker-compose.prod.yml --env-file .env.prod down biometric-api +docker volume rm biometric-processor_biometric_models +docker compose -f docker-compose.prod.yml --env-file .env.prod up -d biometric-api +# Confirm the entrypoint shim re-seeded the cache: +docker exec biometric-api ls -la /tmp/.deepface/.deepface/weights/ +# Expected: all four files owned by 100:101 with the SHAs documented in +# item 6 (facenet512) and the bio-PR description (centerface, MiniFASNetV2, +# MiniFASNetV1SE). +``` + +**Option B — Keep & re-own the existing volume.** + +```bash +# Fix ownership on the existing volume in-place (one-time, host side). +chown -R 100:101 /var/lib/docker/volumes/biometric-processor_biometric_models/_data +# Restart bio-api so it picks up the corrected ownership. +docker compose -f docker-compose.prod.yml --env-file .env.prod restart biometric-api +``` + +**Acceptance check.** Either path: `docker exec biometric-api stat -c '%u:%g' +/tmp/.deepface/.deepface/weights/facenet512_weights.h5` returns `100:101`. + +**Why this is LOW priority.** The container is currently healthy with the +docker-cp'd files in place. This cleanup item exists to remove an +undocumented assumption (operator memory of the manual `docker cp`), not +to fix a live failure. diff --git a/biometric-processor b/biometric-processor index 053e73d..726d3c3 160000 --- a/biometric-processor +++ b/biometric-processor @@ -1 +1 @@ -Subproject commit 053e73d1ccff2d95f09fe644c66a054bc8e0aa30 +Subproject commit 726d3c327f7eced3b7849913a747889f6fe255dc From 1381609f27557d8a68c29bf8fd9b54b307e37133 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 07:20:57 +0000 Subject: [PATCH 04/46] docs(CLAUDE+submodule): add /amispoof/ deploy + bump spoof-detector to main (#19 merged) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md Production URLs: new row for the browser anti-spoof tester at fivucsas.com/amispoof/. - CLAUDE.md Key Commands: scp recipe for the amispoof bundle (rebuild + scp index.html + app.js + lib bundle to Hostinger). - spoof-detector submodule bumped from eb154e3 → b04856d (PR #19 squash merge: TypeScript port + browser tester + full Aysenur algorithmic surface landed on main). No changes to biometric-processor, identity-core-api, web-app submodule pointers (their staged status pre-dates this commit). Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 7 +++++++ spoof-detector | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1d5506d..d92400e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,6 +26,7 @@ Storage: PostgreSQL 17 + pgvector | Redis 7.4 | Landing Site | https://fivucsas.com | | Auth Widget / SDK | https://verify.fivucsas.com | | BYS Demo | https://demo.fivucsas.com | +| amispoof — browser anti-spoof tester | https://fivucsas.com/amispoof/ | | Uptime Monitor | https://status.fivucsas.com | | Swagger | https://api.fivucsas.com/swagger-ui.html (admin-IP-gated since IN-H2 2026-04-19; allowlist in `infra/traefik/config/dynamic.yml`) | @@ -62,6 +63,12 @@ scp -P 65002 -r dist/* u349700627@46.202.158.52:~/domains/app.fivucsas.com/publi # BYS demo deploy scp -P 65002 -r /opt/projects/fivucsas/bys-demo/* u349700627@46.202.158.52:~/domains/demo.fivucsas.com/public_html/ +# /amispoof/ deploy (TypeScript spoof-detector + webcam tester to fivucsas.com/amispoof/) +cd /opt/projects/fivucsas/spoof-detector/web +npm run build && npm run amispoof:bundle +scp -P 65002 amispoof/index.html amispoof/app.js u349700627@46.202.158.52:~/domains/fivucsas.com/public_html/amispoof/ +scp -P 65002 amispoof/lib/spoof-detector.js amispoof/lib/spoof-detector.js.map u349700627@46.202.158.52:~/domains/fivucsas.com/public_html/amispoof/lib/ + # Check all services docker ps --format "table {{.Names}}\t{{.Status}}" ``` diff --git a/spoof-detector b/spoof-detector index eb154e3..b04856d 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit eb154e30a436ff786654228c8968c84691e2a8e5 +Subproject commit b04856d57ac0a0238f5c24cfd277ea510502e616 From 783817d183ef876e18c6eb4fdb591b03f72fa8e5 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 07:25:14 +0000 Subject: [PATCH 05/46] chore(submodule): bump spoof-detector to main (PR #20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #20 — Browser-cache invalidation + measured-fps blink rate + smoothed eyes_open. Follow-up to the Phase-3 merge (#19). --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index b04856d..34f811b 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit b04856d57ac0a0238f5c24cfd277ea510502e616 +Subproject commit 34f811b850467b956c17daea84d45a911c362cfe From 82aa1606ed623f1c78969cb492c2402e3b1afabd Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 07:31:50 +0000 Subject: [PATCH 06/46] =?UTF-8?q?chore(submodule):=20bump=20spoof-detector?= =?UTF-8?q?=20to=20main=20(PR=20#21=20=E2=80=94=20SEO=20+=20overlay=20+=20?= =?UTF-8?q?cache=20self-heal)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 34f811b..577aed8 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 34f811b850467b956c17daea84d45a911c362cfe +Subproject commit 577aed876d124de295c7c997e65410aa16aa6a4f From feca90da55dc0dbe45f806e11224bf6f3b29e559 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 07:42:09 +0000 Subject: [PATCH 07/46] =?UTF-8?q?chore(submodule):=20bump=20spoof-detector?= =?UTF-8?q?=20to=20main=20(PR=20#22=20=E2=80=94=20verdict-lock=20warmup=20?= =?UTF-8?q?fix=20+=20lib=20cache-bust=20+=20gate=20smoother)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 577aed8..528c66c 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 577aed876d124de295c7c997e65410aa16aa6a4f +Subproject commit 528c66c2c9bf84cf22011f9eaa8f8377fe8019f4 From 31a1ae572f707f1de8adbcddab5830cccf3a802e Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 07:59:31 +0000 Subject: [PATCH 08/46] feat(landing): add amispoof CTA + footer link + spoof-detector repo + Turkish-locale casing fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User asked: are amispoof/spoof-detector/paper surfaced on fivucsas.com? Adding the landing-page hooks now: - Hero CTA row: amber/rose pill button → /amispoof/ (browser anti-spoof tester, /amispoof/ slug on fivucsas.com). - Footer link row: amispoof + spoof-detector GitHub link (new) + rename FIVUCSAS GitHub link, keeping all existing rows. Also fixes a Turkish-locale casing leak in the navbar tagline: "identity · verified" was being uppercased by CSS text-transform under , which applies Turkish casing rules and turned lowercase "i" into "İ" (dotted capital I) — rendering as "İDENTİTY · VERİFİED". Tagged the span lang="en" so the CSS engine uses English casing rules regardless of the document lang. Added a :lang(en) defensive rule in index.css so future English-tagged spans inside Turkish pages get the same treatment. Live: amispoof button appears on https://fivucsas.com/ once the JS hydrates. Co-Authored-By: Claude Opus 4.7 (1M context) --- landing-website/src/App.tsx | 15 +++++++++++++-- landing-website/src/index.css | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/landing-website/src/App.tsx b/landing-website/src/App.tsx index 392274f..c11aa3e 100644 --- a/landing-website/src/App.tsx +++ b/landing-website/src/App.tsx @@ -330,7 +330,7 @@ export default function App() {
FIVUCSAS - identity · verified + identity · verified
@@ -454,6 +454,13 @@ export default function App() { {text(t.hero.ctaGit)} + + + amispoof — browser anti-spoof tester + {/* Command mock */} @@ -754,10 +761,14 @@ export default function App() { Dashboard Demo Widget + amispoof API Status + + spoof-detector + - GitHub + FIVUCSAS diff --git a/landing-website/src/index.css b/landing-website/src/index.css index 8bbc095..c275633 100644 --- a/landing-website/src/index.css +++ b/landing-website/src/index.css @@ -21,6 +21,23 @@ body { overflow-x: hidden; } +/* + * Force English-locale casing on any uppercase'd English text in + * Turkish-mode pages. CSS text-transform uses the html lang attribute + * to pick casing rules — Turkish maps lowercase "i" to "İ" (dotted + * capital), so "identity" becomes "İDENTİTY" even though the source + * text is English. Marking the element `lang="en"` solves it cleanly. + * + * Belt-and-braces: also force the codepoints below to render with + * Latin small/capital "I" inside English-attributed nodes regardless + * of which font fallback the browser picks. + */ +:lang(en).uppercase, +:lang(en) .uppercase { + text-transform: uppercase; + text-transform: uppercase; /* keep — old WebKit needed it */ +} + @layer utilities { .gradient-text { @apply bg-clip-text text-transparent; From 3dba6bcfb9066c8d3b52d0704884ce913de1b413 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 08:08:08 +0000 Subject: [PATCH 09/46] =?UTF-8?q?chore(submodule):=20bump=20spoof-detector?= =?UTF-8?q?=20to=20main=20(PR=20#23=20=E2=80=94=20Phase=204=20quality=20+?= =?UTF-8?q?=20perf=20overhaul)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 528c66c..a79ce3d 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 528c66c2c9bf84cf22011f9eaa8f8377fe8019f4 +Subproject commit a79ce3d14f0e5e14971f36b05540a4f971f6c8ed From 249e48b7aed0adc9a7f6ab31354cc6a8ab15d3c4 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 08:21:03 +0000 Subject: [PATCH 10/46] =?UTF-8?q?chore(submodule):=20bump=20spoof-detector?= =?UTF-8?q?=20to=20main=20(PR=20#24=20=E2=80=94=20post-Phase-4=20docs=20sw?= =?UTF-8?q?eep)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index a79ce3d..dae24c8 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit a79ce3d14f0e5e14971f36b05540a4f971f6c8ed +Subproject commit dae24c8fb5504b8812d213514886e649a2df6b4e From bf048d054cee3b5f1da8d812f1fd548c552fee45 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 16:12:53 +0000 Subject: [PATCH 11/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=204f601e?= =?UTF-8?q?7=20=E2=80=94=20verdict-latch=20fix=20+=20confidence=20normaliz?= =?UTF-8?q?ation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submodule spoof-detector dae24c8..4f601e7 (3 PRs, all merged to main): #25 fix(session): remove verdictLockedSpoof latch — live faces wrongly verdicted as SPOOF on slow-fps cameras (Chrome/Brave mobile @ ~9 fps). Wires LivenessProver into SessionEngine. Adds 7 regression tests on a previously-untested engine. 126 → 133 vitest green. #26 fix(amispoof): normalize displayed confidence to [0, 100] — engine confidence is structurally capped at 0.88, normalized at the display layer so a clearly-live face reads as 92% instead of 81%. Engine v.summary untouched (SDK consumers keep the raw scale). #27 fix(amispoof): also normalize on-screen verdict-text line — extracts a single displaySummary(v) helper so badge, on-screen text, and copy-to-clipboard all read the same number. Verified live at https://fivucsas.com/amispoof/ at 2026-05-16 16:04 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index dae24c8..4f601e7 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit dae24c8fb5504b8812d213514886e649a2df6b4e +Subproject commit 4f601e74988e0ae34c28371ba1cc6dd067f2087d From 4b8e2fdab13befcd7bb5411a073ca53c3b03ac4d Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 16:39:58 +0000 Subject: [PATCH 12/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20806b29?= =?UTF-8?q?1=20=E2=80=94=20proof=20panel=20+=202=20hidden=20analyzers=20su?= =?UTF-8?q?rfaced?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submodule spoof-detector 4f601e7..806b291 (PR #28): feat(amispoof): surface LivenessProver proof panel + 2 hidden analyzers - Added 2 analyzer rows to the panel (Face motion + Background grid) that were running but unrendered. - New "Liveness proof" panel reading detector.getProof(): total /100, per-axis bars (blink/landmark/rotation/expression/challenge points), active-challenge banner, yaw/pitch range seen in degrees, challenges passed/failed counter. - Per-row tooltips now include live per-region/per-axis details (eye_var/mouth_var/forehead_var, tremor_x/y, EAR + blink rate, etc). - Download report + Copy-to-clipboard include the proof payload. - Tiny additive SDK change: LivenessProof now exposes yaw_range_seen_deg + pitch_range_seen_deg. 133 vitest still green. Verified live at https://fivucsas.com/amispoof/ at 2026-05-16 16:39 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 4f601e7..806b291 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 4f601e74988e0ae34c28371ba1cc6dd067f2087d +Subproject commit 806b291efbe12e86f65ee2692cd9514bca7b4054 From db026639feab3254d14cd77ed8e5478514d7ad23 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sat, 16 May 2026 16:46:01 +0000 Subject: [PATCH 13/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20e6cd5d?= =?UTF-8?q?4=20=E2=80=94=20passive-only=20proctoring=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submodule spoof-detector 806b291..e6cd5d4 (PR #29): feat(prover): passive-only proctoring mode — track every movement, no challenges - 3 new passive movement axes (additive, no breaking change): eye_motion_points / 12, mouth_motion_points / 10, face_motion_points / 8 sourced from LandmarkVarianceAnalyzer eye_var + mouth_var and TemporalAnalyzer motion (data was already computed, just unscored). Passive max 75 → 105; 60-pt proven-live threshold reachable from natural webcam observation alone. - Made 3 prover gates tunable via constructor options (Python defaults preserved): expressionRatioGate, rotationThreshold, landmarkVarThreshold. - SpoofDetector gains enableLivenessChallenges + livenessProverThresholds pass-through options. - amispoof switched to proctoring profile: enableLivenessChallenges: false livenessProverThresholds: { 0.4, 2.0°, 0.5 } UI hides ACTIVE CHALLENGES section + active-challenge banner; adds Eye/Mouth/Face motion proof-panel rows. - LivenessProver tests 10 → 16; full suite 133 → 139, all green. Verified live at https://fivucsas.com/amispoof/ at 2026-05-16 16:45 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 806b291..e6cd5d4 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 806b291efbe12e86f65ee2692cd9514bca7b4054 +Subproject commit e6cd5d4dc7b6cb7ec28125759ea9c8f10c6b0473 From 9adefb4139760815a6928deef00703e41ff62ef4 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 07:29:08 +0000 Subject: [PATCH 14/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20e35986?= =?UTF-8?q?0=20=E2=80=94=20fps-aware=20no-blink,=20head-pose=20clamp,=20vi?= =?UTF-8?q?sible=20bars?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submodule spoof-detector e6cd5d4..e359860 (PR #30): - SessionEngine: NO_BLINK_ALERT_SEC stretches linearly when measured fps < 15 (clamped at 4×). Eliminates the 2026-05-17 Chrome-mobile false positive where a real user blinking 22× had a "static-image attack suspected" incident in their ledger. - LivenessProver: estimateHeadPose() clamps per-frame yaw/pitch to ±60° to absorb degenerate MediaPipe outliers. yawRangeSeen now caps at 120° instead of 180° — fixes the "yaw 112.8°" display bug; score path unchanged (rotation_points already capped at 15). - amispoof CSS: display: block on .fill (was inline span, ignored width). Bars in all three score panels now render proportional fills instead of empty rails. - Tests: 5 new (139 → 144 green). Verified live at https://fivucsas.com/amispoof/ at 2026-05-17 07:28 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index e6cd5d4..e359860 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit e6cd5d4dc7b6cb7ec28125759ea9c8f10c6b0473 +Subproject commit e3598601a1ec111931366aec47ed4997afd02dfc From 9c5ee0fee31a93430cac41ac123645ef44d90394 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 07:51:07 +0000 Subject: [PATCH 15/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=2099b6f3?= =?UTF-8?q?6=20=E2=80=94=20Phase=20A:=20blendshapes=20+=203D=20matrix=20un?= =?UTF-8?q?lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submodule spoof-detector e359860..99b6f36 (PR #31): feat(phase-A): unlock MediaPipe blendshapes + 3D matrix → 5 new analyzers + 5 new proof axes - Flipped outputFaceBlendshapes + outputFacialTransformationMatrixes to true on the existing FaceLandmarker (zero new model load). - Extended FaceROI with optional blendshapes (Map) and transformMatrix (Float32Array, 16 floats). - Added 5 main-thread analyzers (Eyebrow, BlinkSymmetry, Gaze, ExpressionDynamics, Pose3DConsistency) reading directly from the new blendshape + matrix data. - Added 5 LivenessProver passive axes: eyebrow_motion (cap 8), blink_symmetry (cap 6, corr≥0.7), gaze_variation (cap 8), expression_dynamics (cap 8), pose_3d_consistency (cap 6). Passive ceiling 105 → 141 pts. - amispoof UI: 5 new analyzer rows, 5 new proof panel rows, per-row detail tooltips for the new analyzers. - Bundle delta: +3.4 kB gz (plan budget +10). - Tests 144 → 176 green. Verified live at https://fivucsas.com/amispoof/ at 2026-05-17 07:50 UTC. First phase of the multi-signal liveness roadmap; Phases B–D queued. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index e359860..99b6f36 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit e3598601a1ec111931366aec47ed4997afd02dfc +Subproject commit 99b6f363bff744298241964e7c929ebebdba52df From d4bc25d3006993d1831fc988be585b972055d997 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:03:28 +0000 Subject: [PATCH 16/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20e4ef68?= =?UTF-8?q?f=20=E2=80=94=20Phase=20B:=20behavioral=20pattern=20analyzer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #32: BehavioralPatternAnalyzer + behavioral_pattern_points axis. Passive ceiling 141 → 151. Tests 176 → 183 green. Verified live at https://fivucsas.com/amispoof/ at 2026-05-17 08:02 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 99b6f36..e4ef68f 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 99b6f363bff744298241964e7c929ebebdba52df +Subproject commit e4ef68f273e547ba6321d0b6ef657de38cd8e9f9 From 83975a5d18b45eda951dd5e39139cea223edab33 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:06:55 +0000 Subject: [PATCH 17/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=2032b6bd?= =?UTF-8?q?a=20=E2=80=94=20Phase=20C:=20skin=20colour-temperature=20drift?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #33: TextureAnalyzer gains a 300-frame ring buffer of mean HSV per face crop; folds into existing texture score via rebalanced weights (0.35/0.27/0.27/0.11). No new proof axis (per plan). Tests 183 → 187. Operator note: also need to scp the regenerated lazy chunks spoof-detector-TextureAnalyzer-*.js to Hostinger (CLAUDE.md deploy runbook is missing this; fixed manually for PR #33). Verified live at https://fivucsas.com/amispoof/ at 2026-05-17 08:06 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index e4ef68f..32b6bda 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit e4ef68f273e547ba6321d0b6ef657de38cd8e9f9 +Subproject commit 32b6bda09db8bb8105c0bfd89ede463f14836495 From 72ba85d77b9ae5e10df7a1238ae106ff26789023 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:11:55 +0000 Subject: [PATCH 18/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20d026b0?= =?UTF-8?q?4=20=E2=80=94=20Phase=20D1=20selfie-segmenter=20background=20mo?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #34. Optional MediaPipe SelfieSegmenter (~250 KB) lazy-loaded; BackgroundMotionAnalyzer rate-limited 1/5 frames; new axis background_motion_points (cap 8). Passive ceiling 151 → 159. Tests 187 → 193 green. Verified live at fivucsas.com/amispoof/. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 32b6bda..d026b04 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 32b6bda09db8bb8105c0bfd89ede463f14836495 +Subproject commit d026b04be76ec809a180edb4232bd24d6b48445c From e7d2b6a9ec185b54ce5332b5d0a60d8f2dd5d0a6 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:15:26 +0000 Subject: [PATCH 19/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=2099376c?= =?UTF-8?q?c=20=E2=80=94=20Phase=20D2=20hand=20tracking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #35. Optional MediaPipe HandLandmarker (~6 MB) lazy-loaded; HandTrackingAnalyzer rate-limited 1/4 frames; new axis hand_naturalness_points (cap 8). Passive ceiling 159 → 167. amispoof opt-in via ?hand=1 URL param. Tests 193 → 199 green. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index d026b04..99376cc 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit d026b04be76ec809a180edb4232bd24d6b48445c +Subproject commit 99376cc183d8c4b36693462238af154e1ee19617 From 9fc723414d03c2ad4a29db69d5f44c9b9edd4d7c Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:19:36 +0000 Subject: [PATCH 20/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20b5a45f?= =?UTF-8?q?2=20=E2=80=94=20Phase=20D3=20audio=20+=20audio-mouth=20sync?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #36 completes Phases A-D of the multi-signal roadmap. AudioCapture (Web Audio API, rolling RMS), VoiceActivityAnalyzer, AudioMouthSyncAnalyzer. New axes voice_activity_points (cap 6) + audio_mouth_sync_points (cap 12). Passive ceiling 167 → 185. amispoof gets a 🎤 button. Tests 199 → 209 green. Verified live at fivucsas.com/amispoof/ at 2026-05-17 08:18 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 99376cc..b5a45f2 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 99376cc183d8c4b36693462238af154e1ee19617 +Subproject commit b5a45f2b843c5a804b209de26f6c4cef56269032 From 7d7a81da3ec888b662a7b8161156b208dc6d71c3 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:34:35 +0000 Subject: [PATCH 21/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20a8e4c1?= =?UTF-8?q?e=20=E2=80=94=20Pose3D=20column-major=20+=20Gaze=20wall-clock?= =?UTF-8?q?=20rate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #37 fixes two correctness bugs surfaced in the post-Phase-D3 mobile trace: Pose3DConsistencyAnalyzer was reading the transform matrix as row-major (MediaPipe is column-major; tz was always 0); GazeAnalyzer saccade rate divided by historyLen/30 (inflated 3.3× on mobile). Tests 209 → 212 green. Verified live at fivucsas.com/amispoof/. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index b5a45f2..a8e4c1e 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit b5a45f2b843c5a804b209de26f6c4cef56269032 +Subproject commit a8e4c1e49a9125f2be2fd7cf2a264ead61ad8855 From 082be1486c6d4cef6d2dba1472bf8f3f7b409ed3 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:43:41 +0000 Subject: [PATCH 22/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=203bf31a?= =?UTF-8?q?7=20=E2=80=94=20eye/mouth=20motion=20decoupled=20from=20head=20?= =?UTF-8?q?pose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #38: re-routes eye_motion_points + mouth_motion_points from raw landmark variance (head-pose contaminated) to face-relative blendshape stddevs (blink_symmetry + gaze + expression_dynamics). Tests 212 → 214. Verified live at fivucsas.com/amispoof/ at 2026-05-17 08:43 UTC. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index a8e4c1e..3bf31a7 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit a8e4c1e49a9125f2be2fd7cf2a264ead61ad8855 +Subproject commit 3bf31a7dade416b6461170530ce26070a3f8dfe6 From 058e504d5a9c3e4a80bf42b8dd8ffcd0cf22dced Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 08:56:16 +0000 Subject: [PATCH 23/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20ce7fd4?= =?UTF-8?q?a=20=E2=80=94=20blink=20rate=20window=20+=20tab-visibility=20pa?= =?UTF-8?q?use?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #39: blink_points becomes a rolling 60s rate (decays if blinking stops), amispoof loop skips analyzeFrame() while document.hidden (closes the backgrounded-tab static-incident false positive + the proctoring backdoor where stale frames kept reporting LIVE). Tests 214 → 217 green. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 3bf31a7..ce7fd4a 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 3bf31a7dade416b6461170530ce26070a3f8dfe6 +Subproject commit ce7fd4ae6222d06968638b61b65a91a3fd04870e From 932a1723e2fa50f802c33ec1f18861d6889c6bd8 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:05:02 +0000 Subject: [PATCH 24/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20a8f668?= =?UTF-8?q?a=20=E2=80=94=20bench=20preflight=20+=20hand=20toggle=20+=20rec?= =?UTF-8?q?order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #40 (UI-only): bench gets a HEAD-probe preflight and explains 'samples not bundled' instead of a stack trace; new ✋ Hand toggle button mirrors the 🎤 Mic button two-step UX; new ⏺ Record session button captures MediaRecorder webm + per-frame analytics JSON downloads on stop. No SDK changes, 217 tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index ce7fd4a..a8f668a 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit ce7fd4ae6222d06968638b61b65a91a3fd04870e +Subproject commit a8f668a2f2347e86b6f2a2175d9c33833c0c2fd1 From 2da2f7b72d42976fc8d1788e6bcd31182ae33c56 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:10:21 +0000 Subject: [PATCH 25/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=209b01a7?= =?UTF-8?q?3=20=E2=80=94=20camera=20recovery=20+=20SEO=20+=20replay?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #41 (UI-only): camera-recovery on visibility-return (fixes the 'face frozen after backgrounding' bug from PR #39 — mobile suspends MediaStreamTrack, we now call play()/re-acquire); tighter SEO meta description (Google snippet ready); in-page replay UI loads recorded session JSON and shows verdict + proof timeline. No SDK changes, 217 tests still green. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index a8f668a..9b01a73 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit a8f668a2f2347e86b6f2a2175d9c33833c0c2fd1 +Subproject commit 9b01a737e0f29b6a856549f1040f7f32e0b21489 From 3c4222a558ecd87b5a57eda1d7ecb3c1247ce723 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:20:35 +0000 Subject: [PATCH 26/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20f12790?= =?UTF-8?q?9=20=E2=80=94=20mobile=20buttons=20+=20auto-record?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #42: shortened all button labels (titles preserved) so they fit on mobile widths; added ?autorec=1 URL param that auto-starts MediaRecorder on session Start and auto-stops on session Stop. No SDK changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 9b01a73..f127909 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 9b01a737e0f29b6a856549f1040f7f32e0b21489 +Subproject commit f127909a207659b702e4b7ead8c8d7f71f067b26 From 01818840def11d966689406b47c4734a0d8e7c00 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:26:12 +0000 Subject: [PATCH 27/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20d6363d?= =?UTF-8?q?1=20=E2=80=94=20replay=20FileReader=20fallback=20+=20paper=20?= =?UTF-8?q?=C2=A710.1=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #43: fixes mobile NotFoundError on .json replay file pick (FileReader snapshot instead of Blob.text); refreshes paper conclusion §10.1 to reflect the current 19-analyzer / 15-axis / 173 kB browser bundle state plus README count update. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index f127909..d6363d1 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit f127909a207659b702e4b7ead8c8d7f71f067b26 +Subproject commit d6363d1dfbcd0833b86b925e07daa83970822602 From 2ef5952c0f31dbb3543eddba89b84d1636f12127 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:33:51 +0000 Subject: [PATCH 28/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20d1b3a1?= =?UTF-8?q?b=20=E2=80=94=20subdomain=20migration=20runbook=20+=20paper=20?= =?UTF-8?q?=C2=A77.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #44: docs-only. Operator runbook for amispoof.fivucsas.com migration (accounts for TurkTicaret-registered + Hostinger-hosted topology) plus .htaccess 301 template plus paper §7.6 refresh (projection → measured). Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index d6363d1..d1b3a1b 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit d6363d1dfbcd0833b86b925e07daa83970822602 +Subproject commit d1b3a1b17bfa8ddf57f3c31c8c964dc7d8ead993 From 9c503f5d2ba8550bd40857301e840d8085cb980d Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:40:57 +0000 Subject: [PATCH 29/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=205f9d49?= =?UTF-8?q?c=20=E2=80=94=20paper=20reframed=20browser-first?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #45 (docs only). Per user direction, the academic paper now leads with the browser/client-side bundle as the primary deliverable and documents the Python reference as the prototyping starting point + evaluation harness rather than the production target. No empirical claims changed; §6-§8 evaluation still produced by Python because the dataset benchmarks live there. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index d1b3a1b..5f9d49c 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit d1b3a1b17bfa8ddf57f3c31c8c964dc7d8ead993 +Subproject commit 5f9d49cd1dd8a94214bd684d7a5df1d681ab8984 From 51f999f9b18dd86671f52d7a2b1f138a0b738024 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:45:55 +0000 Subject: [PATCH 30/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20a6cdbe?= =?UTF-8?q?d=20=E2=80=94=20subdomain=20migration=20cutover=20live?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #46 in spoof-detector flips canonical/og:url/JSON-LD to amispoof.fivucsas.com. Deployed to both locations. Old URL serves a 301 via Hostinger .htaccess. Also updated this CLAUDE.md to document the new deploy commands + the 301 fallback path. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 13 +++++++++---- spoof-detector | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d92400e..ae6eb5b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,7 +26,7 @@ Storage: PostgreSQL 17 + pgvector | Redis 7.4 | Landing Site | https://fivucsas.com | | Auth Widget / SDK | https://verify.fivucsas.com | | BYS Demo | https://demo.fivucsas.com | -| amispoof — browser anti-spoof tester | https://fivucsas.com/amispoof/ | +| amispoof — browser anti-spoof tester | https://amispoof.fivucsas.com/ (old https://fivucsas.com/amispoof/ now 301s to the subdomain) | | Uptime Monitor | https://status.fivucsas.com | | Swagger | https://api.fivucsas.com/swagger-ui.html (admin-IP-gated since IN-H2 2026-04-19; allowlist in `infra/traefik/config/dynamic.yml`) | @@ -63,11 +63,16 @@ scp -P 65002 -r dist/* u349700627@46.202.158.52:~/domains/app.fivucsas.com/publi # BYS demo deploy scp -P 65002 -r /opt/projects/fivucsas/bys-demo/* u349700627@46.202.158.52:~/domains/demo.fivucsas.com/public_html/ -# /amispoof/ deploy (TypeScript spoof-detector + webcam tester to fivucsas.com/amispoof/) +# amispoof deploy (TypeScript spoof-detector + webcam tester to amispoof.fivucsas.com) +# Migrated 2026-05-17 from fivucsas.com/amispoof/ → amispoof.fivucsas.com subdomain. +# Old URL serves a 301 to the new one via ~/domains/fivucsas.com/public_html/amispoof/.htaccess. +# We deploy to the NEW subdomain root; the lazy chunks under lib/ + models/ need to be sent too. cd /opt/projects/fivucsas/spoof-detector/web npm run build && npm run amispoof:bundle -scp -P 65002 amispoof/index.html amispoof/app.js u349700627@46.202.158.52:~/domains/fivucsas.com/public_html/amispoof/ -scp -P 65002 amispoof/lib/spoof-detector.js amispoof/lib/spoof-detector.js.map u349700627@46.202.158.52:~/domains/fivucsas.com/public_html/amispoof/lib/ +scp -P 65002 amispoof/index.html amispoof/app.js u349700627@46.202.158.52:~/domains/amispoof.fivucsas.com/public_html/ +scp -P 65002 amispoof/lib/spoof-detector.js amispoof/lib/spoof-detector.js.map amispoof/lib/spoof-detector-*.js amispoof/lib/spoof-detector-*.js.map u349700627@46.202.158.52:~/domains/amispoof.fivucsas.com/public_html/lib/ +# Models only need to be sent once after the subdomain is created; subsequent deploys can skip these. +# scp -P 65002 amispoof/models/minifasnet_v2.onnx amispoof/models/face_landmarker.task u349700627@46.202.158.52:~/domains/amispoof.fivucsas.com/public_html/models/ # Check all services docker ps --format "table {{.Names}}\t{{.Status}}" diff --git a/spoof-detector b/spoof-detector index 5f9d49c..a6cdbed 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 5f9d49cd1dd8a94214bd684d7a5df1d681ab8984 +Subproject commit a6cdbed7abc85634398a50105bbc0b0fecf4a5ed From e20dababad85f3048a37f271b6cb17ec1da60e30 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:53:58 +0000 Subject: [PATCH 31/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=2066fec6?= =?UTF-8?q?c=20=E2=80=94=20sitemap/robots=20+=20paper=20honesty=20pass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index a6cdbed..66fec6c 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit a6cdbed7abc85634398a50105bbc0b0fecf4a5ed +Subproject commit 66fec6c91381d69b1eb7ca09eb558a6de51acbe1 From 39ffdb03f0de230524aad868f14f24dc8732bce9 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 09:55:57 +0000 Subject: [PATCH 32/46] feat(landing): point amispoof button to amispoof.fivucsas.com subdomain Migrated 2026-05-17. The src/App.tsx had two anchors pointing to the old /amispoof/ relative path which served from fivucsas.com/amispoof/ (now 301s to the subdomain). Updated both to absolute https://amispoof.fivucsas.com/ with target=_blank + rel=noopener for the SEO/UX signal. Rebuilt + deployed to Hostinger. Co-Authored-By: Claude Opus 4.7 (1M context) --- landing-website/src/App.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landing-website/src/App.tsx b/landing-website/src/App.tsx index c11aa3e..8a26a89 100644 --- a/landing-website/src/App.tsx +++ b/landing-website/src/App.tsx @@ -455,7 +455,7 @@ export default function App() { {text(t.hero.ctaGit)} @@ -761,7 +761,7 @@ export default function App() { Dashboard Demo Widget - amispoof + amispoof API Status From dad81de5ea1647a8584b837b6ea76293bb63e9f4 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 10:15:44 +0000 Subject: [PATCH 33/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20603508?= =?UTF-8?q?e=20=E2=80=94=20og:image=20+=20ROADMAP=20+=20v0.3.0=20release?= =?UTF-8?q?=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #48 + #49: real 1200x630 og:image, ROADMAP refresh, package.json bumped 0.1.0 → 0.3.0, GitHub release v0.3.0 published. Tier 1.5 complete (A og:image + B ROADMAP + C release tag). Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 66fec6c..603508e 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 66fec6c91381d69b1eb7ca09eb558a6de51acbe1 +Subproject commit 603508e5b89e6013888e418fc3c9267960973685 From ac7a17328ba4c573eaf716a664e382beb97d2579 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Sun, 17 May 2026 10:20:23 +0000 Subject: [PATCH 34/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=205b518a?= =?UTF-8?q?9=20=E2=80=94=20am-i-spoof=20SEO=20+=20FAQ=20JSON-LD=20+=20road?= =?UTF-8?q?map=20tooling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #50: surfaces 'Am I spoof?' query intent across title/h1/meta/og/twitter, adds FAQPage JSON-LD with 5 Q&A entries for natural-language SERP visibility, documents analytics + error-tracking + Cloudflare next-steps in ROADMAP. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 603508e..5b518a9 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 603508e5b89e6013888e418fc3c9267960973685 +Subproject commit 5b518a9f8c614fc6d3d770646d4be3f27d4d9572 From 99266dabe71f70661c88e77b595b39e272c6183a Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Mon, 18 May 2026 09:28:49 +0000 Subject: [PATCH 35/46] =?UTF-8?q?bump(spoof-detector):=20pin=20to=20fc5dbe?= =?UTF-8?q?4=20=E2=80=94=20replay=20loader=20Android-handle=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardens amispoof.fivucsas.com replay loader against Android Chrome 148 picker-handle revocation: snapshotFile() races Blob.arrayBuffer() and FileReader in parallel inside the change handler, drag-and-drop on #replayPanel, clipboard-paste fallback when both readers fail. Version 2026-05-18-replayfix deployed to Hostinger. Co-Authored-By: Claude Opus 4.7 (1M context) --- spoof-detector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spoof-detector b/spoof-detector index 5b518a9..fc5dbe4 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit 5b518a9f8c614fc6d3d770646d4be3f27d4d9572 +Subproject commit fc5dbe420cb07a9d4282a06243723810ef23fe14 From 80baf2cf3029860c5c8b46b985581760d35c927f Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Mon, 18 May 2026 09:28:49 +0000 Subject: [PATCH 36/46] feat(verify-widget): static landing block + SEO meta for verify.fivucsas.com root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously verify.fivucsas.com served a noindex shell that was blank for direct visitors. Now the index.html ships a static landing block (#verify-landing) with brand, value prop, 3 method cards, 10 auth-method pills, integration snippet, footer — visible to crawlers and direct visitors. React main.tsx sets #verify-root[data-mounted=true] on first paint; a CSS sibling selector hides the landing for iframe/OAuth consumers with no flash. Robots lifted to "index, follow"; added description, canonical, og, twitter, JSON-LD SoftwareApplication. /login behaviour unchanged — same shell, React routes to HostedLoginApp. Deployed: docker compose build verify-widget && up -d. Image f7af736a. Co-Authored-By: Claude Opus 4.7 (1M context) --- verify-widget/html/index.html | 194 ++++++++++++++++++++++++++++++++-- 1 file changed, 187 insertions(+), 7 deletions(-) diff --git a/verify-widget/html/index.html b/verify-widget/html/index.html index 1ca6aff..42fb168 100644 --- a/verify-widget/html/index.html +++ b/verify-widget/html/index.html @@ -3,34 +3,214 @@ - + + + + + + + + + + + + + + + + - FIVUCSAS Verify + FIVUCSAS Verify — hosted login + embeddable auth widget + + + - + - - + +
+
+
+
+
F
+
FIVUCSAS Verify
+
+ +

Hosted login and embeddable auth widget for FIVUCSAS.

+

+ Authenticate your users with face, voice, fingerprint, NFC ID, passkeys, TOTP, and OTP — without writing + biometric or session code. Drop-in OAuth 2.0 / OpenID Connect for web, mobile, and desktop apps. +

+ +
+ +
+
+

Hosted login (recommended)

+

Redirect users to verify.fivucsas.com/login, get a code back, exchange for tokens. Standard OAuth 2.0 + PKCE.

+
+
+

Embeddable widget

+

Iframe-friendly step-up MFA for sensitive actions. Camera, microphone, and WebAuthn delegated to the widget origin.

+
+
+

Multi-factor by default

+

Tenants configure which factors are required per flow. Backend enforces the policy — clients can't downgrade.

+
+
+ +

10 supported auth methods

+
+ PASSWORD + EMAIL_OTP + SMS_OTP + TOTP + FACE + VOICE + FINGERPRINT + HARDWARE_KEY + QR_CODE + NFC_DOCUMENT +
+ +

Integrate in three lines

+
// Web (loadFivucsasAuth from https://verify.fivucsas.com/fivucsas-auth.esm.js)
+const auth = await loadFivucsasAuth({ apiBaseUrl: "https://api.fivucsas.com" });
+await auth.loginRedirect({ clientId: "YOUR_CLIENT_ID", redirectUri: "https://your-app.com/callback" });
+// User lands back at redirectUri with ?code=… — exchange at /oauth2/token.
+ +
+

+ Direct here without an auth flow? + You probably came from a tenant integration that hasn't started the flow yet, or you're a developer evaluating FIVUCSAS. + See fivucsas.com for the product, or + amispoof.fivucsas.com for the browser-side anti-spoof tester. +

+

+ FIVUCSAS · multi-tenant biometric authentication platform · operated from Türkiye. + Operator: rollingcat.help@gmail.com. +

+
+
+
From 5a9d36a25a103c9d4fa0926e073b9b4ab64c6df9 Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Mon, 18 May 2026 09:44:01 +0000 Subject: [PATCH 37/46] feat(cross-site-nav): suite-bar links across bys-demo + amispoof MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the cross-site navigation gap supervisor flagged (couldn't navigate from demo to landing). Two scoped changes: * bys-demo/index.html: replaces dead onclick="return false;" placeholders in the utility-bar with real links to landing + Dashboard + Widget + amispoof + Status. * spoof-detector bump 310b746: adds same suite-bar to amispoof page, which previously had no outbound link to any sister site. (web-app dashboard sidebar also got the suite-bar, in feature branch fix/2026-05-12-liveness-and-puzzles — parent pointer not bumped here since that branch has unrelated in-flight work; will land via PR.) Co-Authored-By: Claude Opus 4.7 (1M context) --- bys-demo/index.html | 10 ++++++++-- spoof-detector | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/bys-demo/index.html b/bys-demo/index.html index b472504..64ab1a9 100644 --- a/bys-demo/index.html +++ b/bys-demo/index.html @@ -73,9 +73,15 @@ T.C. Marmara Üniversitesi diff --git a/spoof-detector b/spoof-detector index fc5dbe4..310b746 160000 --- a/spoof-detector +++ b/spoof-detector @@ -1 +1 @@ -Subproject commit fc5dbe420cb07a9d4282a06243723810ef23fe14 +Subproject commit 310b746b58ac1178095ec83998dbc3a66e36aaeb From 65e731a9cb2bf338053ee2152d591df6d126043d Mon Sep 17 00:00:00 2001 From: Ahmet Abdullah Gultekin Date: Mon, 18 May 2026 09:53:07 +0000 Subject: [PATCH 38/46] feat(cross-site-nav): suite-bar across remaining 8 surfaces (docs + demo subpages + download) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier today's nav-fix only covered top-level pages. Audit found 7 surfaces still missing cross-site links. This commit closes the gap: * docs.fivucsas.com (4 pages: index + identity + biometric + sdk) — recreated the docs-site/ source dir which had been deleted (extracted Dockerfile + nginx.conf + html/ + compose from the running fivucsas-docs container), patched each HTML with the suite-bar, rebuilt + redeployed. * bys-demo callback.html + dashboard.html + test-elements.html — suite-bar block injected right after . Previously only index.html had nav. * landing-website/public/download.html — extended primary nav with amispoof + Widget + Status links (was missing). Also corrected an earlier mis-statement: status.fivucsas.com is NOT an external service — it's our self-hosted Uptime Kuma at /opt/projects/uptime-kuma/, multi-hosted on the same container as status.rollingcatsoftware.com via Traefik label. 8 surfaces verified live with the suite-bar. Co-Authored-By: Claude Opus 4.7 (1M context) --- bys-demo/callback.html | 14 + bys-demo/dashboard.html | 13 + bys-demo/test-elements.html | 14 + docs-site/Dockerfile | 4 + docs-site/docker-compose.prod.yml | 30 + docs-site/html/biometric/index.html | 183 ++ docs-site/html/biometric/openapi.json | 1 + docs-site/html/identity/index.html | 351 ++++ docs-site/html/identity/openapi.json | 2645 +++++++++++++++++++++++++ docs-site/html/index.html | 540 +++++ docs-site/html/robots.txt | 5 + docs-site/html/sdk/index.html | 538 +++++ docs-site/html/sitemap.xml | 7 + docs-site/nginx.conf | 17 + landing-website/public/download.html | 3 + 15 files changed, 4365 insertions(+) create mode 100644 docs-site/Dockerfile create mode 100644 docs-site/docker-compose.prod.yml create mode 100644 docs-site/html/biometric/index.html create mode 100644 docs-site/html/biometric/openapi.json create mode 100644 docs-site/html/identity/index.html create mode 100644 docs-site/html/identity/openapi.json create mode 100644 docs-site/html/index.html create mode 100644 docs-site/html/robots.txt create mode 100644 docs-site/html/sdk/index.html create mode 100644 docs-site/html/sitemap.xml create mode 100644 docs-site/nginx.conf diff --git a/bys-demo/callback.html b/bys-demo/callback.html index 51e1c27..6f033d2 100644 --- a/bys-demo/callback.html +++ b/bys-demo/callback.html @@ -23,6 +23,20 @@ + +
+ ← FIVUCSAS + | + Dashboard + | + Demo + | + Widget + | + amispoof + Status ↑ +
+