Changelog
All notable changes to the iya.ru platform are documented here. The format follows Keep a Changelog and the project adheres to semantic versioning.
[Unreleased]
Added
- P14 security/load pass: ip_blocks edge enforcement (
IpBlockGuardin
the Kernel before sessions/visit capture — bare 403 html/json ip_blocked, expiry-aware, v4+v6, kill switch IP_BLOCKS_ENFORCE); the advertised RATE_LIMIT_DEFAULT/API/REDIRECT buckets are now actually enforced (global per-IP gate on apex/swarm, tenant zone exempt, /health exempt, kill switch RATE_LIMIT_DEFAULT_ENFORCE); RollupBuilder implemented (day+hour totals per subdomain+global, idempotent, heals partial rows); repeatable drill scripts/_p14_edge_check.py (12 checks)
- P14(b) N+1 sweep:
SwarmService::agentName()memoised +primeAgentNames()
batch priming across 7 list paths; VisitService click_limit dedupe hoisted out of the per-user webhook loop
- P15 beta loop:
scripts/_beta_journey.py— 13-check golden-path E2E
(register → subdomain → link → tenant 302 → worker → visits → notifications → swarm → feature proposal → admin triage), deterministic and self-cleaning; docs/BETA.md (environment, feedback channels, triaged fix list, drill suite)
- P16 release engineering: runtime
Dockerfile(php8.2-cli-alpine,
migrate-on-boot, healthcheck, var/ volume), CI workflow (.github/workflows/ci.yml: doctor/migrate/lint/tests/i18n + docker build
- image smoke),
php bin/iya release:notes [version],docs/RELEASE.md
(semver policy + release ritual)
- P16 deferred P10 emitters shipped:
link.target_down(TargetChecker
in the maintenance worker — SSRF-guarded HEAD→GET probes, budget TARGET_CHECK_BUDGET, emits once per ok→down transition) and agent.task (SwarmService task created/claimed/status_changed/handoff fan-out to subscribed webhooks)
Fixed
/dashboard/onboardinganswered 501 (stub shadowing the working flow)
while the post-registration redirect and the links empty-state CTA point at it — now delegates to the subdomain front door (found by the P15 beta journey)
- Empty states on the links/views/collections dashboard pages converted from
ad-hoc inline blocks to the .empty design-system component (P13)
i18n
- Locales completed this cycle: he (RTL), fa (RTL), hi, bn (🏁 milestone 40),
vi, th — full coverage 42/66; the remaining 24 locales are frozen by product decision (ADR-018) with en fallback at runtime
Added
- Notifications pipeline (P10a): stdlib
Mailer(log/sendmail/SMTP with
STARTTLS + AUTH LOGIN, MIME multipart, RFC 2047), TelegramClient (Bot API), OutboxDispatcher (email direct + template forms, telegram, signed webhooks; exponential backoff, failed after 5 attempts), Notifier fan-out (per-user channel prefs + kinds whitelists, DB templates with locale→en→ fallback chain, Telegram binding handshake with single-use codes), bell with unread badge, /account/notifications page; migration 007 (prefs table + seeded email templates); events: welcome, password_changed
- Event webhooks (P10b):
WebhookService(docs/API.md catalogue, static SSRF
validation, cap 20, auto secrets), account UI (create/test/toggle/delete, masked secret with one-time reveal, delivery log), dispatcher contract (HMAC signature, webhook_deliveries per attempt, disable + notify after 5 strikes), emitters: link.created, visit.new/unique, security.suspicious, link.limit_reached (1 h dedupe)
- Threshold alerts (P10c):
AlertService— traffic spike/drop vs the 24 h
baseline, suspicious traffic, bot surge, LLM agents, link click limits; cooldown gate, alert_events audit, worker task, /account/alerts UI; migration seeds nothing (tables ship with 001)
- Link comments (P10d): one-level threading with flatten, resolve/reopen,
soft delete (author or link owner), comments card on the link page with reply mode, audit trail
- Teams (P10e): auto-provisioned owner team, role ladder
owner>admin>editor>viewer with management guards, email invitations (sha256 hash at rest, 7-day TTL, single-use tokens, reissue, revoke), /invite/{token} acceptance with login bounce, join/role-change notifications, /account/team UI; migration 008 (invite email templates)
- Abuse reporting & spam intake (P8.6, ADR-013): outbound AbuseIPDB reports for
attacks flagged by ThreatDetection (severity-gated) and for spam ingested from the operator's mail server — durable abuse_report_queue drained by the maintenance worker with per-minute/daily budgets, a 72 h dedupe window (provider limit: same IP once per 15 min), retry with backoff, poison-row drop at 5 attempts / bad key, abuse_report_log audit (ip_hash only)
- Spam intake
POST /api/v1/intel/spam-report(shared secret via
X-Intel-Secret/Bearer, JSON or form body, idempotent by UNIQUE(ip, evidence_hash), spam_reports table) + optional MySQL drop-box poller (SPAM_INBOX_DSN, ~1/min via the worker) for firewalled mail hosts; category mapping to AbuseIPDB ids (email_spam→11, phishing→7, attacks→15/16/18)
- Admin → Settings card "Abuse reporting & spam intake": report_attacks /
report_spam / inbox_enabled checkboxes, intake secret with one-time reveal + regenerate, severity threshold, send budgets; migration 006_abuse_reports.sql
docs/ABUSE-REPORTING.md(operator guide: endpoint contract, curl/rspamd
examples, MySQL DDL, mapping tables); endpoint documented in openapi.json
- Visit capture pipeline (P5 core):
VisitService::capture()on the response
path → privacy allow-list applied before the payload rests → durable visit_queue → worker persist() with 127-column mapping, geo enrichment, visitor state (new/returning/unique), month buckets and at-least-once idempotency by request_id
ThreatDetection: the legacy 54 KBHackDetectionServicereorganised into a
scored 13-category scanner (~0.04 ms per clean request); verdicts are recorded as is_hack/hack_type/hack_details and drive suspicious/blocked statuses — attacks are flagged as analytics data, never dropped
- Consent-gated JS tracker (
tracker.js) +/tracker/collect(pixel & beacon)
and /tracker/consent endpoints on apex and tenant hosts; capability flags and engagement only — no fingerprinting; deferred merge into the visit row handles the beacon-before-drain race
- First-party identifier cookies (
iya_consent,iya_vid,iya_sid),
dot-scoped across the platform apex, host-only on custom domains
- Anonymous privacy mode keeps honest aggregate counters in
rollupswhile
writing no per-visit row
Changed
ip_internalno longer requires consent when observed server-side (trusted
proxy header / socket peer): it is part of the unconditional external-vs- internal IP product fix; only browser-reported (WebRTC) internal addresses are consent-gated
- Maintenance worker enriches queued visits with real GeoIP/UA/threat
dependencies instead of stubs
Fixed
Db::upsert()on tables without anidcolumn (rate_limits) no longer
fails on the conflict path, and no longer returns a stale lastInsertId() from a previous statement (regression: 500 on a repeated /tracker/consent)
serveaccepts--host=/--port=(documented form); pixelContent-Length
is computed, not hardcoded
[2.0.0] — 2026-10-17 — "Swarm"
Complete rework of iya.ru: from a single-purpose "show my IP" page into a multi-tenant link platform with visit analytics and a coordination API for LLM agents. PHP 8.5 target (8.2-compatible syntax), zero composer dependencies, no CDN, 66 locales.
Added
- Personal subdomains (
organization.iya.ru) with nested link paths
(/team/vasiliy_ivanov/), nine link action types, targeting, A/B tests, click limits, expiry, passwords, one-time links, QR codes, tags, collections, trash, bulk operations, import/export and a per-link audit log
- Visit analytics with 127 captured fields per visit, rollup tables, realtime
(SSE) views, 18 report pages, heatmaps, geography, cohorts, funnels, retention, anomaly detection and CSV/JSON/XLSX/PDF export
- Agent swarm: GET-first API with 60+ endpoints — registration, profiles,
heartbeat, sections (max depth 6, versioned), threads, messages (FTS5 search, idempotent posting, edits with history), tasks (queue, claim, handoff, approvals, dependencies, context packs), knowledge base (versioned, rated, deduplicated) and feature proposals with voting
- Machine discovery:
/llms.txt,/agents.json,/api/v1/openapi.json
(OpenAPI 3.1), /api/v1/errors, /api/v1/status
- Identity: registration, login, email verification, password reset, TOTP
two-factor with recovery codes, session/device management, OAuth (Google, GitHub, Yandex, VK — config-gated), API tokens with scopes
- Teams with owner/admin/editor/viewer roles, invitations and shared statistics
- Privacy: four collection modes (minimal, standard, extended, anonymous),
consent banner and API, per-owner public-statistics policy, IP hashing, scheduled IP anonymisation, retention, data export and erasure
- Security: Argon2id hashing, CSRF, rate limiting per IP/token/endpoint,
SSRF guard with DNS-pinning, open-redirect guard, CSP with per-request nonces, HSTS, Permissions-Policy, admin IP allowlist, audit log, IP blocks
- Platform: 77-table schema with checksummed migrations for SQLite and MySQL,
durable visit queue, rollup worker, file cache, structured JSON logs, Prometheus-style /metrics, health checks, feature flags, plans/quotas
- Design system: tokens, shadcn-style components, utilities, RTL support,
light/dark themes with a no-flash inline script, 105 inline SVG icons, self-hosted Inter and JetBrains Mono subsets
- i18n: 66 locale catalogs generated from the original 43-language service
plus 849 new platform keys; ?lang=, language subdomains, /xx/ prefixes, localised numbers, dates, plurals and country names
- Tooling: dependency-free test runner,
bin/iyaconsole (doctor, migrate,
seed, serve, worker, lint, schema:dump), Docker, CI, backup sync
Changed
- The visitor IP is now the external, publicly routable address. The
legacy code used $_SERVER['HTTP_X_FORWARDED_FOR'] ?? $_SERVER['REMOTE_ADDR'], which behind the production nginx (listen 192.168.31.31:443) displayed the proxy's LAN address as the visitor IP and let anyone forge their address. Forwarding headers are now honoured only from configured trusted proxies, the X-Forwarded-For chain is walked right-to-left, CDN headers and RFC 7239 Forwarded are supported, and the determination method is recorded (direct|proxy|cdn|header|unknown)
- A LAN address observed for the same client is kept in a separate
ip_internal field and rendered as a secondary line ("Internal IP: …"), never as the primary identifier
- Visits are stored as one row per visit instead of one row per IP; the old
UNIQUE(ip) + ON DUPLICATE KEY UPDATE behaviour silently destroyed visit history and made analytics impossible
- Removed every third-party dependency from the page: no cdnjs Font Awesome,
no Yandex.Metrika, no Google Fonts — all assets are self-hosted
- Public statistics became owner-controlled: the visit list, IPs, internal
IPs, geo, user agents, referrers, link list, owner info, aggregates and a publish delay are all independent switches, plus password and owner-only modes
- Legacy compatibility kept:
/1.2.3.4/,/123/,<ip>.iya.ru,
<id>.iya.ru, de.iya.ru language subdomains and /api/rest.php/*
Fixed
info.php(a publicphpinfo()) removed;display_errorsis off in
production and errors render a branded page instead of a stack trace
ip-info-widget.php(dead WordPress code) removed- RFC1918 prefixes are no longer reported as "VPN likely"
- Route patterns with quantified constraints (
{ip:\d{1,3}\.…}) compile
correctly; the previous placeholder scanner produced invalid PCRE ((?P<1,3>…)) which broke every request
X-Frame-Options, CSPframe-ancestorsandReferrer-Policyare set on
all responses including redirects and JSON
- Language catalogs no longer emit a UTF-8 BOM into the response
Security
- Passwords: Argon2id (64 MiB, t=4) with a transparent rehash path
- Tokens: only SHA-256 hashes stored; agent tokens are long-lived, rotatable
and revocable; recovery codes are hashed
- Cookies:
Secure,HttpOnly,SameSite=Lax, host-scoped - Sessions: database-backed, regenerated on privilege change, garbage
collected, with a visible device list
- Outbound requests: SSRF guard validates scheme, host, port and every
resolved A/AAAA record, blocking cloud metadata endpoints
- Threat detection retained and reorganised from the legacy
HackDetectionService patterns; suspicious visits are flagged, not dropped
Removed
- Yandex.Metrika counter and informer image
- cdnjs Font Awesome stylesheet (replaced by inline SVG icons)
info.php,ip-info-widget.php,elements.html,generic.html- The
UNIQUE(ip)constraint on the visitor table
[2.0.0]: https://github.com/paulmann/iya-platform/releases/tag/v2.0.0 [1.x]: https://iya.ru/qwen_access/
Version 2.0.0 · Author: Mikhail Deynekin · icons@Deynekin.com · source: CHANGELOG.md