From 9c4d693b25ddd28e60c0727fb9a85e42d2cf5783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Janczak?= Date: Mon, 8 Jun 2026 16:11:47 +0200 Subject: [PATCH] release: v5.2.3 - Redis optimizations, stats scaling, DNS and CSP fixes --- .gitignore | 3 + CHANGELOG.md | 363 +++ LICENSE | 300 ++- README.md | 1071 +++++++-- art/diagrams/01-system-overview.svg | 98 + art/diagrams/02-redirect-lifecycle.svg | 109 + art/diagrams/03-stateless-vs-stateful.svg | 78 + art/diagrams/04-custom-domain-routing.svg | 80 + art/diagrams/05-targeting-and-ab.svg | 85 + art/diagrams/06-webhooks-and-security.svg | 83 + art/diagrams/07-stats-and-queue.svg | 116 + art/diagrams/08-audit-trust-summary.svg | 143 ++ art/diagrams/generate-glass-diagrams.py | 353 +++ art/v3_1.png | Bin 246922 -> 0 bytes art/v3_2.png | Bin 256743 -> 0 bytes art/v3_3.png | Bin 478473 -> 0 bytes art/v3_4.png | Bin 257687 -> 0 bytes art/v3_5.png | Bin 471074 -> 0 bytes composer.lock | 24 +- config/filament-short-url.php | 236 +- ...api_and_utm_fields_to_short_urls_table.php | 48 + ...001_audit_schema_and_performance_fixes.php | 63 + ...001_add_security_counts_to_daily_stats.php | 24 + ...cross_dimensional_stats_to_daily_stats.php | 24 + phpstan.neon.dist | 9 + resources/css/plugin.css | 2133 +++++++++++++++++ resources/dist/filament-short-url.css | 2 +- resources/dist/filament-short-url.js | 2 +- resources/dist/meta-scraper.js | 216 +- resources/lang/en/default.php | 220 +- resources/lang/pl/default.php | 220 +- resources/views/app-linking-preview.blade.php | 131 +- .../forms/components/number-stepper.blade.php | 216 ++ .../components/segment-control.blade.php | 169 ++ .../components/traffic-splitter.blade.php | 2 +- .../views/modals/share-after-create.blade.php | 4 +- resources/views/pixel-loading.blade.php | 2 +- resources/views/public-stats-layout.blade.php | 48 + .../views/public-stats-password.blade.php | 27 + .../views/public-stats-rate-limited.blade.php | 12 + resources/views/public-stats.blade.php | 69 + resources/views/redirect-html.blade.php | 57 +- .../views/sidebar/social-preview.blade.php | 34 +- .../table/public-stats-url-field.blade.php | 44 + .../views/webhook-payload-example.blade.php | 96 +- .../views/widgets/device-breakdown.blade.php | 6 +- .../views/widgets/geo-breakdown.blade.php | 6 +- resources/views/widgets/live-feed.blade.php | 61 +- .../views/widgets/sources-breakdown.blade.php | 12 +- resources/views/widgets/variants.blade.php | 2 +- routes/web.php | 66 +- scripts/k6/redirect-baseline.js | 92 + .../AggregateAndPruneVisitsCommand.php | 299 ++- .../Commands/StressRedirectCommand.php | 83 + .../Commands/SyncBufferedCountersCommand.php | 115 +- .../Commands/VerifyCustomDomainsCommand.php | 55 + .../Forms/Components/NumberStepper.php | 194 ++ .../Forms/Components/SegmentControl.php | 223 ++ .../ShortUrlCustomDomainResource.php | 17 + .../Pages/ListShortUrlCustomDomains.php | 7 + .../Resources/ShortUrlPixelResource.php | 74 +- src/Filament/Resources/ShortUrlResource.php | 10 + .../ShortUrlResource/Pages/ListShortUrls.php | 27 +- .../Pages/Settings/Tabs/AdvancedTab.php | 91 +- .../Pages/Settings/Tabs/DeveloperTab.php | 11 +- .../Pages/Settings/Tabs/Ga4Tab.php | 83 +- .../Pages/Settings/Tabs/GeneralTab.php | 209 +- .../Pages/Settings/Tabs/GeoIpTab.php | 7 +- .../Pages/ShortUrlSettingsPage.php | 13 +- .../Pages/ViewShortUrlLogs.php | 19 +- .../Pages/ViewShortUrlStats.php | 4 + .../Schemas/Fields/QrDesignerSidebarField.php | 148 +- .../ShortUrlResource/Schemas/ShortUrlForm.php | 45 +- .../Support/PasswordOpenGraphGuard.php | 84 + .../Schemas/Support/TabCardHeader.php | 45 + .../Schemas/Support/WebhookPayloadExample.php | 107 + .../ShortUrlResource/Schemas/Tabs/LinkTab.php | 685 +++--- .../Schemas/Tabs/MarketingTab.php | 178 +- .../Schemas/Tabs/PasswordTab.php | 337 ++- .../Schemas/Tabs/SeoAndCloakingTab.php | 255 +- .../Schemas/Tabs/TargetingTab.php | 848 ++++--- .../Schemas/Tabs/TrackingTab.php | 309 ++- .../Schemas/Tabs/ValidityAndLimitsTab.php | 258 +- .../Tables/ShortUrlsTable.php | 67 + .../Widgets/ShortUrlLiveFeedWidget.php | 30 +- .../ShortUrlSecurityBreakdownWidget.php | 66 +- .../Widgets/ShortUrlVisitsChart.php | 247 +- .../Widgets/ShortUrlWorldMapWidget.php | 75 +- src/FilamentShortUrlServiceProvider.php | 44 +- .../Controllers/ShortUrlApiController.php | 392 ++- .../ShortUrlFolderApiController.php | 114 + .../ShortUrlLiveFeedStreamController.php | 150 ++ .../Controllers/ShortUrlLogoController.php | 6 - .../ShortUrlPublicStatsController.php | 230 ++ .../ShortUrlRedirectController.php | 488 +--- .../Controllers/ShortUrlTagApiController.php | 114 + .../Controllers/ShortUrlUtilityController.php | 39 + .../Middleware/AuthenticateShortUrlApi.php | 37 +- .../Concerns/ShortUrlApiAttributes.php | 55 + src/Http/Requests/StoreShortUrlRequest.php | 31 +- src/Http/Requests/UpdateShortUrlRequest.php | 47 +- src/Http/Requests/UpsertShortUrlRequest.php | 92 + src/Http/Resources/ShortUrlResource.php | 37 +- src/Http/Resources/ShortUrlVisitResource.php | 46 + src/Http/Support/ApiLinkScope.php | 56 + src/Jobs/SendWebhookJob.php | 27 + src/Jobs/TrackShortUrlVisitJob.php | 73 +- src/Jobs/VerifyQueueWorkerJob.php | 29 + src/Models/Concerns/HasSecurityStats.php | 119 + src/Models/Concerns/HasStats.php | 977 +------- src/Models/Concerns/HasStatsCache.php | 604 +++++ src/Models/Concerns/HasStatsQueries.php | 269 +++ src/Models/Concerns/HasTargeting.php | 13 +- src/Models/Concerns/HasVisitCounters.php | 292 +++ src/Models/ShortUrl.php | 305 ++- src/Models/ShortUrlCustomDomain.php | 285 ++- src/Models/ShortUrlDailyStats.php | 24 + src/Models/ShortUrlFolder.php | 6 +- src/Models/ShortUrlTag.php | 6 +- src/Policies/ShortUrlPolicy.php | 72 + src/Rules/OutboundUrl.php | 33 + src/Rules/SafeUrl.php | 4 + src/Services/BotDetector.php | 136 ++ src/Services/BotIpVerifier.php | 46 + .../Ga4MeasurementProtocolService.php | 284 +++ src/Services/GeoIpService.php | 29 +- src/Services/IframeableChecker.php | 132 + src/Services/LinkUtmMerger.php | 47 + src/Services/LiveFeedBroadcaster.php | 221 ++ src/Services/OgFormImageResolver.php | 77 + src/Services/OgImageImporter.php | 27 +- src/Services/OgMetaPresenter.php | 104 + src/Services/OutboundUrlValidator.php | 24 + src/Services/ProxyDetectionService.php | 16 +- .../Queue/PluginQueueWorkerTester.php | 110 + src/Services/Queue/QueueWorkerProbe.php | 61 + src/Services/RedirectUrlResolver.php | 39 + src/Services/Redis/PluginRedisConnection.php | 97 + .../Redis/PluginRedisConnectionTester.php | 162 ++ src/Services/RobotsTagApplicator.php | 36 + src/Services/SafeBrowsingService.php | 24 +- src/Services/ShortUrlPasswordHasher.php | 41 + src/Services/ShortUrlRedirectHandler.php | 474 ++++ src/Services/ShortUrlService.php | 17 +- src/Services/ShortUrlSettingsManager.php | 284 ++- src/Services/ShortUrlTracker.php | 81 +- .../Stats/CrossDimensionalStatsEngine.php | 526 ++++ src/Services/Stats/FilteredStatsCollector.php | 439 ++++ src/Services/Stats/StatsCacheHelper.php | 59 + src/Services/Stats/StatsScalingProfile.php | 101 + src/Services/Stats/StatsVisitRecorder.php | 32 + src/Services/Stats/TodayStatsBuffer.php | 232 ++ src/Services/StatsSqlHelper.php | 84 + src/Services/UrlMetaScraper.php | 68 +- src/Services/UserAgentParser.php | 13 +- src/Services/VisitCounterBuffer.php | 644 +++++ src/Services/VisitCsvExporter.php | 82 + src/Services/VisitSlotReservation.php | 168 ++ src/Support/ApiStatsFilterParser.php | 58 + src/Support/CustomDomainValidator.php | 50 + src/Support/HostNormalizer.php | 65 + src/Support/LinkUserScope.php | 53 + src/Support/LockedUrlKeyGuard.php | 60 + src/Support/ResourceOwnershipValidator.php | 44 + src/Support/ShortUrlCacheInvalidator.php | 58 + tests/Feature/AuditEnhancementsTest.php | 222 ++ tests/Feature/AuditFixesTest.php | 164 ++ tests/Feature/BotDetectorTest.php | 52 + tests/Feature/CrossDimensionalStatsTest.php | 171 ++ tests/Feature/EnterpriseHardeningTest.php | 179 ++ tests/Feature/IframeableCheckerTest.php | 44 + tests/Feature/LiveFeedBroadcasterTest.php | 47 + tests/Feature/OgFormImageResolverTest.php | 53 + tests/Feature/PasswordOpenGraphTest.php | 65 + tests/Feature/PublicStatsUiTest.php | 82 + tests/Feature/RedisSettingsTest.php | 70 + tests/Feature/RoadmapHardeningTest.php | 304 +++ tests/Feature/SecurityHardeningFixesTest.php | 139 ++ tests/Feature/ShareAfterCreateModalTest.php | 50 + tests/Feature/ShortUrlApiTest.php | 4 + .../ShortUrlComplexInteractionsTest.php | 5 - .../Feature/ShortUrlCoreOptimizationTest.php | 76 +- tests/Feature/ShortUrlCustomDomainTest.php | 2 +- tests/Feature/ShortUrlRedirectTest.php | 80 +- tests/Feature/ShortUrlScrapeMetaTest.php | 5 +- .../ShortUrlSettingsAndFormOptionsTest.php | 13 +- tests/Feature/ShortUrlTempStorageTest.php | 2 +- tests/Feature/StatsAuditHardeningTest.php | 256 ++ tests/Feature/StatsScalingTest.php | 168 ++ tests/Feature/WorldClassHardeningTest.php | 253 ++ tests/Unit/LockedUrlKeyGuardTest.php | 54 + tests/Unit/NumberStepperTest.php | 43 + .../Unit/PluginRedisConnectionTesterTest.php | 27 + tests/Unit/SegmentControlTest.php | 67 + tests/Unit/WebhookPayloadExampleTest.php | 13 + 195 files changed, 21717 insertions(+), 4098 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 art/diagrams/01-system-overview.svg create mode 100644 art/diagrams/02-redirect-lifecycle.svg create mode 100644 art/diagrams/03-stateless-vs-stateful.svg create mode 100644 art/diagrams/04-custom-domain-routing.svg create mode 100644 art/diagrams/05-targeting-and-ab.svg create mode 100644 art/diagrams/06-webhooks-and-security.svg create mode 100644 art/diagrams/07-stats-and-queue.svg create mode 100644 art/diagrams/08-audit-trust-summary.svg create mode 100644 art/diagrams/generate-glass-diagrams.py delete mode 100644 art/v3_1.png delete mode 100644 art/v3_2.png delete mode 100644 art/v3_3.png delete mode 100644 art/v3_4.png delete mode 100644 art/v3_5.png create mode 100644 database/migrations/2026_06_08_000001_add_api_and_utm_fields_to_short_urls_table.php create mode 100644 database/migrations/2026_06_09_000001_audit_schema_and_performance_fixes.php create mode 100644 database/migrations/2026_06_10_000001_add_security_counts_to_daily_stats.php create mode 100644 database/migrations/2026_06_11_000001_add_cross_dimensional_stats_to_daily_stats.php create mode 100644 phpstan.neon.dist create mode 100644 resources/views/forms/components/number-stepper.blade.php create mode 100644 resources/views/forms/components/segment-control.blade.php create mode 100644 resources/views/public-stats-layout.blade.php create mode 100644 resources/views/public-stats-password.blade.php create mode 100644 resources/views/public-stats-rate-limited.blade.php create mode 100644 resources/views/public-stats.blade.php create mode 100644 resources/views/table/public-stats-url-field.blade.php create mode 100644 scripts/k6/redirect-baseline.js create mode 100644 src/Console/Commands/StressRedirectCommand.php create mode 100644 src/Console/Commands/VerifyCustomDomainsCommand.php create mode 100644 src/Filament/Forms/Components/NumberStepper.php create mode 100644 src/Filament/Forms/Components/SegmentControl.php create mode 100644 src/Filament/Resources/ShortUrlResource/Schemas/Support/PasswordOpenGraphGuard.php create mode 100644 src/Filament/Resources/ShortUrlResource/Schemas/Support/TabCardHeader.php create mode 100644 src/Filament/Resources/ShortUrlResource/Schemas/Support/WebhookPayloadExample.php create mode 100644 src/Http/Controllers/ShortUrlFolderApiController.php create mode 100644 src/Http/Controllers/ShortUrlLiveFeedStreamController.php create mode 100644 src/Http/Controllers/ShortUrlPublicStatsController.php create mode 100644 src/Http/Controllers/ShortUrlTagApiController.php create mode 100644 src/Http/Controllers/ShortUrlUtilityController.php create mode 100644 src/Http/Requests/Concerns/ShortUrlApiAttributes.php create mode 100644 src/Http/Requests/UpsertShortUrlRequest.php create mode 100644 src/Http/Resources/ShortUrlVisitResource.php create mode 100644 src/Http/Support/ApiLinkScope.php create mode 100644 src/Jobs/VerifyQueueWorkerJob.php create mode 100644 src/Models/Concerns/HasSecurityStats.php create mode 100644 src/Models/Concerns/HasStatsCache.php create mode 100644 src/Models/Concerns/HasStatsQueries.php create mode 100644 src/Models/Concerns/HasVisitCounters.php create mode 100644 src/Policies/ShortUrlPolicy.php create mode 100644 src/Rules/OutboundUrl.php create mode 100644 src/Services/BotDetector.php create mode 100644 src/Services/BotIpVerifier.php create mode 100644 src/Services/Ga4MeasurementProtocolService.php create mode 100644 src/Services/IframeableChecker.php create mode 100644 src/Services/LinkUtmMerger.php create mode 100644 src/Services/LiveFeedBroadcaster.php create mode 100644 src/Services/OgFormImageResolver.php create mode 100644 src/Services/OgMetaPresenter.php create mode 100644 src/Services/OutboundUrlValidator.php create mode 100644 src/Services/Queue/PluginQueueWorkerTester.php create mode 100644 src/Services/Queue/QueueWorkerProbe.php create mode 100644 src/Services/RedirectUrlResolver.php create mode 100644 src/Services/Redis/PluginRedisConnection.php create mode 100644 src/Services/Redis/PluginRedisConnectionTester.php create mode 100644 src/Services/RobotsTagApplicator.php create mode 100644 src/Services/ShortUrlPasswordHasher.php create mode 100644 src/Services/ShortUrlRedirectHandler.php create mode 100644 src/Services/Stats/CrossDimensionalStatsEngine.php create mode 100644 src/Services/Stats/FilteredStatsCollector.php create mode 100644 src/Services/Stats/StatsCacheHelper.php create mode 100644 src/Services/Stats/StatsScalingProfile.php create mode 100644 src/Services/Stats/StatsVisitRecorder.php create mode 100644 src/Services/Stats/TodayStatsBuffer.php create mode 100644 src/Services/StatsSqlHelper.php create mode 100644 src/Services/VisitCounterBuffer.php create mode 100644 src/Services/VisitCsvExporter.php create mode 100644 src/Services/VisitSlotReservation.php create mode 100644 src/Support/ApiStatsFilterParser.php create mode 100644 src/Support/CustomDomainValidator.php create mode 100644 src/Support/HostNormalizer.php create mode 100644 src/Support/LinkUserScope.php create mode 100644 src/Support/LockedUrlKeyGuard.php create mode 100644 src/Support/ResourceOwnershipValidator.php create mode 100644 src/Support/ShortUrlCacheInvalidator.php create mode 100644 tests/Feature/AuditEnhancementsTest.php create mode 100644 tests/Feature/AuditFixesTest.php create mode 100644 tests/Feature/BotDetectorTest.php create mode 100644 tests/Feature/CrossDimensionalStatsTest.php create mode 100644 tests/Feature/EnterpriseHardeningTest.php create mode 100644 tests/Feature/IframeableCheckerTest.php create mode 100644 tests/Feature/LiveFeedBroadcasterTest.php create mode 100644 tests/Feature/OgFormImageResolverTest.php create mode 100644 tests/Feature/PasswordOpenGraphTest.php create mode 100644 tests/Feature/PublicStatsUiTest.php create mode 100644 tests/Feature/RedisSettingsTest.php create mode 100644 tests/Feature/RoadmapHardeningTest.php create mode 100644 tests/Feature/SecurityHardeningFixesTest.php create mode 100644 tests/Feature/ShareAfterCreateModalTest.php create mode 100644 tests/Feature/StatsAuditHardeningTest.php create mode 100644 tests/Feature/StatsScalingTest.php create mode 100644 tests/Feature/WorldClassHardeningTest.php create mode 100644 tests/Unit/LockedUrlKeyGuardTest.php create mode 100644 tests/Unit/NumberStepperTest.php create mode 100644 tests/Unit/PluginRedisConnectionTesterTest.php create mode 100644 tests/Unit/SegmentControlTest.php create mode 100644 tests/Unit/WebhookPayloadExampleTest.php diff --git a/.gitignore b/.gitignore index 30a5fd4..b4d426c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,6 @@ *.log .DS_Store Thumbs.db + +# Internal maintainer notes (not shipped) +/AUDIT_IMPLEMENTATION.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..4d40de2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,363 @@ +# Changelog + +All notable changes to **janczakb/filament-short-url** are documented here. + +Recent releases (v5.x) are summarized in [README.md](README.md#changelog). This file contains the full history for older versions and the complete **v5.2.0** release notes (including enterprise hardening follow-up, signed 2026-06-08). + +--- + +## v5.2.3 + +> **Production Redis mode, stats scaling for high traffic, and Settings diagnostics.** + +### Settings → Redis & queue diagnostics + +- **Redis connection in Settings** — When **Queue Connection = `redis`**, configure host, port, password, database, and key prefix in **Settings → General**. Values are stored in `short_url_settings` and **override** `database.redis` + `queue.connections.redis` at runtime (independent of `CACHE_STORE`). +- **Test Redis connection** — PING + pipelined `INCR`/`SADD` probe (same Redis primitives as visit counters). Uses unsaved form values via preview overrides. +- **Test queue worker** — Dispatches `VerifyQueueWorkerJob` on the selected connection/queue and waits up to 12 s for a worker to process it; reports the exact `php artisan queue:work {connection} --queue={name}` command on failure. +- **Unified queue callout** — Single info box per async driver with a **dynamic** worker command from current Settings (no duplicate boxes; hidden entirely for `sync`). +- **`withPreviewSettings()`** — Settings test buttons apply form state temporarily without saving. + +### Stats scaling (`queue_connection = redis`) + +- **`StatsScalingProfile`** — Central profile: auto counter buffering, dedicated Redis counters, Redis today buffer, SQL micro-cache for sync mode. +- **`PluginRedisConnection`** — Resolves Redis from `queue.connections.redis` (phpredis/Predis), not from `CACHE_STORE`. +- **`VisitCounterBuffer`** — Unified buffered totals/uniques: dedicated Redis path when Settings queue is `redis`, Laravel cache path for sync + manual toggle. +- **`TodayStatsBuffer`** — Pipelined Redis counters for today's summary and hourly charts; merged live in `HasStatsCache` without busting historical stats cache on every click. +- **`StatsVisitRecorder`** — Hot-path stats side effects extracted from the visit job; **`TrackShortUrlVisitJob`** no longer clears full stats cache per visit. +- **`SyncBufferedCountersCommand`** — Refactored to flush via `VisitCounterBuffer` (Redis dirty-set + cache path). +- **Live feed** — `LiveFeedBroadcaster` prefers plugin queue Redis over cache-store Redis (unchanged priority, now shares infra with counters/stats when queue is `redis`). + +### Config & migrations + +- **`config/filament-short-url.php`** — New `redis.*` defaults (`REDIS_HOST`, `REDIS_PORT`, …) used as fallbacks before Settings overrides. +- **Service provider** — All package migrations registered in `hasMigrations()` including `2026_06_10_000001` and `2026_06_11_000001`. + +### Tests + +- **`tests/Feature/StatsScalingTest.php`** — Redis queue profile, today buffer, counter buffering. +- **`tests/Feature/RedisSettingsTest.php`** — Config overrides, masked password preservation, sync worker test. +- **`tests/Unit/PluginRedisConnectionTesterTest.php`** — Redis health-check service. + +--- + +## v5.2.2 + +> **Enterprise hardening, airtight visit counters, trait refactor, and i18n audit.** + +### Security + +- **XSS** — `@json()` in pixel interstitial and Alpine stats widgets (replaces `addslashes()`). +- **Atomic `max_visits`** — `VisitSlotReservation::reserveAtomicSlot()` with fast-path SQL and pessimistic lock near cap. +- **Single-use bots** — BotDetector gate before consuming single-use links. +- **Buffer double-count** — `revertBufferedIncrements()` before DB fallback in `HasVisitCounters`. +- **API tenancy** — `owner_user_id` required on API keys when `scope_links_to_user` is enabled. +- **IDOR** — `ResourceOwnershipValidator` for folder/tag/pixel on API writes. +- **Redirect pipeline** — Limits before app interstitial; caps enforced when `track_visits=false`. + +### Performance + +- **`max_visits_pessimistic_remaining`** config (default 5). +- **`LiveFeedBroadcaster`** — Redis source priority: plugin **Settings → Queue Connection = redis** first, then `CACHE_STORE=redis`; pub/sub push with PhpRedis, SSE poll fallback otherwise. +- **`StatsCacheHelper`** — `Cache::lock` on stats miss. +- **`preloadBufferedCountersForIds()`** — Batch buffer reads in list tables. + +### Quality + +- **Public stats** — Uniform 404; public-safe response subset. +- **Octane** — A/B variant on request attributes. +- **Filtered uniques** — Rollup merge in `FilteredStatsCollector`. +- **`findByKey()`** — `domain_scope_id` index path. + +### Maintainability + +- **`HasStats` facade** — `HasVisitCounters`, `HasStatsCache`, `HasStatsQueries`, `HasSecurityStats`. +- **i18n** — API, redirect, and visitor-facing strings use translation keys (EN + PL). + +### Tests + +- **`tests/Feature/WorldClassHardeningTest.php`** — Comprehensive audit regression suite. + +--- + +## v5.2.1 + +> **Audit roadmap follow-up** — Authorization policy, API stats parity, SSE live feed, load baselines, and hardening tests. + +### Authorization + +- **`ShortUrlPolicy`** — `view`, `update`, `delete`, `restore`, `forceDelete`, and `manageSettings` aligned with `scope_links_to_user` / `user_id`. Auto-registered in `FilamentShortUrlServiceProvider` when the host has no custom policy. +- **Stats page** — `ViewShortUrlStats` calls `authorize('view', $record)`. + +### REST API + +- **Filtered stats** — `GET /links/{idOrKey}/stats` passes query filters to `FilteredStatsCollector` (same dimensions as the Filament stats dashboard). +- **`ApiStatsFilterParser`** — Parses `CrossDimensionalStatsEngine::FILTER_KEYS` plus aliases `country` → `country_code`, `device` → `device_type`. +- **Response meta** — Returns `meta.date_from`, `meta.date_to`, and `meta.filters`. + +### Live feed (SSE) + +- **`ShortUrlLiveFeedStreamController`** — Authenticated SSE endpoint at `GET /short-url/live-feed/{shortUrl}/stream`. +- **Widget** — Alpine `EventSource` + `ShortUrlLiveFeedWidget::onStreamUpdate()` replaces Livewire polling. +- **Config** — `live_feed.sse_interval_seconds` (default 3), `live_feed.sse_max_duration_seconds` (default 120). + +### Load & stress baselines + +- **`php artisan short-url:stress-redirect {key}`** — In-process redirect benchmark (avg / min / p95 / max ms). +- **`scripts/k6/redirect-baseline.js`** — k6 HTTP load script with setup probe, configurable VUs/duration, and p95 thresholds. + +### Tests + +- **`tests/Feature/RoadmapHardeningTest.php`** — Policy scoping, API stats filters, webhook HMAC E2E via `Http::fake()`, `max_visits` burst guard, SSE stream authorization, stress command smoke test. + +--- + +## v5.2.0 + +> **Main release theme: SEO & Social + integration hardening** — Open Graph metadata, link cloaking, search indexing control, expanded REST API, production security fixes, and **enterprise hardening** (GA4 MP, Geo-IP, custom domain DNS gate). + +### Stats & security hardening (audit follow-up) + +- **Filtered stats optimizer** — `FilteredStatsCollector` replaces 15+ raw GROUP BY queries with daily JSON rollups + single summary + cursor pass. +- **Cross-dimensional daily rollups** — `CrossDimensionalStatsEngine` stores single- and two-filter slices in `short_url_daily_stats` (`cross_dimensional_stats`, `cross_filter_pairs`, `filter_qr_counts`); aggregation uses one cursor pass instead of 11 GROUP BY queries; re-aggregation backfills NULL cross columns. +- **Stats cache helper** — `StatsCacheHelper` wraps remember/forget with driver-agnostic fallbacks (file, database, Redis, Memcached, array). +- **Security widget cache** — `getSecurityBreakdownStats()` with daily `all_visits_count` / `bot_visits_count` / `proxy_visits_count` rollups. +- **World map** — Always routes through `getCachedStats()`. +- **VPN fail-closed** — When `block_with_403`, detection timeout/error blocks access. +- **Public stats password** — Accepted only via POST body or `Authorization` header (not query string). +- **Custom domain legacy URLs** — `/s/{key}` → 301 → `/{key}` on verified custom domains. + +### SEO, Open Graph & Link Cloaking + +- **New SEO & Social tab** — `og_title`, `og_description`, `og_image`, `do_index`, `is_cloaked`, live social preview sidebar. +- **Custom Open Graph meta** — Per-link OG/Twitter tags for Facebook, LinkedIn, X, Slack, WhatsApp, and other preview bots. +- **Search Engine Indexing (`do_index`)** — Per-link toggle; when off, all responses emit `noindex, nofollow`. +- **Link Cloaking (`is_cloaked`)** — Iframe embedding on your short-link domain for humans; standalone OG page for crawlers. +- **OG meta scraper** — SSRF-safe streaming fetch (`GET /short-url/scrape-meta`). +- **OG image pipeline** — WebP conversion, temp storage, promotion to permanent disk on save. +- **Iframeable pre-check** — `POST /short-url/check-iframeable` with redirect-chain validation. + +### REST API + +- **Bulk create** — `POST /api/short-url/links/bulk` (max 100). +- **Upsert** — `PUT /api/short-url/links/upsert` by `external_id` or `url_key` + `destination_url`. +- **Helpers** — `GET /links/exists`, `/links/random`, `/links/info`. +- **CSV export** — `GET /links/{idOrKey}/visits/export`. +- **Tags & Folders CRUD** — `/api/short-url/tags`, `/api/short-url/folders`. +- **Per-key owner scoping** — Optional `owner_user_id` on API keys. +- **Link-level UTM** — `utm_*` and `ref` stored on the link and merged on redirect. +- **Expanded serializer** — `external_id`, UTM fields, tags, folder, archive, public stats flags. +- **Visits, bulk delete/update** — Paginated visits log, bulk delete by IDs/keys, bulk patch. + +### Public Stats + +- **Shareable endpoint** — `GET /short-url/public-stats/{url_key}` with optional password (`public_stats_enabled`, `public_stats_password`). + +### Security & Reliability + +- **Password hashing** — Link passwords hashed at rest; legacy plain-text re-hashed on save. +- **DNS-aware SSRF guard** — Webhooks, OG scraper, iframe checker block hostnames resolving to private IPs. +- **`OutboundUrl` rule** — Shared validation for outbound URLs. +- **Safe Browsing fail-closed** — Unreachable API rejects URLs when checking is enabled. +- **Global webhook toggle** — `global_webhook_enabled` must be on for global webhooks to fire. +- **`log-error` hardened** — Auth + throttle + validation. +- **Bot hardening** — Bot visit exclusion, optional click dedup, Googlebot IP verify, `?bot=1` secret. +- **Single-use fix** — No visit recorded after single-use consumption. +- **Redis counter fix** — Buffered stats sync with Redis cache driver. + +### Settings GUI + +- **Analytics & Bot Detection** — Click deduplication, Googlebot verify, bot debug secret in **Settings → Advanced**. + +### Database (safe upgrade) + +- **Migration `2026_06_08_000001`** — Adds nullable `external_id`, `utm_*`, `ref`, `public_stats_*`, and index on `short_url_visits (short_url_id, ip_hash)`. No existing data modified. +- **Migration `2026_06_09_000001`** — Adds `domain_scope_id`, composite unique `(url_key, domain_scope_id)`, FK on `custom_domain_id` (`ON DELETE SET NULL`), admin/aggregation indexes, and JSON stats columns on `short_url_daily_stats`. Existing rows backfilled (`domain_scope_id = custom_domain_id` where set, else `0`). +- **Cross-database** — No `->after()` hints, no DB-specific `ENUM`; runs on SQLite, MySQL, and PostgreSQL. + +### Performance & reliability audit + +- **`domain_scope_id` + composite unique** — Same slug on default domain and custom domains without collision. +- **Custom domain routing** — Root-level URLs on verified domains; reserved segments blocked (`api`, `admin`, `auth`, …). +- **`getRealTimeTotalVisits()`** — Merges buffered counters for accurate `max_visits` / single-use checks with cached redirect models. +- **HasStats / aggregation** — Incremental daily aggregation, bot/proxy filters, chunked prune, Redis dirty-set fix, scoped buffer reads, unique metric via `COUNT(DISTINCT ip_hash)`. +- **`SyncBufferedCountersCommand`** — Atomic batch flush; temp keys cleaned after commit. +- **`VerifyCustomDomainsCommand`** — Scheduled DNS re-verification for custom domains. +- **API** — Transactions on bulk/upsert, `ShortUrlCacheInvalidator`, scoped `exists`, hashed-only API keys, public stats throttle. +- **Security** — `AuthenticateShortUrlApi` drops plaintext legacy keys; public stats password from query/body/Authorization. +- **Settings** — VPN cache TTL + timeout in GUI; settings cache 3600s; queue default remains **`sync`** (opt-in `database`/`redis`). +- **Support classes** — `HostNormalizer`, `ShortUrlCacheInvalidator`, `LinkUtmMerger`, `ApiLinkScope`, `VisitCsvExporter`. +- **Verification doc** — Full itemized checklist in **[AUDIT_IMPLEMENTATION.md](AUDIT_IMPLEMENTATION.md)**. + +### Refactoring + +- **`ShortUrlRedirectHandler`**, **`RedirectUrlResolver`**, **`OgMetaPresenter`**, **`StatsSqlHelper`**, **`SafeUrl` rule**. + +### Enterprise hardening (follow-up — 2026-06-08) + +> **Signed:** Bartek Janczak — post-audit production hardening before v5.2.0 tag. + +#### GA4 Measurement Protocol (enterprise) + +- **`Ga4MeasurementProtocolService`** — Extracted from `TrackShortUrlVisitJob`; singleton registered in service provider. +- **Full MP payload** — `timestamp_micros`, `session_id`, `engagement_time_msec`, standard events `page_view` + `click`, custom `short_url_visit`. +- **Measurement ID validation** — Rejects invalid `ga_tracking_id` format before HTTP; logs and skips send. +- **Settings Test connection** — Requires real `G-XXXXXXXXXX` (or Firebase App ID); validates via GA4 debug collector with the configured secret (not placeholder `G-XXXXXXXXXX`). +- **Privacy-safe `client_id`** — Deterministic SHA-256 hash of IP + User Agent (unchanged semantics, now in dedicated service). + +#### Geo-IP foolproofing + +- **Headers driver auto-trust** — Saving `geo_ip_driver = headers` forces `trust_cdn_headers = true` in DB and runtime config. +- **Offline fallback chain** — When CDN headers are missing, Headers driver falls back to MaxMind, then ip-api (instead of returning empty geo). +- **ip-api rate limit** — Laravel `RateLimiter` guard: max 40 lookups/minute (`fsu_geo_ip_api:{YmdHi}`). +- **Settings UI** — GeoIP tab info callout explains auto-trust and fallback behaviour. + +#### Custom domains (DNS gate) + +- **Multi-record verification** — All A records resolved; multiple CNAME targets; IP match via `array_intersect`; CNAME chain to `app.url` host. +- **Activation enforcement** — `custom_domains.enforce_dns_on_activate` (default `true`, env `SHORT_URL_CUSTOM_DOMAIN_ENFORCE_DNS`); saving with `is_active=true` without DNS → auto-deactivate. +- **Removed `SERVER_ADDR` fallback** — Verification relies on DNS resolution against `app.url` only. + +#### API & settings + +- **Upsert + `lock_url_key`** — `UpsertShortUrlRequest` blocks changing `url_key` and `custom_domain_id` when lock is enabled (closes panel/API parity gap). +- **`ShortUrlSettingsManager`** — `last_aggregation_date` in system keys; secret preservation on masked save; `trust_cdn_headers` derived when driver is headers. + +#### Stats & reliability audit (P0–P2) + +- **Redis dirty-set prefix** — `SyncBufferedCountersCommand` uses store prefix for dirty ID set. +- **Webhook SSRF** — `SendWebhookJob`: `allow_redirects => false`, rejects 3xx responses. +- **Buffer fallback** — `HasStats::incrementVisits()` DB fallback when dirty-ID tracking fails. +- **Unique visits** — Human clicks from daily rollups; no DISTINCT override inflation. +- **Cross-filter stats** — Two-filter pairs in aggregation; gap fill in `FilteredStatsCollector`. +- **Per-link webhooks** — Respects `webhook_events` on link model. +- **Octane** — `GeoIpService::flush()`, `ShortUrl::flushBufferedCounterMemory()` on request terminate. +- **Security** — Redirect HTML XSS fix; public stats rate limit; visit logs default to stats-counted scope. +- **Tests** — `EnterpriseHardeningTest.php`, `StatsAuditHardeningTest.php`; run `php artisan test packages/filament-short-url/tests/ --compact` in the host app. + +#### Critical audit hardening (follow-up — 2026-06-08) + +- **Stateless redirect default** — Main `/s/{key}` route uses `throttle:120,1` only (no `web` middleware). Password flow stays on `/s-auth/{key}` with explicit `web`. +- **Filament `lock_url_key` server-side** — `LockedUrlKeyGuard` sanitizes Filament save data and blocks model persistence when key/domain change is attempted (parity with API). +- **Multi-tenant Filament scope** — `LinkUserScope` filters `ShortUrlResource` by `user_id` when `scope_links_to_user` is enabled (default). +- **Custom domain ownership in API** — `CustomDomainValidator` ensures API keys/users can only assign domains they own. +- **Scoped `exists` API** — `custom_domain_id` + `domain_scope_id` in response; checks per domain scope. +- **Safe Browsing on redirect** — `SafeBrowsingService::isSafeCached()` re-checks destination at redirect time (configurable TTL). +- **Webhook signing required** — Global webhooks require `webhook_signing_secret` when `webhook_signing_required` is true (default); jobs skip unsigned dispatch. +- **Hot path optimizations** — Single domain resolution passed to `findByKey()`; pixels lazy-loaded; `max_visits` enforced under row lock before tracking. +- **Docs** — README claims corrected (no sub-15ms; dub.co parity table scoped; production checklist; sync queue default documented). +- **Tests** — `SecurityHardeningFixesTest.php`, `LockedUrlKeyGuardTest.php`. + +--- + +## v4.0.0 + +- **Custom Domains Branding** — Let users connect branded custom domains with real-time CNAME/A record DNS verification and automatic prefix-free root routing. +- **Cache Leak Fix** — Solved a critical caching bug where single-use links could be visited multiple times during the cache TTL by passing the host-specific key suffix to cache invalidations. +- **Aggregator Performance Tuning** — Restructured daily aggregation SQL queries to filter only active URL IDs, leveraging composite indexes and running native DB aggregations. +- **Logo Upload Authorization** — Secured the logo upload API with resource-level permission and policy checks. +- **DB-Free Registration Phase** — Deferred settings overrides to the service provider boot phase to prevent database errors during offline configuration caching. +- **Conditional Redirection Fallback** — Added configurations to optionally disable global fallback route registration. + +## v3.5.0 + +- **User Attribution** — Short URLs now record the creator. Their avatar and hover card (name + email) are displayed in the list table. +- **Relative Date Badges** — Creation dates show compact relative strings (`2h`, `5d`, `3mo`). Hover to see the precise date and timezone. +- **Keyboard Shortcuts** — Hover any table row and press `E` (edit), `Q` (QR code), `I` (share), `S` (statistics), or `X` (delete). +- **Unified Action Dropdown** — All per-row actions consolidated under a single 3-dot button with keyboard shortcut badges. +- **Row Click → Statistics** — Clicking a table row navigates directly to the link's Statistics page. +- **A/B Split Testing** — Weighted traffic rotation in root-level links and nested targeting rules, with drag-based sliders and a one-click "Balance weights" action. +- **Variant Analytics** — Tracks which A/B variant was served and shows a "Variant Clicks Distribution" chart in the analytics dashboard. +- **Query Optimization** — Composite DB index on the visits table; queries filter by indexed `country_code`; Eloquent hydration bypassed with `toBase()->get()` for large sets. +- **GDPR IP Anonymization** — Opt-in IP masking (IPv4/IPv6) with SHA-256 hashing for unique visitor identification, managed via Settings. +- **Google Safe Browsing in REST API** — Safety checks now apply to all incoming URLs in the API (destination, A/B variants, targeting rules). +- **Settings URL Cleanup** — Tab query parameters are now human-readable (e.g. `?tab=qr` instead of `?tab=qr-defaults::data::tab`). +- **Analytics Enhancements** — QR Scan Conversion Rate card with period-over-period delta, browser/OS version subtexts, A/B click-share vs. weight comparison. + +## v3.3.0 + +- **Advanced Multi-Filter Targeting Engine** — Replaced the legacy single-strategy selection panel with a highly flexible rule engine supporting custom `AND` / `OR` logic matching. +- **Granular Filter Categories** — Added support for filtering by devices (Desktop, Mobile, Tablet), platform operating systems (Windows, macOS, Linux, iOS, Android, Fire OS), countries (multiselect with search), and browser languages (multiselect with search). +- **Client-Side Flag Icons** — Integrated high-quality country flag icons dynamically loaded from a trusted CDN (`flagcdn.com`) inside both the Filament form targeting dropdown and the analytics country statistics widget. +- **Redirection Performance Boost** — Bypassed full user agent parsing (which involves regular expression scanning for versions) during redirections by introducing specialized fast-path `getDeviceType` and `getOs` helper functions. +- **Dynamic Legacy Data Adapter** — Added automatic on-the-fly hydration and upgrade of legacy database records to the new rule format when loaded in the Filament form. + +## v3.2.0 + +- **Expanded Developer REST API** — Added new endpoints to inspect single short URLs (`GET /api/short-url/links/{idOrKey}`), fetch real-time click metrics (`GET /api/short-url/links/{idOrKey}/stats`), and fully update links programmatically (`PUT/PATCH /api/short-url/links/{idOrKey}`). Enabled flexible lookup dynamically by ID or URL key. +- **REST API Throttling & Rate Limiting** — Added route middleware throttling for the Developer REST API. Current default: `throttle:120,1` (configurable via `filament-short-url.middleware`); per-key limits default to 60 req/min. +- **Unified Validation System** — Cleaned up and unified API and admin panel form validation, supporting advanced fields like granular logging parameters (`track_visits`, `track_browser_language`, etc.), custom GA4 tracking IDs (`ga_tracking_id`), and auto-open deep linking options (`auto_open_app_mobile`). +- **Media Controller Separation** — Refactored internal logo uploads and serving routes out of the public REST API controller into a dedicated `ShortUrlLogoController` (Single Responsibility compliance). +- **Alpine.js Webhook Payload Preview** — Replaced the Filament package CodeEditor component with a custom, high-reliability dark-mode HTML/CSS component featuring live code highlighting and copy-to-clipboard functionality powered by Alpine.js. + +## v3.1.0 + +- **Database-Backed & Cached Settings** — Relocated user configuration from local JSON files to the database (`short_url_settings` table) with automatic caching and zero-downtime migration of legacy settings. +- **High-Performance Aggregations** — Completely refactored the statistics aggregator command to run optimized GROUP BY queries directly in the database, reducing memory usage (OOM protection) to near zero. +- **Secure Hashed API Keys** — API keys are now stored securely as SHA-256 hashes, verified using constant-time comparisons (`hash_equals()`), and displayed only once upon generation. +- **HMAC Signed Webhooks** — Webhook payloads are now optionally signed with a configured secret key and verified via the `X-ShortUrl-Signature` header. +- **Privacy-Safe GA4 Client IDs** — Replaced random UUIDs with deterministic client IDs based on hashed visitor IP and User Agent, ensuring session integrity in Google Analytics without storing raw visitor data. +- **Browser Cache Prevention for Limited Links** — Force temporary `302` redirects automatically for single-use, max-visit, or expiring links to prevent browsers from caching redirects and bypassing tracking logic. +- **Proxy Detection Optimization** — Implemented an aggressive 800ms timeout for external proxy checkers and reduced cache time for transient rate-limit failures to 60 seconds. + +## v3.0.0 + +- **Native App Linking (Mobile Auto-Open)** — Automatically match and redirect mobile visitors directly inside 24+ native mobile apps (such as WhatsApp, YouTube, TikTok, Instagram, Spotify, etc.) using custom schemes, complete with a glassmorphic redirect page and a live interactive matching preview widget. +- **Global Deep Linking (Universal Links & App Links)** — Easily serve iOS `apple-app-site-association` and Android `.well-known/assetlinks.json` configuration files directly from your root domain to support OS-level native integration (disabled by default, managed via Settings). +- **Central Retargeting Pixel Registry** — Introduced a premium Many-to-Many pixel management registry. Define pixels centrally (Meta Pixel, Google Tag, LinkedIn Insight, TikTok Pixel, Pinterest Tag) and easily associate them with short links via the Filament panel or the REST API. +- **Standalone Settings Page** — Relocated the Settings interface from a resource header sub-action to a standalone sidebar navigation page under the default plugin group. +- **Retargeting Pixel API** — The REST API now fully exposes the `pixels` relationship array parameter for registering and linking retargeting pixels programmatically. +- **Enhanced Browser Language Redirection** — Robust double-pass language targeting logic matching exact locales first (e.g. `en-US`, `zh-CN`) and falling back to base language codes (e.g. `en`, `zh`). +- **Custom Branded Expiry Pages** — Replaced raw 410 HTTP errors with a beautiful, fully localized, dark-mode compatible HTML expiry page displaying the Site Name, expired link details, and a homepage button. + +## v2.0.0 + +- **Interactive QR Code Designer Branding Logo** — Upload custom brand logos inside the QR designer canvas in Filament. Configure logo sizing, margins, shapes (square/circle), and toggle dot backing removal to prevent dots overlapping with the logo. +- **Dedicated QR Code Scan Tracking** — Differentiates visitor clicks from physical QR code scans by dynamically appending source tags (`?source=qr`). Added a new database tracking column (`is_qr_scan` on visits, `qr_scans` on short URLs, and `qr_visits_count` on daily stats). Displays a dedicated scans counter badge in the Filament list table. +- **Browser Language Detection & Statistics** — Captures visitor browser preferred language headers (`browser_language` field) and aggregates them into the daily stats table. Displays a new "Top Languages" widget breakdown in the link statistics dashboard. +- **High-Traffic Performance Safeguards & Robust Rollbacks** — Atomic database transactions for buffered counter updates with fail-safe rollback that restores cache values in case of DB connection failures. Prevents N+1 queries by preloading request-wide counters in a single batch cache lookup. +- **Support for Empty Route Prefix (Root-Level URLs)** — Enhanced `getShortUrl()` to support empty route prefixes without generating double slashes (e.g. `domain.com/abc123` instead of `domain.com//abc123`). + +## v1.7.0 + +- **Role-based Settings Access Control** — New `authorizeSettingsUsing(Closure)` method on the plugin to restrict who can access the Settings page. Supports any callable returning a `bool`. Also auto-detects a `manageSettings` method on a registered `ShortUrl` policy. The Settings button in the table header is hidden automatically when access is denied. + +## v1.6.0 + +- **Google Safe Browsing Integration** — Automatic safety checks against Google's API during link creation or modification. Includes bypass settings, asynchronous checking option, and alert badges. +- **VPN / Proxy / Bot Filtering** — Detect and filter out VPN/proxy traffic and Tor nodes using external proxy detection APIs to keep traffic analytics clean. +- **Visitor World Map Widget** — Live interactive SVG world map showing clicks distribution per country, custom intensity highlighting, and hover details. + +## v1.5.1 + +- **REST API On/Off Toggle** — Enable or disable the entire developer REST API from Settings → API & Webhooks without touching code. Returns `503 Service Unavailable` when disabled. + +## v1.5.0 + +- **Social Retargeting Pixels** — Attach Meta Pixel, Google Tag (GA4/Ads), and LinkedIn Insight Tag to any short URL. A premium glassmorphic interstitial page executes pixel scripts in the visitor's browser before forwarding them to the destination. Enables building remarketing audiences even on external domains. +- **Developer REST API** — Full `GET /api/short-url/links`, `POST /api/short-url/links`, and `DELETE /api/short-url/links/{id}` endpoints with API Key authentication (managed via Settings UI). Supports creating links with all available attributes including pixels and webhooks. +- **Webhook System** — Real-time HTTP POST notifications on `visited`, `created`, `expired`, and `limit_reached` events. Configurable per-link or globally. Dispatched asynchronously via `SendWebhookJob` with 3-attempt retry policy and 10-second backoff. +- **Settings: API & Webhooks Tab** — New settings tab to manage global webhook URL, monitored event types, and developer API key management with name labels and per-key activation toggles. + +## v1.4.0 + +- **Validity Date Ranges (From-To)** — Set activation dates (`activated_at` and `expires_at`) to control exactly when a short link is active. +- **Custom Visit Limit Counters** — Define a custom maximum visit limit (`max_visits`) after which a link automatically expires (e.g., active for 3 hits). +- **Custom Expiration Fallbacks** — Redirect expired/inactive visitors to a custom `expiration_redirect_url` rather than showing a static 410 Gone error page. +- **Fluent Builder APIs** — Fluent method additions (`activatedAt()`, `deactivatedAt()`, `maxVisits()`, `expirationRedirectUrl()`) in the developer query builder. + +## v1.3.0 + +- **Automatic Scheduler Registration** — Zero-configuration task registration within the ServiceProvider booted phase (dynamically honors Settings toggles). +- **Interactive Settings Validators** — Adds real-time "Test connection" verify action for GA4 Measurement Protocol and "Verify file" action for MaxMind database paths. +- **Robust Table Row Copy Action** — High-reliability, conflict-free click-to-copy in table rows with built-in fallback helper for non-HTTPS (secure context) browser environments. + +## v1.2.0 + +- **Password-protected links** — Session-based unlock flow with a styled prompt page. +- **Redirect warning pages** — Interstitial security screen before external redirects. +- **Smart targeting** — Device-based, Country/Geo-based, and A/B weighted rotation rules per link. +- **Rate limiting** — Configurable per-IP redirect throttling with 429 responses. +- **Daily stats aggregation** — Nightly `short-url:aggregate-and-prune` command for scalable log management. +- **Extended Settings GUI** — New "Performance & Security" tab for aggregation and rate limiting configuration. +- **Polish translations** — Full `pl` locale support for all new features. diff --git a/LICENSE b/LICENSE index e2ff6e8..1a01fd9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ ================================================================================ - SOURCE-AVAILABLE & DUAL-COMMERCIAL LICENSE — VERSION 1.1 + SOURCE-AVAILABLE & DUAL-COMMERCIAL LICENSE — VERSION 1.3 janczakb/filament-short-url (the "Software") ================================================================================ @@ -11,6 +11,11 @@ SOFTWARE. BY TAKING ANY OF THOSE ACTIONS, YOU UNCONDITIONALLY ACCEPT EVERY TERM AND CONDITION OF THIS AGREEMENT. IF YOU DO NOT ACCEPT, DO NOT DOWNLOAD, INSTALL, OR USE THE SOFTWARE IN ANY WAY. +This License is intended for professional and business use (B2B). You represent +that You are acquiring the Software for professional purposes and not as a +consumer acting outside Your trade, business, craft, or profession, to the +extent such qualification is relevant under applicable law. + ================================================================================ SECTION 1 — DEFINITIONS ================================================================================ @@ -21,27 +26,73 @@ SECTION 1 — DEFINITIONS documentation, and any future updates, patches, or versions thereof. 1.2 "Copyright Holder" means Bartłomiej Janczak, sole and exclusive author - and owner of all intellectual property rights in and to the Software. + and owner of all intellectual property rights in and to the Software, + except for third-party components governed by Section 6.4. 1.3 "You" (or "Your") means the individual or legal entity exercising rights under this License. -1.4 "Permitted Free Use" means: +1.4 "Official Distribution Channels" means distribution points operated or + expressly authorized in writing by the Copyright Holder for distributing + the Software to licensees, including without limitation: + (a) the official GitHub repository maintained by the Copyright Holder; + (b) the janczakb/filament-short-url package listing on Packagist.org; + (c) any mirror, release artifact, or update channel explicitly linked + from the official repository or Copyright Holder website. + Receiving a copy through an Official Distribution Channel at the Copyright + Holder's direction is not "redistribution" by You. + +1.5 "Software Transfer" means, with respect to the Software or any portion + thereof (including unmodified vendor copies, source trees, archives, + Composer packages, Docker images, or deployment artifacts from which the + Software can be extracted): + (a) delivering, disclosing, publishing, or making accessible such copies + to any third party; + (b) granting any third party the right or practical ability to copy, + redistribute, sublicense, republish, or further transfer the Software; + (c) placing the Software in a repository, package index, shared drive, + client portal, or artifact store where the client or any third party + may obtain standalone copies independent of Your controlled runtime; + (d) licensing or selling access to the Software itself, rather than to + services or outcomes produced using the Software under this License. + For the avoidance of doubt, Software Transfer does NOT include: + (i) operating the Software on infrastructure controlled by You or Your + client where the client receives no standalone copy and no rights + to extract, republish, or sublicense the Software; + (ii) delivering an integrated application to a client under Section + 1.7(b) when the client receives only runtime access and no rights + described in subsections (a) through (d) above. + +1.6 "Distinct Client Engagement" means one separately contracted scope of work + for one end client identified in writing before deployment begins. Multiple + projects, environments, brands, or business units of the same client + constitute separate engagements unless expressly covered by one written + statement of work. + +1.7 "Permitted Free Use" means use that satisfies ALL of Sections 1.7(a) or + (b), Section 1.12 (High-Volume Use limit), and every restriction in + Section 3: (a) Personal, educational, research, or internal business use by a single natural person or a single legal entity solely for that entity's own - internal operations; - (b) Deployment of the Software for a single, uniquely identified client as - a work-for-hire or freelance project, provided that: - (i) the client does not receive, resell, or otherwise transfer the - Software or any portion thereof to any third party; - (ii) the deployment is limited to one (1) production environment per - distinct client engagement; and - (iii) the monthly tracked-click/redirect volume of the resulting - system does not exceed 1,000,000 (one million) events per - calendar month across all deployed instances attributable to - that engagement. + internal operations, including staging and non-production environments + controlled by that same entity; + (b) Deployment of the Software for one Distinct Client Engagement as a + work-for-hire or freelance project, provided that: + (i) You remain the sole licensee and operator responsible for + compliance with this License; + (ii) the engagement uses no more than one (1) production environment + for that client unless a Commercial License expressly authorizes + more; + (iii) You do not perform any Software Transfer to the client or any + third party, except that the client may receive compiled or + integrated application access without standalone Software copies + or redistribution rights as described in Section 1.5; + (iv) the client does not resell, license, distribute, or operate the + Software as a product for third parties; + (v) Qualified Events attributable to that engagement do not exceed + the High-Volume Use threshold in Section 1.12. -1.5 "Commercial Distribution" means any act of selling, renting, leasing, +1.8 "Commercial Distribution" means any act of selling, renting, leasing, sublicensing, or otherwise transferring — for monetary gain, in-kind consideration, or any other commercial benefit — the Software, or any Derivative Work, or any Bundled Product, to one or more third parties. @@ -55,30 +106,41 @@ SECTION 1 — DEFINITIONS which the Software's functionality is a material component; (c) providing the Software to a client under a contract where the client subsequently resells, licenses, or distributes the resulting product - to end users. + to end users; + (d) performing any Software Transfer for consideration or as part of a + commercial deliverable. -1.6 "Derivative Work" means any work that is based upon, incorporates, +1.9 "Derivative Work" means any work that is based upon, incorporates, modifies, adapts, translates, or is otherwise derived from the Software or any substantial part thereof, regardless of the programming language, file format, or packaging used. -1.7 "Bundled Product" means any software application, plugin, extension, +1.10 "Bundled Product" means any software application, plugin, extension, theme, template, framework, boilerplate, starter kit, digital product, or service offering in which the Software — in whole or in any part, modified or unmodified — is included, embedded, required as a dependency, or integrated in any manner, irrespective of whether the Software constitutes the primary component or a minor ancillary element. -1.8 "High-Volume Use" means any deployment — whether by a single entity or - across multiple instances — that generates more than 1,000,000 (one - million) combined tracked clicks, redirects, or link events per calendar - month attributable to the Software's functionality. +1.11 "Qualified Event" means each HTTP redirect response served through the + Software's redirect routes and each visit or click recorded by the + Software's built-in tracking, counters, analytics, or daily-stats + subsystems, whether or not visit logging is enabled for a given link. + Automated health checks, load tests, and crawler traffic that the Software + explicitly excludes from counters in its own code are excluded. -1.9 "Repository" means any version-control hosting service or platform, +1.12 "High-Volume Use" means any deployment — whether internal, client, + single-instance, or multi-instance — that generates more than 1,000,000 + (one million) Qualified Events in any calendar month attributable to + deployments operated under Your license. Attribution includes all + environments (production, staging, preview, and replicas) that You + control or cause to be operated for the same entity or engagement. + +1.13 "Repository" means any version-control hosting service or platform, including without limitation GitHub, GitLab, Bitbucket, Codeberg, or any self-hosted equivalent, whether public, private, or restricted. -1.10 "Package Registry" means any software distribution index, including +1.14 "Package Registry" means any software distribution index, including without limitation Packagist, npm, PyPI, or any equivalent service. ================================================================================ @@ -89,16 +151,23 @@ Subject strictly to the conditions and restrictions in Sections 3 and 4, the Copyright Holder grants You a worldwide, royalty-free, non-exclusive, non-sublicensable, non-transferable license to: -2.1 Download, install, and use the Software solely for Permitted Free Use as - defined in Section 1.4. +2.1 Obtain copies of the Software only through Official Distribution Channels + or other channels expressly authorized in writing by the Copyright Holder, + and install and use the Software solely for Permitted Free Use as defined + in Section 1.7. Merely downloading or installing a copy from an Official + Distribution Channel does not constitute redistribution by You. 2.2 Modify the Software solely for Your own private or internal use under - Section 1.4(a) or (b), provided that all such modifications: + Section 1.7(a) or (b), provided that all such modifications: (a) are kept strictly private and are never distributed, published, shared, disclosed, or made accessible to any third party in any form; (b) retain this entire License and all copyright notices without alteration. +2.3 Make a reasonable number of backup copies and deployment artifacts + strictly necessary to operate Permitted Free Use, provided such copies + remain under Your control and are not subject to Software Transfer. + No rights are granted except those expressly set out in this Section 2. All other rights are reserved exclusively by the Copyright Holder. @@ -107,31 +176,38 @@ SECTION 3 — STRICT RESTRICTIONS AND PROHIBITIONS ================================================================================ The following acts are CATEGORICALLY AND UNCONDITIONALLY PROHIBITED unless -You hold a valid, current, written Commercial License issued by the Copyright -Holder under Section 5: +You hold a valid, current Commercial License issued by the Copyright Holder +under Section 5: 3.1 REDISTRIBUTION IN ANY FORM. You may not publish, upload, transmit, broadcast, mirror, host, or otherwise make available the Software — in whole or in any part, modified or unmodified — through any means, including but not limited to: - (a) any public or private Repository, regardless of access controls; - (b) any Package Registry; + (a) any public or private Repository, regardless of access controls, + except as expressly permitted in Section 3.1(A); + (b) any Package Registry, except Official Distribution Channels operated + by the Copyright Holder; (c) file-sharing platforms, cloud storage, or peer-to-peer networks; (d) email, instant messaging, or any other communication channel. - SOLE EXCEPTION — CONTRIBUTION FORKS: A single public fork of the - official repository on GitHub is permitted exclusively to submit a Pull - Request to the original repository. Such a fork: - (i) must be created solely for the purpose of the specific contribution; - (ii) must not be used for any other purpose, including personal hosting - or showcase; + (A) CONTRIBUTION FORKS — SOLE EXCEPTION. Public fork(s) of the official + repository on GitHub are permitted exclusively to prepare and submit Pull + Request(s) to the original repository. Each such fork: + (i) must be created and maintained solely for contribution work; + (ii) must not be used for hosting, demonstration, resale, or any + independent product; (iii) must reproduce this License in full without modification; (iv) must be deleted or converted to private within thirty (30) calendar - days of the earlier of: (A) the Pull Request being closed, merged, - or rejected; or (B) ninety (90) calendar days of inactivity. Failure - to do so constitutes a material breach of this License. + days of the earlier of: (A) all related Pull Request(s) being + closed, merged, or rejected; or (B) ninety (90) calendar days of + inactivity on that fork. Failure to comply constitutes a material + breach of this License. -3.2 BUNDLING AND COMMERCIAL EXPLOITATION. You may not: +3.2 SOFTWARE TRANSFER. You may not perform any Software Transfer except as + expressly allowed without transfer in Section 1.5(i)–(ii) and Section + 1.7(b)(iii). + +3.3 BUNDLING AND COMMERCIAL EXPLOITATION. You may not: (a) include, embed, bundle, or integrate the Software, in whole or in any part, into any Bundled Product that is or will be subject to Commercial Distribution; @@ -144,30 +220,38 @@ Holder under Section 5: end users, whether individually priced or as part of a bundle or subscription. -3.3 COMMERCIAL DISTRIBUTION. You may not engage in any act constituting - Commercial Distribution as defined in Section 1.5. +3.4 COMMERCIAL DISTRIBUTION. You may not engage in any act constituting + Commercial Distribution as defined in Section 1.8. -3.4 HIGH-VOLUME USE without a Commercial License. You may not operate any +3.5 HIGH-VOLUME USE without a Commercial License. You may not operate any system or service that constitutes High-Volume Use as defined in - Section 1.8. + Section 1.12, including internal business use under Section 1.7(a). -3.5 RELABELING AND MISATTRIBUTION. You may not: +3.6 RELABELING AND MISATTRIBUTION. You may not: (a) publish, distribute, or otherwise make available the Software under a different package name, author name, or vendor identity; - (b) remove, obscure, alter, or misrepresent the copyright notice or author - attribution in any copy or portion of the Software; + (b) remove, obscure, alter, or misrepresent the copyright notice, license + text, or author attribution in any copy or portion of the Software; (c) represent, imply, or permit others to believe that the Software was created by anyone other than Bartłomiej Janczak. -3.6 SUBLICENSING AND TRANSFER. You may not grant any sublicense, assign any - rights, or otherwise transfer this License or any rights granted hereunder - to any third party. +3.7 SUBLICENSING AND TRANSFER OF LICENSE RIGHTS. You may not grant any + sublicense, assign any rights, or otherwise transfer this License or any + rights granted hereunder to any third party. Hosting the Software for a + client under Section 1.7(b) does not sublicense rights to the client. -3.7 CIRCUMVENTION. You may not structure any transaction, arrangement, or +3.8 CIRCUMVENTION. You may not structure any transaction, arrangement, or technical implementation with the intent or effect of circumventing any restriction in this Section 3, including without limitation by splitting a - Bundled Product into artificially separate components or by using an - intermediary entity. + Bundled Product into artificially separate components, using an + intermediary entity, masking the Software as a different dependency, + or separating repositories solely to evade transfer or redistribution + prohibitions. + +3.9 COMPETING PRODUCTS. You may not use the Software, or any Derivative Work, + as the core of a substantially similar link-management, URL-shortening, + QR-code, or redirect-analytics product offered to third parties, whether + open or closed source, except under a Commercial License. ================================================================================ SECTION 4 — CONTRIBUTOR LICENSE AGREEMENT (CLA) @@ -182,12 +266,14 @@ SECTION 4 — CONTRIBUTOR LICENSE AGREEMENT (CLA) terms the Copyright Holder deems appropriate, including proprietary terms. 4.2 You represent and warrant that: - (a) You are the sole author of the contribution and have the full legal - right to grant the license in Section 4.1; + (a) You are the sole author of the contribution or have obtained all + rights necessary to grant the license in Section 4.1; (b) the contribution does not infringe any third-party intellectual property rights; (c) the contribution is not subject to any license, agreement, or - obligation that would conflict with the grant in Section 4.1. + obligation that would conflict with the grant in Section 4.1; + (d) if the contribution includes work created for an employer or client, + You have authority to submit it under Section 4.1. 4.3 The Copyright Holder retains the right to reject any contribution without giving reasons and without incurring any obligation to the contributor. @@ -197,10 +283,11 @@ SECTION 5 — COMMERCIAL LICENSING ================================================================================ 5.1 Any use not expressly permitted by Section 2 — including Commercial - Distribution, Bundled Product distribution, High-Volume Use, or any - other commercial exploitation of the Software — requires a separate, - written Commercial License issued by the Copyright Holder prior to such - use. + Distribution, Software Transfer beyond Section 1.5(i)–(ii), Bundled + Product distribution, High-Volume Use, competing products under + Section 3.9, or any other commercial exploitation of the Software — + requires a separate Commercial License issued by the Copyright Holder + prior to such use. 5.2 To inquire about or obtain a Commercial License, contact: @@ -209,11 +296,15 @@ SECTION 5 — COMMERCIAL LICENSING 5.3 Commercial Licenses are issued at the Copyright Holder's sole discretion and may be subject to a one-time fee, recurring royalty, revenue-sharing - arrangement, or other terms as agreed in writing. No Commercial License - is effective until signed in writing by the Copyright Holder. + arrangement, or other terms as agreed in a written instrument signed by + the Copyright Holder, including a qualified electronic signature or other + form of document with legal effect equivalent to writing under applicable + law. No Commercial License is effective until so signed or otherwise + authenticated by the Copyright Holder. -5.4 Any act of Commercial Distribution, Bundled Product distribution, or - High-Volume Use without a valid Commercial License: +5.4 Any act of Commercial Distribution, prohibited Software Transfer, + Bundled Product distribution, High-Volume Use, or competing use under + Section 3.9 without a valid Commercial License: (a) is null and void ab initio; (b) constitutes a material breach of this License; (c) constitutes direct infringement of the Copyright Holder's exclusive @@ -232,7 +323,7 @@ SECTION 6 — INTELLECTUAL PROPERTY 6.1 The Software is licensed, not sold. Bartłomiej Janczak retains all right, title, and interest in and to the Software, including all copyrights, moral rights, trade secrets, and any other intellectual property rights, - worldwide. + worldwide, except as expressly granted in this License. 6.2 This License does not transfer to You any ownership interest in or to the Software, any trademarks, trade names, or service marks of the Copyright @@ -250,6 +341,11 @@ SECTION 6 — INTELLECTUAL PROPERTY janczakb/filament-short-url package and does not modify or supersede the terms of any third-party license. +6.5 PATENTS. No patent rights are granted except to the minimum extent + necessary for You to exercise the copyright permissions expressly granted + in Section 2 during Permitted Free Use or under a valid Commercial + License. All other patent rights are reserved. + ================================================================================ SECTION 7 — TERMINATION ================================================================================ @@ -262,7 +358,8 @@ SECTION 7 — TERMINATION (a) immediately cease all use, reproduction, and distribution of the Software; (b) permanently delete all copies, full or partial, of the Software in - Your possession or control, including backups and archived copies; + Your possession or control, including backups, deployment artifacts, + and archived copies; (c) upon request, certify in writing to the Copyright Holder that You have complied with Section 7.2(b). @@ -277,20 +374,58 @@ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, AND NON-INFRINGEMENT. THE COPYRIGHT HOLDER DOES NOT WARRANT THAT THE SOFTWARE WILL MEET YOUR REQUIREMENTS, OPERATE WITHOUT INTERRUPTION, OR BE FREE FROM ERRORS OR SECURITY VULNERABILITIES. -YOU ASSUME ALL RISKS ASSOCIATED WITH YOUR USE OF THE SOFTWARE. + +YOU ASSUME THE ENTIRE RISK AS TO THE QUALITY, PERFORMANCE, SECURITY, AND +RESULTS OF YOUR USE OF THE SOFTWARE. YOU ARE SOLELY RESPONSIBLE FOR ANY DAMAGE +TO YOUR SYSTEMS, DATA, BUSINESS, OR THIRD PARTIES ARISING FROM YOUR USE OR +INABILITY TO USE THE SOFTWARE. ================================================================================ -SECTION 9 — LIMITATION OF LIABILITY +SECTION 9 — LIMITATION OF LIABILITY (LICENSEE CLAIMS AGAINST COPYRIGHT HOLDER) ================================================================================ -TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, -CONSEQUENTIAL, OR PUNITIVE DAMAGES (INCLUDING BUT NOT LIMITED TO LOSS OF -PROFITS, LOSS OF DATA, BUSINESS INTERRUPTION, OR COST OF SUBSTITUTE GOODS OR -SERVICES) ARISING OUT OF OR IN CONNECTION WITH THIS LICENSE OR THE USE OR -INABILITY TO USE THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH -DAMAGES. THE COPYRIGHT HOLDER'S TOTAL LIABILITY FOR ANY CLAIM ARISING UNDER -THIS LICENSE SHALL NOT EXCEED EUR 100 (ONE HUNDRED EURO). +This Section limits claims that a Licensee (or any user acting under this +License) may bring AGAINST the Copyright Holder. It does NOT limit, reduce, +or cap any remedy, claim, or enforcement action that the Copyright Holder may +bring against You for breach of this License, copyright infringement, or +unauthorized use, including without limitation Sections 5.4, 7, and 10.8. + +9.1 EXCLUSION OF DAMAGES. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, + THE COPYRIGHT HOLDER SHALL NOT BE LIABLE TO YOU FOR ANY DAMAGES OF ANY + KIND OR NATURE, WHETHER DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + CONSEQUENTIAL, OR PUNITIVE, INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, + REVENUE, GOODWILL, DATA, BUSINESS, OR OPPORTUNITY, ARISING OUT OF OR IN + CONNECTION WITH THIS LICENSE OR THE SOFTWARE, EVEN IF THE COPYRIGHT + HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +9.2 ZERO LIABILITY FOR PROFESSIONAL USE. WHERE PERMITTED FOR PROFESSIONAL OR + BUSINESS USE (B2B), THE COPYRIGHT HOLDER SHALL HAVE NO LIABILITY WHATSOEVER + TO YOU UNDER OR IN CONNECTION WITH THIS LICENSE. + +9.3 MANDATORY-LAW FLOOR. IF A COMPETENT COURT FINALLY DETERMINES THAT ANY + PART OF SECTIONS 9.1 OR 9.2 CANNOT BE ENFORCED FOR A PARTICULAR CLAIM, + THE COPYRIGHT HOLDER'S TOTAL AGGREGATE LIABILITY FOR THAT CLAIM AND ALL + RELATED CLAIMS ARISING UNDER THIS LICENSE SHALL NOT EXCEED THE GREATER OF: + (a) EUR 0 (ZERO EURO); OR + (b) THE FEES ACTUALLY PAID BY YOU TO THE COPYRIGHT HOLDER FOR A VALID + COMMERCIAL LICENSE FOR THE SOFTWARE IN THE TWELVE (12) MONTHS + IMMEDIATELY PRECEDING THE EVENT GIVING RISE TO THE CLAIM; + AND IN ALL CASES ONLY TO THE MINIMUM EXTENT REQUIRED BY MANDATORY + APPLICABLE LAW. NOTHING IN THIS SECTION 9.3 EXPANDS LIABILITY BEYOND + WHAT APPLICABLE LAW REQUIRES. + +9.4 EXCLUSIVE REMEDY. YOUR SOLE AND EXCLUSIVE REMEDY FOR DISSATISFACTION WITH + THE SOFTWARE OR ANY BREACH BY THE COPYRIGHT HOLDER OF THIS LICENSE IS TO + DISCONTINUE USE OF THE SOFTWARE AND DELETE ALL COPIES IN YOUR POSSESSION + OR CONTROL. + +9.5 NO LIMITATION ON COPYRIGHT HOLDER ENFORCEMENT. For clarity: if You breach + this License, the Copyright Holder may pursue all remedies available under + this License and applicable law without regard to this Section 9. A person + who infringes or misuses the Software is not a beneficiary of this + limitation and may owe the Copyright Holder damages, injunctive relief, + disgorgement of profits, and other remedies far exceeding any amount + referenced in Section 9.3. ================================================================================ SECTION 10 — GENERAL PROVISIONS @@ -324,11 +459,18 @@ SECTION 10 — GENERAL PROVISIONS publish revised versions of this License. Each version will be identified by a version number. The version under which You obtained the Software governs Your use unless the Copyright Holder expressly states otherwise. - This is version 1.1, effective 1 January 2026. + This is version 1.3, effective 8 June 2026. Version 1.3 supersedes + version 1.2 for copies obtained on or after its effective date. 10.7 LANGUAGE. This License is written in English. Any translation is provided for convenience only; in the event of a conflict the English text prevails. +10.8 AUDIT. Upon reasonable prior written notice, the Copyright Holder may + request written confirmation that Your use remains within Permitted Free + Use, including monthly Qualified Event volumes. You must respond within + thirty (30) calendar days with good-faith information. Refusal to respond + or material misrepresentation constitutes a breach of this License. + ================================================================================ END OF LICENSE -================================================================================ \ No newline at end of file +================================================================================ diff --git a/README.md b/README.md index 74e5dea..f0b1ade 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

Filament Short URL

-

The most complete open-source link management plugin for Laravel & Filament.
Short URLs · QR codes · Analytics · Targeting · Deep links · Webhooks · REST API

+

The most complete self-hosted link management plugin for Laravel & Filament.
Short URLs · QR codes · Analytics · SEO & OG Meta · Link Cloaking · Targeting · Deep links · Webhooks · REST API

Latest Version on Packagist @@ -17,71 +17,80 @@ A self-hosted **URL shortener, redirect engine, and QR code manager** built as a [Filament v5](https://filamentphp.com) plugin. Drop it into any Laravel 11+ application to replace Bitly, Dub.co, or Rebrandly subscriptions — with full data ownership, no click limits, and no monthly fees. -On top of basic link shortening it ships: multi-channel analytics with live activity feed, cross-filtering, and cross-domain retargeting pixels; advanced routing rules (device, country, language, A/B); mobile deep linking into 24+ native apps; a REST API with scoped keys; HMAC-signed webhooks; and offline GDPR-safe Geo-IP — all managed from a polished Filament admin panel. +On top of basic link shortening it ships: per-link **SEO & Open Graph meta**, **Search Engine Indexing**, and **Link Cloaking**; multi-channel analytics with live activity feed, cross-filtering, and cross-domain retargeting pixels; advanced routing rules (device, country, language, A/B); mobile deep linking into 24+ native apps; a REST API with scoped keys; HMAC-signed webhooks; and offline GDPR-safe Geo-IP — all managed from a polished Filament admin panel. --- ## Screenshots + + + + + + + + + + + + +
Analytics dashboardShort URL creation form
Smart targeting rulesSettings panel
Visit logs table
-

- Analytics dashboard - Short URL creation form -

-

- Smart targeting rules - Settings panel -

-

- Visit logs table -

--- ## Features - 🔗 **Short Link Generation** — Auto-generate collision-free Base62 keys or set your own custom slugs. -- 🌐 **Custom Domain Branding** — Register your own domains, verify DNS in real-time (A/CNAME), and serve links directly from the domain root — no `/s/` prefix needed. -- 🌍 **Multiple Geo-IP Drivers** — Resolve visitor countries offline via MaxMind, from CDN headers (Cloudflare `CF-IPCountry`, CloudFront), or via ip-api.com fallback. +- 🌐 **Custom Domain Branding** — Register your own domains, verify DNS in real-time (multi A-record + CNAME matching), enforce verification before activation, and serve links directly from the domain root — no `/s/` prefix needed. +- 🔎 **SEO & Open Graph Meta** — Per-link `og:title`, `og:description`, and `og:image` with live social preview in Filament. Auto-scrape metadata from the destination URL, import OG images to storage, and serve bot-safe preview pages to Facebook, LinkedIn, X, Slack, and other crawlers. +- 🎭 **Link Cloaking** — Mask the destination URL in the browser address bar by embedding the target in an iframe on your short-link domain. Social bots still receive your custom OG tags; human visitors stay on your branded URL. +- 🚫 **Search Engine Indexing Control** — Per-link toggle to allow or block search engine indexing. When disabled, every redirect and interstitial response sends `X-Robots-Tag: noindex, nofollow` plus matching `` tags. +- 🌍 **Multiple Geo-IP Drivers** — Resolve visitor countries from CDN headers (Cloudflare, CloudFront), offline MaxMind, or ip-api.com. The **Headers** driver auto-enables CDN trust on save and falls back to MaxMind → ip-api when edge headers are missing (ip-api rate-limited to protect the free tier). - 🗺️ **Visitor World Map** — Visualize click distribution on an interactive SVG world map with per-country hover stats. - 📈 **Full Analytics Dashboard** — Track total/unique visits, referrers, devices, browsers, OSes, and browser languages. Fully cross-filterable (e.g., "only mobile visits from Poland in May"). -- ⚡ **Live Activity Feed** — Standalone real-time visit stream at `/stats/live`. Uses `MAX(id)` polling — returns a ~100-byte response when nothing changed, full render only on new visits. +- ⚡ **Live Activity Feed** — Real-time visit stream on each link's stats page (`/{record}/stats/live`). Browser uses **SSE + `EventSource`** (not Livewire `wire:poll`). The server **auto-detects** your cache stack and picks the fastest transport — see [Live Activity Feed](#live-activity-feed-sse). +- 🚀 **Production Redis mode** — Set **Queue Connection = `redis`** in Settings for async visit jobs plus dedicated Redis counters, today/hourly stats, and live feed — independent of `CACHE_STORE`. Configure Redis host/port/password in the panel; **Test Redis** and **Test queue worker** buttons validate before save. - 🗂️ **Folders & Tags** — Assign each link to a folder and up to 5 tags. Click any folder or tag to navigate to a filtered link list. - 🗄️ **Link Archiving** — Soft-archive links instead of deleting them. Archived links are hidden by default and fully restorable. -- 🛡️ **VPN, Proxy & Bot Filtering** — Filter out Tor nodes, VPNs, anonymous proxies, and known bot user agents from visit stats. -- 🔍 **Google Safe Browsing** — All target URLs are checked against Google's threat database on create and edit. -- 🎨 **SVG QR Code Designer** — Full dot style, gradient, margin, and logo customization. Export as SVG or high-resolution PNG. -- 📊 **QR Scan Tracking** — QR scans are recorded separately from regular web clicks via `?source=qr`. Shown as a distinct metric in analytics. +- 🛡️ **VPN, Proxy & Bot Filtering** — Optional VPN/proxy/Tor detection (flag or block) when enabled in **Settings → Advanced**. Known bot and social-preview crawlers are excluded from visit logs entirely (not logged or counted). +- 🔍 **Google Safe Browsing** — When enabled in **Settings → Advanced**, target URLs are checked against Google's threat database on create and edit (Filament panel and REST API). +- 🎨 **SVG QR Code Designer** — Full dot style, gradient, margin, and logo customization. Export as SVG or high-resolution PNG directly from the admin panel (client-side via QRCodeStyling — colors, gradients, and logo are preserved). +- 📊 **QR Scan Tracking** — QR scans are recorded separately from regular web clicks via `?source=qr` or `?qr=1`. Shown as a distinct metric in analytics. - ✈️ **Browser Language Targeting** — Route visitors to different destinations based on their browser's `Accept-Language` header. -- 🚀 **Ultra-Fast Redirects** — Sub-15ms redirect response time. Bypasses the heavy Laravel session/cookie middleware group for standard links, falling back to a stateful route dynamically only when password protection is active. -- 🎯 **Server-Side GA4** — Send `short_url_visit` events via GA4 Measurement Protocol, completely bypassing browser ad-blockers. +- 🚀 **Fast Redirects** — The main `/s/{key}` route skips Laravel's `web` middleware (no session/cookies on every click). Password-protected links use a separate stateful `/s-auth/{key}` route. Simple redirects stay lean; HTML interstitials (warning page, pixels, cloaking) add latency by design. +- 🎯 **Server-Side GA4 (Enterprise MP)** — Full GA4 Measurement Protocol payloads (`page_view`, `click`, `short_url_visit`) with `session_id`, `timestamp_micros`, and engagement time. Per-link Measurement ID validated before send; Settings **Test connection** verifies the real `G-XXXXXXXXXX` + API secret pair via the debug collector. - ⚙️ **UTM Builder** — Build and preview campaign URLs with a real-time UTM form that syncs bidirectionally with the destination URL field. -- 🔒 **Link Expiration & Caps** — Set `activated_at`, `expires_at`, `max_visits`, single-use mode, and a custom fallback URL for when any of these conditions are met. +- 🔒 **Link Expiration & Caps** — Set `activated_at`, `expires_at`, `max_visits`, and single-use mode. `expiration_redirect_url` applies only when the link is **expired or inactive** (time/schedule); visit-cap and single-use limits return **410 Gone** (or a branded expiry page), not a fallback redirect. - ➡️ **Query Parameter Forwarding** — Automatically append incoming query strings (UTM parameters, ad tokens, discount codes) to the destination. - 🛠️ **Settings Panel** — All configuration in a dedicated Filament settings page — no `.env` editing required for day-to-day changes. -- 🔑 **Password-Protected Links** — Visitors enter a password before being redirected. Session-persisted, so they only see the prompt once. -- ⚠️ **Redirect Warning Pages** — Show a confirmation screen before sending visitors off to external domains. +- 🔑 **Password-Protected Links** — Visitors enter a password before being redirected. Passwords are hashed at rest (bcrypt/argon). Session-persisted unlock — visitors only see the prompt once per session. +- 🛑 **Redirect Warning Pages** — Show a confirmation screen before sending visitors off to external domains. - 🎲 **A/B Split Testing** — Distribute traffic across 2–5 weighted variants. Works at the root level or nested inside targeting rules. +- 📊 **Public Stats Pages** — Per-link shareable analytics: HTML dashboard for visitors, JSON API for embeds, optional password — no API key required. - 📉 **Log Aggregation & Pruning** — A nightly command aggregates raw visits into daily summaries and prunes records older than the configured retention window. Keeps the database lean even at millions of visits. - 🏷️ **Retargeting Pixel Registry** — Define Meta Pixel, Google Tag, LinkedIn Insight, TikTok, and Pinterest pixels once, then attach them to any link via a simple checkbox list. -- 🔌 **REST API with Scoped Keys** — Full CRUD API, SHA-256 hashed key storage, `links:read-only` / `links:read-write` scopes, and per-key rate limits. -- 📡 **Webhooks** — HMAC-SHA256 signed HTTP POST callbacks on `visited`, `created`, `expired`, and `limit_reached` events. Dispatched asynchronously with 3-attempt exponential retry. +- 🔌 **REST API with Scoped Keys** — Full CRUD API, bulk create/update/delete, upsert by `external_id`, visit log + CSV export, tags/folders CRUD, public stats, SHA-256 hashed keys, `links:read-only` / `links:read-write` scopes, **`owner_user_id` required when `scope_links_to_user` is enabled**, and per-key rate limits. +- 📡 **Webhooks** — HMAC-SHA256 signed HTTP POST callbacks on `visited`, `created`, `expired`, and `limit_reached` events. Outbound URLs are validated against SSRF blocklists before dispatch. - 📱 **Mobile Deep Linking** — Detect mobile visitors and launch the link directly inside 24+ native apps (Instagram, YouTube, Spotify, TikTok, WhatsApp, etc.). - 🍎 **Universal Links & Android App Links** — Serve `apple-app-site-association` and `assetlinks.json` directly from your domain to enable OS-level native app integration. - 💀 **Branded Expiry Pages** — When a link expires or hits its limit, visitors see a styled page with your site name instead of a bare 410 error. - 👤 **User Attribution** — Each link records its creator. Avatar and name/email hover card visible in the table. - 🕒 **Relative Time Badges** — Compact timestamps (`2h`, `5d`, `3mo`) with exact date on hover. -- ⌨️ **Keyboard Shortcuts** — `E` edit, `Q` QR, `I` copy, `S` stats, `X` delete — available on any row hover. +- ⌨️ **Keyboard Shortcuts** — `E` edit, `Q` QR, `I` share/copy, `S` stats, `P` public stats, `X` delete — available on any row hover. - ⋯ **Unified Action Menu** — All row actions in one 3-dot dropdown with shortcut badges. --- ## vs. Bitly, Dub.co & Rebrandly +Self-hosted link management for Laravel/Filament — **not** a full Dub.co-style attribution SaaS (no workspaces, conversion tracking, or edge analytics pipeline). + | Feature | Filament Short URL | Bitly | Dub.co | Rebrandly | |---|:---:|:---:|:---:|:---:| | Self-hosted | ✅ | ❌ | partial¹ | ❌ | -| Unlimited links & clicks | ✅ | ❌ | ❌ | ❌ | +| Unlimited links & clicks² | ✅ | ❌ | ❌ | ❌ | | Custom domains | ✅ | 💰 | 💰 | 💰 | | QR code designer | ✅ | 💰 basic | 💰 basic | 💰 | | A/B split testing | ✅ | ❌ | ✅ | ❌ | @@ -89,17 +98,40 @@ On top of basic link shortening it ships: multi-channel analytics with live acti | Mobile deep linking | ✅ 24+ apps | 💰 Enterprise | 💰 partial | 💰 | | Server-side GA4 (ad-block bypass) | ✅ | ❌ | ❌ | ❌ | | Live activity feed | ✅ | ❌ | ✅ | ❌ | -| Cross-filtering analytics | ✅ | 💰 basic | partial | 💰 basic | -| REST API | ✅ scoped keys | 💰 | ✅ | 💰 | -| Webhooks | ✅ HMAC-signed | 💰 Enterprise | ✅ | 💰 | +| Cross-filtering analytics (panel) | ✅ | 💰 basic | ✅ | 💰 basic | +| REST API | ✅ scoped keys | 💰 | ✅ deep | 💰 | +| Webhooks (global event list) | ✅ HMAC-signed | 💰 Enterprise | ✅ per-workspace | 💰 | +| Workspaces / team RBAC | ❌ | ✅ | ✅ | ✅ | +| Lead & sale conversion tracking | ❌ | 💰 | ✅ core | 💰 | | Offline GDPR Geo-IP (MaxMind) | ✅ | ❌ | ❌ | ❌ | -| VPN & bot filtering | ✅ | ❌ | ❌ | ❌ | -| Google Safe Browsing on save | ✅ | ❌ | ❌ | ❌ | +| VPN & bot filtering³ | ✅ | ❌ | ❌ | ❌ | +| Google Safe Browsing³ | ✅ save + redirect | ❌ | ❌ | ❌ | +| Custom OG meta & social previews | ✅ | 💰 | ✅ | 💰 | +| Link cloaking (iframe masking) | ✅ | ❌ | ✅ | ❌ | +| Per-link search indexing control | ✅ | ❌ | partial | ❌ | | Filament/Laravel admin panel | ✅ | ❌ | ❌ | ❌ | | Data stays on your server | ✅ | ❌ | partial¹ | ❌ | | Monthly cost | **$0** | $0–$199+ | $0–$190+ | $0–$49+ | -> 💰 = paid plans only  ·  ¹ Dub.co is open-source (AGPLv3) but self-hosting requires external managed services (Tinybird, PlanetScale, Upstash) +> 💰 = paid plans only  ·  ¹ Dub.co is open-source (AGPLv3) but self-hosting expects external managed services (Tinybird, PlanetScale, Upstash)  ·  ² Throughput depends on your PHP/DB/Redis stack — not a CDN edge product  ·  ³ VPN/proxy detection and Safe Browsing are **disabled by default**; enable in **Settings → Advanced**. Per-link webhooks use a URL only; subscribed events are configured globally in Settings → Developer. + +--- + +## Architecture + +Filament-native short-link engine inside your Laravel app — **not** a Dub.co-style attribution SaaS. + +| Route | Middleware | Session | Purpose | +|-------|------------|---------|---------| +| `/s/{key}` | `throttle:120,1` only | No | Main redirect (stateless hot path) | +| `/s-auth/{key}` | `web` + throttle | Yes | Password unlock (once per session) | +| `/s/public-stats/{key}` | `web` + throttle | Yes (password unlock) | Public HTML stats page + JSON API | +| Custom domain `/{key}` | same as `/s/{key}` | No | Branded links without prefix | +| `/api/short-url/*` | API auth + throttle | No | CRUD, bulk, exists, stats | + +**Multi-tenant (single app):** `ShortUrlResource` scopes by `user_id` when `SHORT_URL_SCOPE_TO_USER=true` (default). API keys **must** include `owner_user_id` when link scoping is enabled; keys without an owner receive **403 Forbidden**. No workspaces, team RBAC, or billing. + +**Stats retention:** raw visits pruned after ~90 days (configurable). Long-range API stats use daily rollups in `short_url_daily_stats`. --- @@ -126,6 +158,114 @@ php artisan vendor:publish --tag=filament-short-url-migrations php artisan migrate ``` +### Production checklist + +| Task | Why | +|------|-----| +| `* * * * * php artisan schedule:run` | Domain verification, counter sync, visit aggregation/pruning | +| Queue worker (when async) | Default `sync` needs no worker. For **`redis`** or **`database`**, run `php artisan queue:work {connection} --queue={name}` matching **Settings → General** (not necessarily `QUEUE_CONNECTION` in `.env`). Use **Test queue worker** in Settings to verify. | +| `SHORT_URL_SCOPE_TO_USER=true` | Filament users see only their own links (default) | +| Enable Safe Browsing / VPN in Settings | Security features are **off by default** | +| Global webhook signing secret | Required when global webhook is enabled | + +Raw visit logs are pruned after the retention window (default 90 days); long-range API stats rely on daily rollups. + +### Load & stress testing + +Two complementary baselines — use both before high-traffic launches or after infra changes. + +**1. In-process (Artisan)** — measures Laravel routing + redirect handler without HTTP overhead: + +```bash +php artisan short-url:stress-redirect your-key --requests=200 --warmup=10 +``` + +Reports avg / min / p95 / max in milliseconds. Best for quick regressions in CI or after code changes. + +**2. HTTP load (k6)** — concurrent requests against a running app (Herd, staging, production-like stack): + +```bash +# Install k6: https://grafana.com/docs/k6/latest/set-up/install-k6/ +k6 run vendor/janczakb/filament-short-url/scripts/k6/redirect-baseline.js \ + -e BASE_URL=https://your-app.test \ + -e URL_KEY=bench-key \ + -e VUS=20 \ + -e DURATION=1m \ + -e P95_MS=500 +``` + +| Variable | Default | Purpose | +|----------|---------|---------| +| `BASE_URL` | *(required)* | App origin, e.g. `https://wyachts.test` | +| `URL_KEY` | *(required)* | Existing short link key | +| `ROUTE_PREFIX` | `s` | From `SHORT_URL_ROUTE_PREFIX` | +| `VUS` | `10` | Concurrent virtual users | +| `DURATION` | `30s` | Test duration | +| `P95_MS` | `500` | Fail threshold for p95 latency | +| `SLEEP_MS` | — | Optional pause between iterations | + +**Tips** + +- Create a dedicated link with `track_visits=false` for a pure redirect benchmark (no queue/DB write noise). +- Compare SQLite vs MySQL on staging — driver and connection pool matter under load. +- These tests establish **your** baseline; they are not a license throughput guarantee. + +--- + +## Upgrading + +When updating from **v5.1.x or earlier**: + +```bash +composer update janczakb/filament-short-url +php artisan migrate +``` + +### Migration safety (v5.2.0) + +Three additive migrations ship with this release. They do **not** drop, rename, or rewrite existing rows. Your links, visit history, API keys, and settings remain intact. + +| Migration | Purpose | +|---|---| +| `2026_06_08_000001_add_api_and_utm_fields_to_short_urls_table` | API/UTM/public-stats columns + visit index | +| `2026_06_09_000001_audit_schema_and_performance_fixes` | `domain_scope_id`, composite unique keys, FK, stats columns | +| `2026_06_10_000001_add_security_counts_to_daily_stats` | `all_visits_count`, `bot_visits_count`, `proxy_visits_count` on daily rollups | +| `2026_06_11_000001_add_cross_dimensional_stats_to_daily_stats` | Cross-filter JSON rollups for filtered dashboard widgets | + +#### `2026_06_08_000001` + +| Database change | Effect on existing installs | +|---|---| +| `external_id`, `utm_*`, `ref` columns | Added as **nullable** — all existing links stay `NULL` until you set values | +| `public_stats_enabled` | Added with default **`false`** — public stats stay off until enabled per link | +| `public_stats_password` | Added as **nullable** | +| Index on `short_url_visits (short_url_id, ip_hash)` | Performance only — no data change | + +#### `2026_06_09_000001` + +| Database change | Effect on existing installs | +|---|---| +| `domain_scope_id` | Added with default **`0`** (default app domain). Existing custom-domain links get `domain_scope_id = custom_domain_id`. | +| Unique `(url_key, domain_scope_id)` | Replaces global `url_key` unique — allows the same slug on different domains | +| FK `custom_domain_id` → `short_url_custom_domains` | `ON DELETE SET NULL` when a domain is removed | +| Index `(user_id, is_archived, id)` | Admin list performance | +| Index `(short_url_id, id)` on visits | Aggregation/pruning performance | +| JSON columns on `short_url_daily_stats` | `utm_terms`, `utm_contents`, `browser_versions`, `os_versions` for richer charts | + +#### `2026_06_10_000001` + +| Database change | Effect on existing installs | +|---|---| +| `all_visits_count` | Total daily visits including bots/proxies (security widget rollups) | +| `bot_visits_count` | Pre-aggregated bot traffic per day | +| `proxy_visits_count` | Pre-aggregated VPN/proxy traffic per day | + +All three migrations use no `->after()` column ordering hints and no database-specific `ENUM` types, so they run cleanly on **SQLite, MySQL, and PostgreSQL**. + +**If you publish migrations to `database/migrations/`**, make sure both new files are present after `composer update`, then run `php artisan migrate`. If you never published migrations, the package service provider registers them automatically — `migrate` is still required once per release that adds schema changes. + +**Rollback:** `php artisan migrate:rollback --step=4` removes the v5.2.0 columns and indexes (one step per migration). Existing link data created before v5.2.0 is unaffected. + --- ## Publishing Package Assets @@ -234,7 +374,7 @@ public function manageSettings(User $user): bool } ``` -Then register it in `AuthServiceProvider`: +Then register the policy in your application (e.g. `AppServiceProvider` or `bootstrap/app.php` in Laravel 11+): ```php use Bjanczak\FilamentShortUrl\Models\ShortUrl; @@ -253,10 +393,10 @@ protected $policies = [ The package comes with a built-in admin settings dashboard. It is accessible directly from your sidebar menu under the same navigation group as your links. -Settings are stored dynamically in the database (`short_url_settings` table), cached indefinitely, and immediately override config defaults. Legacy settings from `filament-short-url-settings.json` are automatically imported on first load. +Settings are stored dynamically in the database (`short_url_settings` table), cached for 3600 seconds by default, and immediately override config defaults. Legacy settings from `filament-short-url-settings.json` are automatically imported on first load. > [!NOTE] -> **Modular Tab Architecture (New in v3.5.0)**: The settings form is split into 8 independent, single-responsibility tab classes (`LinkTab`, `TargetingTab`, `TrackingTab`, `SecurityTab`, `GeoIpTab`, `VpnDetectionTab`, `QrDesignTab`, `PixelsTab`) to reduce load times, improve code maintainability, and allow clean tab query parameters navigation (e.g. `?tab=qr`). +> **Modular Tab Architecture**: The **Settings** page is split into independent tab classes (`GeneralTab`, `GeoIpTab`, `Ga4Tab`, `AdvancedTab`, `TrackingDefaultsTab`, `QrDefaultsTab`, `DeveloperTab`, `DeepLinkingTab`) with clean query-string navigation (e.g. `?tab=qr-defaults`). The **link create/edit form** uses separate tabs: **Link Details**, **Targeting**, **Password**, **Expiration**, **Tracking**, **Marketing & API**, and **SEO & Social**. The settings panel allows you to configure: @@ -265,26 +405,45 @@ The settings panel allows you to configure: * **Default Redirect Status**: Choose `302 (Found / Temporary)` or `301 (Moved Permanently)`. * *Note: `302` is highly recommended for analytics accuracy because browsers cache `301` redirects, skipping subsequent logs.* * **Key Length**: Default character count (base62) for auto-generated keys (default: `6`). -* **Queue Connection**: Define the Laravel queue connection (e.g. `redis`, `database`, `sync`) used for processing visit analytics asynchronously. +* **Queue Connection**: Laravel queue driver for async visit tracking. Default is **`sync`** (works without a background worker). Set to `database` or `redis` in **Settings → General** (or `SHORT_URL_QUEUE`) when you run a matching queue worker. +* **Queue Name**: Target queue for visit tracking and counter sync jobs (default: `default`). Shown when connection ≠ `sync`. +* **Queue mode callout**: One contextual info box per async driver with a **dynamic** worker command, e.g. `php artisan queue:work redis --queue=default`. Hidden for `sync`. +* **Redis connection** (when Queue = `redis`): Host, port, password, database index, and optional key prefix — stored in settings and **override** `database.redis` / `queue.connections.redis` at runtime (no `.env` edit required for day-to-day ops). +* **Test Redis connection**: PING + counter-style Redis probe using current form values (works before Save). +* **Test queue worker**: Enqueues a probe job and confirms a worker processes it within 12 seconds; shows the exact worker command if not. ### 2. Geo-IP Country Detection Toggle country tracking and select from three drivers: -* **Headers** (Edge Resolution): Automatically detects client country using standard edge headers (e.g. Cloudflare's `CF-IPCountry`, AWS CloudFront's `CloudFront-Viewer-Country`, or generic proxies). +* **Headers** (Edge Resolution, recommended behind CDN): Reads country/city from edge headers (`CF-IPCountry`, `CloudFront-Viewer-Country`, etc.). Saving settings with this driver **automatically enables** **Trust CDN & Proxy Headers**. When headers are missing, the plugin falls back to MaxMind, then ip-api.com (rate-limited). * **MaxMind** (Offline Resolution): Reads from a local GeoIP2 database (such as the free GeoLite2-Country database). -* **IP-API** (Online Fallback): Makes an external API call to `ip-api.com` with configurable timeout. +* **IP-API** (Online): Direct lookup via `ip-api.com` with configurable timeout and built-in rate limiting (40 requests/minute guard). + +> **Tip:** Enable **Trust CDN & Proxy Headers** only when your app sits behind a real CDN or reverse proxy. The Headers driver turns this on for you on save — do not enable it on a server that receives traffic directly from the internet. ### 3. Google Analytics 4 (GA4) Integration -Sends server-side `short_url_visit` hits using the **GA4 Measurement Protocol API**. This bypasses browser-side AdBlockers entirely. -* **GA4 API Secret**: Create this secret in Google Analytics under `Admin -> Data Streams -> Measurement Protocol API secrets`. -* **Firebase App ID / Measurement ID**: The target analytics stream identifier. +Sends server-side hits using the **GA4 Measurement Protocol API** — bypassing browser ad-blockers entirely. Hits are dispatched asynchronously in `TrackShortUrlVisitJob` (never blocking redirects). + +Each visit sends a standards-compliant MP payload: +* **`page_view`** — `page_location`, `page_title`, `page_referrer`, `language` +* **`click`** — outbound link metadata (`link_url`, `link_domain`) +* **`short_url_visit`** — plugin-specific dimensions (`url_key`, device, country, QR flag) +* **`session_id`**, **`timestamp_micros`**, **`engagement_time_msec`** — required for GA4 session reporting + +Configuration: +* **GA4 API Secret** — Create in Google Analytics under `Admin → Data Streams → Measurement Protocol API secrets`. +* **Per-link Measurement ID** — Set `ga_tracking_id` (`G-XXXXXXXXXX`) on each link. Invalid IDs are rejected before HTTP. +* **Firebase App ID** (optional) — Use instead of Measurement ID for Firebase streams. +* **Test connection** — In **Settings → GA4**, enter your Measurement ID and click **Test connection**. Uses the GA4 **debug collector** with your real stream ID + secret (not a placeholder). + +Privacy: `client_id` is a deterministic SHA-256 hash of IP + User Agent — no raw PII sent to Google. ### 4. Counter Buffering (Write-back Caching) For extremely high-traffic applications, direct database writes for click counts can cause row-locking bottlenecks. -* **Buffer Click Counts**: Toggling this option buffers total and unique visit count increments in the application cache. +* **Buffer Click Counts**: Toggling this option buffers total and unique visit count increments in the application cache (or **dedicated Redis** when Queue Connection = `redis` — then buffering is **automatic** and the toggle is disabled). * **Cron Synchronization**: When enabled, the synchronization command flushes counts to the database. The package automatically registers this in the Laravel Scheduler to run every minute when counter buffering is active, so you only need to ensure the standard Laravel schedule runner (`* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1`) is running on your server. * Command: `php artisan short-url:sync-counters` -### 5. Performance & Security Tab (new in v1.2.0) +### 5. Advanced Tab — Aggregation, Rate Limiting & Security #### High-Traffic Log Management (Aggregation & Pruning) At scale, the `short_url_visits` table can grow to tens of gigabytes. The aggregation system solves this: @@ -300,34 +459,45 @@ Prevent redirect abuse and bot traffic flooding: When a client exceeds the limit, a `429 Too Many Requests` response is returned with a `Retry-After` header. +#### Analytics & Bot Detection (Settings GUI) + +These options were previously `.env`-only; they are now editable under **Settings → Performance & Security** (`?tab=advanced`) → **Analytics & Bot Detection**: + +* **Click deduplication** — Ignore repeat clicks from the same IP within a configurable window (default: off, 1 hour). Mirrors `SHORT_URL_CLICK_DEDUP` / `SHORT_URL_CLICK_DEDUP_HOURS`. +* **Verify Googlebot IP** — Optional reverse-DNS + forward IP check for Googlebot user agents. Mirrors `SHORT_URL_VERIFY_GOOGLEBOT_IP`. +* **Bot debug secret** — Allows `?bot=1` preview testing in production when the query matches `SHORT_URL_BOT_DEBUG_SECRET`. + --- -## Password-Protected Links (new in v1.2.0) +## Password-Protected Links -You can require visitors to enter a password before being redirected. Enable this in the **Targeting & Security** tab of the short URL form: +You can require visitors to enter a password before being redirected. Enable this in the **Password** tab of the short URL form: -- Set a plain-text password in the **Access Password** field. +- Set a password in the **Access Password** field. - Visitors will see a styled password prompt page before gaining access. - The unlock state is stored in the PHP session — visitors only need to enter the password once per session. +- Passwords are **hashed at rest** using Laravel's `Hash` facade (bcrypt/argon). Legacy plain-text passwords created before v5.2.0 are still verified and automatically re-hashed on the next save. ```php -// Programmatically — set via fillable attributes +// Programmatically — set via fillable attributes (hashed automatically on save) $shortUrl = ShortUrl::destination('https://secret.example.com') ->create(); $shortUrl->update(['password' => 'my-secret-pass']); + +// Verify programmatically +$shortUrl->verifyPassword('my-secret-pass'); // true +$shortUrl->hasPassword(); // true ``` -> **Note**: Passwords are currently stored as plain text. For sensitive use-cases, hash the password and compare with `Hash::check()` by overriding the redirect controller. - > [!NOTE] -> **Stateful Route Transition (v5.1.0+)**: To achieve ultra-fast (<15ms) redirects for normal links, the main `/s/{key}` path completely bypasses Laravel's session/cookie `web` middleware group. If password protection is detected, the engine dynamically redirects the visitor to a stateful `/s-auth/{key}` route where sessions are loaded and the password prompt is served. +> **Stateful vs stateless routes**: The main `/s/{key}` redirect uses only rate limiting by default (no `web` middleware). Password unlock uses `/s-auth/{key}`, which loads sessions. Visit tracking defaults to the `sync` queue so redirects work without a queue worker; switch to `redis`/`database` in **Settings → General** for async tracking on high-traffic sites. --- -## Redirect Warning Pages (new in v1.2.0) +## Redirect Warning Pages -Enable the **Show Redirect Warning Page** toggle in the **Targeting & Security** tab to display a safety interstitial before redirecting. +Enable the **Show Redirect Warning Page** toggle in the **Password** tab to display a safety interstitial before redirecting. The warning page: - Shows the destination URL clearly so visitors can verify they trust it. @@ -339,12 +509,12 @@ This feature is useful for NSFW links, external partner links, or any URL that l --- -## Security & Anti-Fraud v2.0 (new in v1.6.0) +## Security & Anti-Fraud Protect your application redirection routes and visitor data from malicious activities and automated scrapers. ### 1. VPN & Proxy Detection -Filter out anonymous proxy, VPN, or Tor connections to ensure clean analytics and prevent abuse. +When enabled in **Settings → Advanced**, filter anonymous proxy, VPN, or Tor connections to keep analytics clean and prevent abuse. Disabled by default. * **Driver Selection**: Choose between the free **IP-API** service (default) or the premium **VPNAPI.io** service (requires setting an API key). * **Configurable Action**: * **Flag Only**: Flags VPN/Proxy visits in database statistics for inspection but allows the redirection to continue. @@ -352,28 +522,28 @@ Filter out anonymous proxy, VPN, or Tor connections to ensure clean analytics an * **Verify Key**: An interactive "Verify connection" action is available in settings to check your API credentials. ### 2. Google Safe Browsing URL Verification -Scan and verify all user-provided target URLs against Google's safe browsing lookup API on creation and edit. +When enabled in **Settings → Advanced**, scan and verify user-provided target URLs against Google's Safe Browsing API on creation, edit, **and redirect** (cached per destination URL). Disabled by default. * **Protection**: Blocks malware, phishing, and social engineering domains. * **Filament UI**: Displays a clean status badge and blocks form saving if the target URL is flagged as unsafe. * **Verify Key**: Includes a "Test API Connection" action on the settings dashboard to validate your Safe Browsing API credentials. --- -## Custom Branded Expiry Pages (new in v3.0.0) +## Custom Branded Expiry Pages When a short URL is expired, deactivated, or has reached its maximum visit limit, it needs to handle the redirect gracefully: * **Custom Fallback URL**: If configured, visitors are immediately redirected to the `expiration_redirect_url` target. * **Branded Expiry Page (Default)**: If no fallback URL is specified, the system displays a premium branded, fully localized, dark-mode compatible HTML page (`expired.blade.php`) instead of a generic browser `410 Gone` error. - - Automatically displays your customized **Site Name** (and host application logo if the `setting('logo_path')` helper is defined). + - Automatically displays your customized **Site Name** from **Settings → General** (falls back to `config('app.name')`) and the host application logo when the `setting('logo_path')` helper is defined. The same branding is used on password prompts, public stats pages, and other visitor-facing screens. - Displays a clean visual alert state with details about the expired link. - Features a friendly back button pointing to your website's homepage. - Can be easily customized by publishing package views: `php artisan vendor:publish --tag=filament-short-url-views`. --- -## Smart Link Targeting (updated in v3.5.0) +## Smart Link Targeting -The **Targeting & Security** tab exposes a powerful rule engine that lets you route different visitors to different destinations — all from a single short URL. +The **Targeting** tab exposes a powerful rule engine that lets you route different visitors to different destinations — all from a single short URL. You can configure multiple rules evaluated sequentially from top to bottom. Each rule contains: - A **Target URL** (the redirect destination if rule matches). @@ -384,7 +554,7 @@ You can configure multiple rules evaluated sequentially from top to bottom. Each - **Country**: Filter by country codes (e.g. `PL`, `US`, `DE`) with flags display. - **Language**: Filter by preferred browser language codes (e.g. `pl`, `en`, `de`). -### Multi-Filter JSON Schema (v3.3.0+) +### Multi-Filter JSON Schema For programmatic or REST API updates, pass an array of rules to `targeting_rules`: @@ -436,7 +606,7 @@ If your database contains legacy single-strategy rules (e.g. `'type' => 'device' --- -## A/B Split Testing & Weighted Traffic Rotation (new in v3.5.0) +## A/B Split Testing & Weighted Traffic Rotation A single short URL can distribute traffic across 2–5 landing pages using configurable weights. This is useful for comparing conversion rates between different pages without changing the link you've already shared. @@ -495,13 +665,13 @@ To nest a split test inside a targeting rule, use `variants` within the rule obj --- -## Native App Linking & Deep Linking (new in v3.0.0) +## Native App Linking & Deep Linking This package supports two distinct levels of mobile app integration: **Per-Link App Linking** (client-side redirects using custom schemes) and **Global Deep Linking Files** (domain association files for OS-level native integration). ### 1. Per-Link App Linking (Mobile Auto-Open) -When creating or editing a short URL, the **App Linking** tab allows you to configure automatic redirects into native mobile applications. +When creating or editing a short URL, the **Targeting** tab includes an **App Linking** section where you configure automatic redirects into native mobile applications. * **How it works**: If a destination URL matches one of the 24+ pre-configured native applications (such as YouTube, TikTok, Instagram, Facebook, Spotify, WhatsApp, Messenger, etc.), the plugin can bypass standard web views for mobile visitors. * **The Interstitial Experience**: If **Auto open app on mobile** is enabled, mobile visitors are shown a premium glassmorphic redirect interstitial page that triggers the corresponding custom URL scheme (e.g. `whatsapp://`, `instagram://`, `youtube://`) to launch the native app directly, with fallback options to open in a web browser. @@ -567,7 +737,7 @@ To support seamless OS-level integrations without browser intermediaries—such --- -## High-Traffic Optimizations (new in v1.2.0) +## High-Traffic Optimizations ### Daily Stats Aggregation @@ -576,8 +746,11 @@ The `short_url_daily_stats` table stores pre-aggregated daily summaries per shor | Column | Description | |--------|-------------| | `date` | The calendar day | -| `visits_count` | Total visits | -| `unique_visits_count` | Unique visitors (by hashed IP) | +| `visits_count` | Human visits (excludes bots/proxies) | +| `unique_visits_count` | Unique human visitors (by hashed IP) | +| `all_visits_count` | All visits including bots and proxies | +| `bot_visits_count` | Bot visits for the day | +| `proxy_visits_count` | VPN/proxy visits for the day | | `device_stats` | JSON — visit counts by device type | | `browser_stats` | JSON — visit counts by browser | | `os_stats` | JSON — visit counts by operating system | @@ -589,34 +762,56 @@ The `short_url_daily_stats` table stores pre-aggregated daily summaries per shor | `utm_campaign_stats` | JSON — visit counts by UTM campaign | | `qr_visits_count` | Pre-aggregated daily QR code scans | | `language_stats` | JSON — visit counts by browser language preferences | +| `utm_terms`, `utm_contents` | JSON — UTM term/content breakdowns | +| `browser_versions`, `os_versions` | JSON — version-level breakdowns | +| `variant_stats` | JSON — A/B variant breakdowns | -The `getCachedStats()` model method **automatically merges** data from both tables: historical days come from `short_url_daily_stats`, while today's data comes directly from `short_url_visits` — completely transparent to the dashboard. +The `getCachedStats()` model method **automatically merges** data from both tables: historical days come from `short_url_daily_stats`, while today's data is merged live — from **Redis today buffer** when Settings queue is `redis`, otherwise from a short-TTL SQL micro-cache on `short_url_visits`. + +**Filtered stats** (dashboard filters active) use `FilteredStatsCollector`: pruned history is read from **cross-dimensional daily rollups** (`cross_dimensional_stats`, `cross_filter_pairs`) where possible, the retention window contributes exact `unique` via one SQL query, and only **today** (or 3+ simultaneous filters) requires a bounded raw scan. Results are cached via `StatsCacheHelper` (works on file, database, Redis, Memcached, and array cache drivers). Historical stats cache is **not** invalidated on every visit when using the Redis scaling profile. + +**Security breakdown** (bots / VPN widget) uses `getSecurityBreakdownStats()` with the same daily rollups plus a single aggregated query on raw visits. + +#### Production scaling profile (`queue_connection = redis`) + +When **Settings → Queue Connection = `redis`** and Redis is reachable: + +| Component | Role | +|-----------|------| +| `PluginRedisConnection` | Shared Redis from `queue.connections.redis` (not `CACHE_STORE`) | +| `VisitCounterBuffer` | Buffered link totals/uniques + dirty-set sync | +| `TodayStatsBuffer` | Live today summary + hourly chart counters | +| `StatsScalingProfile` | Chooses Redis vs SQL/cache paths per feature | +| `StatsVisitRecorder` | Stats side effects in the visit job without full cache bust | + +Requires a running worker: `php artisan queue:work redis --queue=` (use **Test queue worker** in Settings to verify). ### Queue Worker vs. Synchronous Execution The plugin is designed to be highly reliable and performant regardless of whether your hosting environment runs a background queue worker. -#### 1. With a Queue Worker (Recommended) -If your application runs a queue worker (e.g. via supervisor running `php artisan queue:work`), you should configure the **Queue Connection** in the **Settings** panel (or via `SHORT_URL_QUEUE` env variable) to your primary queue driver (like `redis` or `database`). -- **Performance**: High-speed redirects are prioritized. Visitor clicks, Geo-IP lookups, GA4 hits, and Webhook dispatches are immediately deferred to background queue jobs (`TrackShortUrlVisitJob` and `SendWebhookJob`), keeping redirect response times under 20ms. -- **Worker Configuration**: Ensure your queue listener is running: +#### 1. With a Queue Worker (Recommended for high traffic) +If your application runs a queue worker (e.g. via Supervisor), configure **Queue Connection** in **Settings → General** (or `SHORT_URL_QUEUE`) to `database` or `redis`. The package default is **`sync`** so redirects work out of the box without a worker. +- **Performance**: With `database` or `redis` queue, visitor clicks, Geo-IP lookups, GA4 hits, and webhook dispatches run in `TrackShortUrlVisitJob` / `SendWebhookJob` **after** the redirect response is sent — keeping the hot path lean. Latency still depends on your stack (DB, Redis, enabled security features, HTML interstitials). +- **Worker command**: Must match **Settings → General**, not necessarily `.env` `QUEUE_CONNECTION`: ```bash - php artisan queue:work --queue=default + php artisan queue:work redis --queue=default ``` - *(If you customized the queue name in Settings, replace `default` with your custom queue name).* + Replace `redis` with your selected connection and `default` with your **Queue Name** from Settings. +- **Verify before production**: Use **Test queue worker** in Settings (dispatches a probe job and waits for processing). -#### 2. Without a Queue Worker (Sync Driver Fallback) +#### 2. Without a Queue Worker (Sync Driver) If you do not run background queue workers, set the **Queue Connection** to `sync`. - **Automatic Fallback**: The plugin dynamically executes all jobs synchronously on the request thread. The visitor is redirected as soon as the synchronous processes (log recording, webhook calls, etc.) complete. - **Fault Tolerance (Safety Nets)**: Webhook failures (timeouts, 500 errors) or database tracking glitches can happen. The plugin wraps all synchronous execution points in strict error boundaries (`try/catch`). **A failed tracking job or a slow/offline webhook will NEVER crash the visitor's redirect or programmatic REST API responses.** They are gracefully logged in the background. ### Queue-Based Counter Fallback -When Redis is not available and counter buffering is enabled, the `IncrementVisitJob` is dispatched to the configured queue connection. This guarantees visit counts are not lost during cache evictions or restarts. +When counter buffering is enabled and Settings queue is **not** `redis`, `IncrementVisitJob` can dispatch to the configured queue connection so counts survive cache evictions. With **queue = `redis`**, counters use dedicated Redis keys via `VisitCounterBuffer` and flush through `short-url:sync-counters`. --- -## Social Retargeting Pixels & Central Pixel Registry (new in v3.0.0) +## Social Retargeting Pixels & Central Pixel Registry Instead of manually copy-pasting tracking pixel IDs (Meta, Google Tag, LinkedIn, TikTok, Pinterest) every time you create a new link, the package features a centralized **Retargeting Pixel Registry** with a Many-to-Many relationship. You define your marketing pixels once in the new **Pixel Registry** resource, and then easily select them via checkbox/list options when creating or editing short links. @@ -664,15 +859,17 @@ $shortUrl->pixels()->sync([$pixelId1, $pixelId2]); --- -## Custom Domain Branding (new in v4.0.0) +## Custom Domain Branding Branded links (e.g. `go.company.com/abc123` instead of `yourdomain.com/s/abc123`) significantly increase click-through rates and build trust. This package includes a full-featured custom domain manager out-of-the-box. ### Key Features -1. **Dynamic CNAME/A Record Verification**: Resolves DNS records in real-time natively using PHP's `dns_get_record()` (with fallback to `dig`) and matches against the host server IP — supports both CNAME and A record setups. -2. **DNS Setup Instructions**: Step-by-step instructions for non-technical users to connect their domain in their registrar (GoDaddy, Cloudflare, Namecheap, etc.) directly in the Filament UI. -3. **Verified-Only Assignment**: Only domains that pass DNS verification can be associated with short URLs. -4. **Root-Level Redirections**: When a visitor hits a custom domain link (e.g. `go.company.com/abc123`), the request is matched directly at the root path — omitting the default `/s/` prefix automatically. +1. **Multi-record DNS verification**: Resolves all A records and CNAME targets via `dns_get_record()` (with `gethostbynamel` / `dig` fallbacks). Matches when **any** resolved IP intersects the app host, or when a CNAME chain points to `app.url`. +2. **Activation gate**: Domains cannot stay active without passing DNS verification (`custom_domains.enforce_dns_on_activate`, default `true`). Failed activation attempts are automatically deactivated. +3. **DNS Setup Instructions**: Step-by-step instructions for non-technical users to connect their domain in their registrar (GoDaddy, Cloudflare, Namecheap, etc.) directly in the Filament UI. +4. **Verified-Only Assignment**: Only domains that are **active and verified** can be associated with short URLs (enforced in Filament and API validation). +5. **Root-Level Redirections**: When a visitor hits a custom domain link (e.g. `go.company.com/abc123`), the request is matched directly at the root path — omitting the default `/s/` prefix automatically. +6. **Scheduled re-verification**: `short-url:verify-custom-domains` re-checks DNS daily for active domains. ### How to use 1. Go to the **Custom Domains** page in the Filament admin panel (sidebar → Links → Custom Domains). @@ -766,9 +963,9 @@ go.company.com { ### Server Requirements - DNS verification runs natively using PHP's built-in `dns_get_record()` function. -- If native DNS functions are disabled or fail, the plugin falls back to using the `dig` CLI tool (`apt install dnsutils` on Debian/Ubuntu, `yum install bind-utils` on CentOS/RHEL). -- If neither is available, the plugin falls back to `$_SERVER['SERVER_ADDR']` for A record matching. -- The DNS verification runs synchronously in the admin panel when the **Verify** button is clicked. For production use with many domains, consider wrapping it in a queued job. +- If native DNS functions are disabled or fail, the plugin falls back to the `dig` CLI tool (`apt install dnsutils` on Debian/Ubuntu, `yum install bind-utils` on CentOS/RHEL). +- Verification compares resolved records against `config('app.url')` host IPs — no hardcoded server IP or `SERVER_ADDR` fallback. +- The DNS verification runs synchronously in the admin panel when the **Verify** button is clicked. For production use with many domains, the scheduled `short-url:verify-custom-domains` command handles bulk re-checks. ### Database Schema @@ -788,7 +985,7 @@ Short URLs reference their custom domain via `custom_domain_id` (foreign key on --- -## Developer REST API (new in v1.5.0) +## Developer REST API The plugin exposes a REST API that allows external systems (CRMs, Zapier, Make, custom integrations) to manage short URLs programmatically. @@ -832,6 +1029,14 @@ Each API key can have its own **individual rate limit** (requests per minute), i Configure the rate limit for each key in **Settings → API & Webhooks → Developer API Keys**. The default is **60 requests per minute**. +### Per-Key Link Scoping (Owner User ID) + +When **`scope_links_to_user`** is enabled (default), every API key **must** include an **`owner_user_id`**. All link queries and mutations through that key are scoped to short URLs where `user_id` matches — useful for multi-user panels or per-team integrations. Keys without an owner ID are rejected with **403 Forbidden**. + +When link scoping is **disabled** (`SHORT_URL_SCOPE_TO_USER=false`), `owner_user_id` is optional and keys without an owner retain access to all links (subject to scope and rate limits). + +Configure in **Settings → API & Webhooks → Developer API Keys → Owner User ID**. + ### Endpoints All endpoints are prefix-grouped under `/api/short-url/` and are protected by the API Key middleware. The effective rate limit is the lower of the global route throttle and the per-key rate limit. @@ -862,7 +1067,7 @@ curl https://yourdomain.com/api/short-url/links \ "expires_at": null, "webhook_url": null, "targeting_rules": null, - "password": null, + "password_protected": false, "show_warning_page": false, "auto_open_app_mobile": false, "ga_tracking_id": null, @@ -909,26 +1114,12 @@ curl https://yourdomain.com/api/short-url/links/abc123 \ "redirect_status_code": 302, "total_visits": 47, "unique_visits": 31, - "max_visits": null, - "activated_at": null, - "expires_at": null, - "webhook_url": null, - "targeting_rules": null, - "password": null, - "show_warning_page": false, - "auto_open_app_mobile": false, - "ga_tracking_id": null, - "track_visits": true, - "track_ip_address": true, - "track_browser": true, - "track_browser_version": true, - "track_operating_system": true, - "track_operating_system_version": true, - "track_device_type": true, - "track_referer_url": true, - "track_browser_language": true, - "pixels": [], - "notes": null, + "password_protected": false, + "is_cloaked": false, + "do_index": false, + "og_title": null, + "og_description": null, + "og_image": null, "created_at": "2026-06-01T12:00:00+00:00" } } @@ -984,8 +1175,13 @@ curl -X PATCH https://yourdomain.com/api/short-url/links/promo26 \ | `expires_at` | datetime | ❌ | Expiration timestamp (must be after or equal to `activated_at`) | | `pixels` | array of integers | ❌ | List of registered retargeting pixel IDs to associate with the link | | `webhook_url` | string (URL) | ❌ | Per-link webhook URL for immediate event notifications | -| `targeting_rules` | array | ❌ | Advanced Multi-Filter Targeting Rules JSON schema (see [Smart Link Targeting](#smart-link-targeting-updated-in-v340) for schema details) | -| `password` | string | ❌ | Password to protect the short URL | +| `targeting_rules` | array | ❌ | Advanced Multi-Filter Targeting Rules JSON schema (see [Smart Link Targeting](#smart-link-targeting) for schema details) | +| `is_cloaked` | boolean | ❌ | Embed destination in an iframe on your short-link domain (default: `false`) | +| `do_index` | boolean | ❌ | Allow search engine indexing of the short link (default: `false`) | +| `og_title` | string | ❌ | Custom Open Graph / Twitter Card title (max 255 chars) | +| `og_description` | string | ❌ | Custom social preview description (max 500 chars) | +| `og_image` | string (URL) | ❌ | Public URL to an OG image (Filament upload/import pipeline stores processed images on disk) | +| `password` | string | ❌ | Password to protect the short URL (hashed automatically on save; never returned in API responses) | | `show_warning_page` | boolean | ❌ | Toggle redirect warning interstitial page | | `auto_open_app_mobile` | boolean | ❌ | Auto open deep link in native application on mobile devices | | `ga_tracking_id` | string | ❌ | Custom Google Analytics 4 Measurement ID for this link (`G-XXXXXXXXXX`, max 50 chars) | @@ -998,6 +1194,14 @@ curl -X PATCH https://yourdomain.com/api/short-url/links/promo26 \ | `track_device_type` | boolean | ❌ | Track client device type (default: `true`) | | `track_referer_url` | boolean | ❌ | Track visitor referrer URL (default: `true`) | | `track_browser_language` | boolean | ❌ | Track visitor preferred browser language (default: `true`) | +| `external_id` | string | ❌ | External CRM/integration identifier (unique, max 255 chars) | +| `folder_id` | integer | ❌ | Assign link to a folder (`short_url_folders.id`) | +| `tag_ids` | array of integers | ❌ | Up to 5 tag IDs to attach | +| `is_archived` | boolean | ❌ | Archive instead of delete (hidden from default list) | +| `utm_source` / `utm_medium` / `utm_campaign` / `utm_term` / `utm_content` | string | ❌ | Stored on the link and merged into the destination URL on redirect (existing query params on the destination are preserved; link-level values fill gaps only) | +| `ref` | string | ❌ | Appended as `?ref=` on redirect when not already present | +| `public_stats_enabled` | boolean | ❌ | Enable the public stats page + JSON endpoint for this link (default: `false`) | +| `public_stats_password` | string | ❌ | Optional password for public stats (hashed at rest; session unlock on HTML, Bearer/POST for JSON) | **Response (POST & PUT/PATCH success):** `200 OK` (or `201 Created` for POST) containing: ```json @@ -1017,9 +1221,14 @@ curl -X PATCH https://yourdomain.com/api/short-url/links/promo26 \ "expires_at": null, "webhook_url": "https://api.mycrm.com/clicks", "targeting_rules": null, - "password": null, + "password_protected": false, "show_warning_page": false, "auto_open_app_mobile": false, + "is_cloaked": false, + "do_index": false, + "og_title": null, + "og_description": null, + "og_image": null, "ga_tracking_id": null, "track_visits": true, "track_ip_address": true, @@ -1105,6 +1314,143 @@ curl https://yourdomain.com/api/short-url/links/promo26/stats \ } ``` +#### `GET /api/short-url/links/{idOrKey}/visits` +List raw visit logs for a single short URL (paginated, 30 per page by default). + +```bash +curl "https://yourdomain.com/api/short-url/links/promo26/visits?per_page=50" \ + -H "X-Api-Key: sh_key_your_key_here" +``` + +**Response:** `200 OK` with paginated visit records (`id`, `visited_at`, `browser`, `country_code`, `referer_host`, `is_qr_scan`, UTM fields, etc.). The raw password hash is never included. + +#### `POST /api/short-url/links/bulk-delete` +Delete multiple short URLs in a single request (max 100). + +```bash +curl -X POST https://yourdomain.com/api/short-url/links/bulk-delete \ + -H "X-Api-Key: sh_key_your_key_here" \ + -H "Content-Type: application/json" \ + -d '{"ids": [1, 2, 3]}' +``` + +Alternatively, pass `"keys": ["promo26", "summer24"]` instead of `"ids"`. + +**Response:** `200 OK` +```json +{ + "message": "Short URLs deleted successfully.", + "deleted": 3 +} +``` + +#### `PATCH /api/short-url/links/bulk-update` +Update multiple short URLs at once (max 100). Supported fields in the `data` object: `is_enabled`, `notes`, `expires_at`, `max_visits`. + +```bash +curl -X PATCH https://yourdomain.com/api/short-url/links/bulk-update \ + -H "X-Api-Key: sh_key_your_key_here" \ + -H "Content-Type: application/json" \ + -d '{ + "keys": ["promo26", "summer24"], + "data": { "is_enabled": false } + }' +``` + +**Response:** `200 OK` +```json +{ + "message": "Short URLs updated successfully.", + "updated": 2 +} +``` + +#### `PUT /api/short-url/links/upsert` +Create or update a link by **`external_id`**, or by matching **`url_key` + `destination_url`**. Accepts the same fields as `POST /links`. When a match exists, unique validation ignores the existing record. + +```bash +curl -X PUT https://yourdomain.com/api/short-url/links/upsert \ + -H "X-Api-Key: sh_key_your_key_here" \ + -H "Content-Type: application/json" \ + -d '{ + "external_id": "crm-deal-42", + "destination_url": "https://example.com/offer", + "url_key": "offer42", + "notes": "Synced from CRM" + }' +``` + +**Response:** `200 OK` (updated) or `201 Created` (new link). + +#### `POST /api/short-url/links/bulk` +Bulk-create up to **100** links in one request. + +```bash +curl -X POST https://yourdomain.com/api/short-url/links/bulk \ + -H "X-Api-Key: sh_key_your_key_here" \ + -H "Content-Type: application/json" \ + -d '{ + "links": [ + {"destination_url": "https://a.com", "url_key": "bulk-a"}, + {"destination_url": "https://b.com", "url_key": "bulk-b"} + ] + }' +``` + +**Response:** `201 Created` with `data` array of created links. + +#### `GET /api/short-url/links/exists` +Check whether a URL key is already taken **within a domain scope** (default app domain = scope `0`, custom domain = its ID). + +```bash +# Default domain scope +curl "https://yourdomain.com/api/short-url/links/exists?url_key=promo26" \ + -H "X-Api-Key: sh_key_your_key_here" + +# Custom domain scope (same slug can exist on another domain) +curl "https://yourdomain.com/api/short-url/links/exists?url_key=promo26&custom_domain_id=3" \ + -H "X-Api-Key: sh_key_your_key_here" +``` + +**Response:** `200 OK` — `{"exists": true, "url_key": "promo26", "domain_scope_id": 0}` + +#### `GET /api/short-url/links/random` +Return one random link from the scoped collection (respects `owner_user_id` on the API key). + +#### `GET /api/short-url/links/info` +Lightweight lookup by `url_key` **or** `external_id` query parameter. + +```bash +curl "https://yourdomain.com/api/short-url/links/info?external_id=crm-deal-42" \ + -H "X-Api-Key: sh_key_your_key_here" +``` + +#### `GET /api/short-url/links/{idOrKey}/visits/export` +Download raw visit logs as **CSV** (same filters as the paginated visits endpoint). + +```bash +curl "https://yourdomain.com/api/short-url/links/promo26/visits/export" \ + -H "X-Api-Key: sh_key_your_key_here" \ + -o visits.csv +``` + +#### Tags & Folders REST API + +Manage organization metadata programmatically: + +| Method | Endpoint | Description | +|---|---|---| +| `GET` | `/api/short-url/tags` | List tags | +| `POST` | `/api/short-url/tags` | Create tag (`name`, optional `color`) | +| `PUT/PATCH` | `/api/short-url/tags/{id}` | Update tag | +| `DELETE` | `/api/short-url/tags/{id}` | Delete tag | +| `GET` | `/api/short-url/folders` | List folders | +| `POST` | `/api/short-url/folders` | Create folder (`name`) | +| `PUT/PATCH` | `/api/short-url/folders/{id}` | Update folder | +| `DELETE` | `/api/short-url/folders/{id}` | Delete folder | + +Attach tags and folders to links via `tag_ids` and `folder_id` on create/update/upsert requests. + #### `DELETE /api/short-url/links/{idOrKey}` Permanently delete a short URL by its database `id` or short `url_key`. @@ -1125,13 +1471,14 @@ curl -X DELETE https://yourdomain.com/api/short-url/links/promo26 \ | HTTP Code | Reason | |---|---| | `401 Unauthorized` | Missing or invalid API key | +| `403 Forbidden` | Read-only API key attempted a write, or public stats password missing/invalid | | `404 Not Found` | Short URL not found | | `422 Unprocessable Entity` | Validation error (see `errors` field in response) | | `503 Service Unavailable` | REST API is disabled in Settings | --- -## Webhooks (new in v1.5.0) +## Webhooks Webhooks allow external systems to receive real-time HTTP POST notifications when events occur on your short URLs. Payloads are dispatched **asynchronously** via the Laravel Queue — redirects are never blocked. @@ -1208,11 +1555,16 @@ All webhook requests are HTTP POST with `Content-Type: application/json` and the If the webhook endpoint returns a non-2xx response or is unreachable, the `SendWebhookJob` will automatically retry up to **3 times** with a **10-second backoff** between attempts. Failed jobs land in your queue's failed jobs table after exhausting retries. +### Outbound URL Validation + +Before a webhook job is queued or executed, the target URL is validated against an SSRF blocklist. Requests to `localhost`, link-local/private IP ranges, cloud metadata endpoints, and `.local`/`.internal` hostnames are rejected. Hostnames are **DNS-resolved** — URLs that resolve to private IPs are blocked even when the hostname looks public. This applies to per-link webhooks, global webhooks, OG image imports, and the iframeable pre-check redirect chain. + ### Webhook Priority (per-link vs global) -The resolution order is: +The global webhook URL fires only when **Enable Global Webhook** is turned on in Settings (or `global_webhook_enabled` is `true` in config). Resolution order: + 1. If the short URL has its own `webhook_url` → use it (always fires, regardless of global event selection). -2. If no per-link URL is set, and a **Global Webhook URL** is configured, and the event type is in the selected **Monitored Events** list → use the global URL. +2. If no per-link URL is set, **global webhook is enabled**, a **Global Webhook URL** is configured, and the event type is in the selected **Monitored Events** list → use the global URL. 3. Otherwise no webhook is fired. --- @@ -1228,7 +1580,7 @@ You can also pre-configure all parameters via your `.env` file: ### User Integration Configuration -The `user` configuration block (new in v3.5.0) connects the package to your application's `User` model so that creator avatars and details are displayed in the admin table: +The `user` configuration block connects the package to your application's `User` model so that creator avatars and details are displayed in the admin table: ```php // config/filament-short-url.php @@ -1260,11 +1612,20 @@ The package automatically attaches the currently authenticated user's ID to ever | `SHORT_URL_STATS_CACHE_TTL` | `geo_ip.stats_cache_ttl` | `300` | Caching TTL in seconds for dashboard charts. | | `SHORT_URL_QUEUE` | `queue_connection` | `'sync'` | Queue connection for recording visits. | | `SHORT_URL_QUEUE_NAME` | `queue_name` | `'default'` | Queue name to which visit tracking jobs are dispatched. | +| `REDIS_HOST` | `redis.host` | `'127.0.0.1'` | Default Redis host (overridden by Settings when queue = `redis`). | +| `REDIS_PORT` | `redis.port` | `6379` | Default Redis port. | +| `REDIS_PASSWORD` | `redis.password` | `null` | Default Redis password. | +| `REDIS_DB` | `redis.database` | `0` | Default Redis database index. | +| `REDIS_PREFIX` | `redis.prefix` | `''` | Optional Redis key prefix for plugin keys. | | `SHORT_URL_CACHE_TTL` | `cache_ttl` | `3600` | Redirection model caching TTL (set to `0` to disable). | | `GA4_API_SECRET` | `ga4.api_secret` | `null` | Google Analytics 4 Measurement Protocol API Secret. | | `FIREBASE_APP_ID` | `ga4.firebase_app_id` | `null` | Google Analytics 4 Firebase App ID (or uses Measurement ID). | | `SHORT_URL_COUNTER_BUFFERING` | `counter_buffering.enabled` | `false` | Buffer click counts in cache (flushed via console command). | -| `SHORT_URL_TRUST_CDN_HEADERS` | `trust_cdn_headers` | `false` | Trust proxy/CDN headers to extract real client IP and country. | +| `SHORT_URL_LIVE_FEED_SSE_INTERVAL` | `live_feed.sse_interval_seconds` | `3` | Live feed SSE heartbeat / poll interval (seconds). | +| `SHORT_URL_LIVE_FEED_SSE_MAX_DURATION` | `live_feed.sse_max_duration_seconds` | `120` | Max SSE stream duration before client reconnects. | +| `SHORT_URL_LIVE_FEED_REDIS_PUSH` | `live_feed.use_redis_push` | `true` | Use Redis pub/sub push when cache is Redis + PhpRedis; `false` forces SSE poll. | +| `SHORT_URL_TRUST_CDN_HEADERS` | `trust_cdn_headers` | `false`¹ | Trust proxy/CDN headers to extract real client IP and country. | +| `SHORT_URL_CUSTOM_DOMAIN_ENFORCE_DNS` | `custom_domains.enforce_dns_on_activate` | `true` | Require passing DNS verification before a custom domain can stay active. | | `SHORT_URL_PRUNING_ENABLED` | `pruning.enabled` | `true` | Enable daily aggregation and log pruning. | | `SHORT_URL_PRUNING_DAYS` | `pruning.retention_days` | `90` | Number of days to retain raw visit logs. | | `SHORT_URL_RATE_LIMITING` | `rate_limiting.enabled` | `false` | Enable per-IP redirect rate limiting. | @@ -1272,6 +1633,19 @@ The package automatically attaches the currently authenticated user's ID to ever | `SHORT_URL_RATE_LIMIT_DECAY` | `rate_limiting.decay_seconds` | `60` | Rate limiter rolling window in seconds. | | `SHORT_URL_DEEP_LINKING_ENABLED` | `deep_linking.enabled` | `false` | Enable serving domain association files for deep linking. | | `SHORT_URL_ENABLE_FALLBACK` | `enable_fallback_route` | `true` | Toggle registration of the global fallback redirection route. | +| `SHORT_URL_API_ENABLED` | `api_enabled` | `false` | Enable the Developer REST API (`/api/short-url/*`). | +| `SHORT_URL_VPN_DETECTION` | `vpn_detection.enabled` | `false` | Enable VPN/proxy detection on incoming visits. | +| `SHORT_URL_VPN_DRIVER` | `vpn_detection.driver` | `'ip-api'` | VPN detection driver (`ip-api` or `vpnapi`). | +| `SHORT_URL_VPNAPI_KEY` | `vpn_detection.vpnapi_key` | `null` | VPNAPI.io API key (when driver is `vpnapi`). | +| `SHORT_URL_VPN_BLOCK_ACTION` | `vpn_detection.block_action` | `'flag_only'` | Action on VPN/proxy detection (`flag_only` or `block_with_403`). | +| `SHORT_URL_SAFE_BROWSING` | `safe_browsing.enabled` | `false` | Enable Google Safe Browsing URL checks. | +| `SHORT_URL_SAFE_BROWSING_KEY` | `safe_browsing.api_key` | `null` | Google Safe Browsing API key. | +| `SHORT_URL_CLICK_DEDUP` | `click_deduplication.enabled` | `false` | Ignore repeat clicks from the same IP within the dedup window. | +| `SHORT_URL_CLICK_DEDUP_HOURS` | `click_deduplication.hours` | `1` | Deduplication window in hours. | +| `SHORT_URL_VERIFY_GOOGLEBOT_IP` | `bot_detection.verify_google_bot_ip` | `false` | Verify Googlebot user agents via reverse DNS + forward IP check. | +| `SHORT_URL_BOT_DEBUG_SECRET` | `bot_detection.debug_secret` | `null` | Secret value for `?bot=` debug param outside local/testing environments. | + +> ¹ When `geo_ip.driver` is `headers`, **Trust CDN & Proxy Headers** is automatically enabled on settings save and at runtime. The effective default for CDN deployments is therefore `true`. > [!TIP] > **Database Configuration Preferred**: Avoid configuring large, multi-line JSON blocks (such as `apple-app-site-association` and `assetlinks.json`) via `.env` file environment variables as it is error-prone and can cause parsing issues. The recommended approach is to configure them dynamically via the **Filament Settings Panel**, which stores them securely in the database with real-time JSON validation. @@ -1284,13 +1658,15 @@ The package automatically attaches the currently authenticated user's ID to ever |---------|-------------| | `short-url:sync-counters` | Flushes buffered visit counts from cache to the database. Schedule every minute when counter buffering is enabled. | | `short-url:aggregate-and-prune` | Aggregates previous days' raw visits into `short_url_daily_stats` and prunes raw records older than the configured retention period. Schedule daily (e.g. at `02:00`). | +| `short-url:verify-custom-domains` | Re-verifies DNS for all active custom domains. Schedule daily in production. | -### Automatic Scheduler Registration (v1.3.0) +### Automatic Scheduler Registration As of version `1.3.0`, **you no longer need to manually copy scheduled commands into your host application code!** The plugin automatically registers the required tasks inside its ServiceProvider booted phase, dynamically respecting your Settings GUI toggles: - **`short-url:aggregate-and-prune`** is scheduled **daily at 02:00** (runs automatically only if **Enable Daily Aggregation** is ON). - **`short-url:sync-counters`** is scheduled **every minute** (runs automatically only if **Buffer Visit Counts in Cache** is ON). +- **`short-url:verify-custom-domains`** is scheduled **daily** (always registered; re-checks DNS for active custom domains). > [!IMPORTANT] > **Cron Setup Required**: @@ -1364,6 +1740,8 @@ echo $shortUrl->getShortUrl(); // https://yourdomain.com/s/promo2026 ## Model Helpers & Service ### ShortUrl Model Methods +* `hasPassword(): bool` — Whether the link requires a password. +* `verifyPassword(string $plain): bool` — Verify a plain-text password against the stored hash (supports legacy plain-text values). * `isActive(): bool` — Checks if the short URL is enabled and within its activation/expiration timestamps. * `isExpired(): bool` — Checks if the URL has passed its expiration date. * `getShortUrl(): string` — Resolves the complete URL string. @@ -1403,7 +1781,174 @@ Event::listen(ShortUrlVisited::class, function (ShortUrlVisited $event) { --- ## Visitor Filtering (Bot Detection) -Visits from scrapers, search bots, and web crawlers are automatically ignored to keep stats clean. The system matches user agents against a custom list (including Googlebot, Bingbot, Applebot, Facebook, Twitter, and generic curl/http request clients). + +Visits from scrapers, search bots, and web crawlers are **not recorded** in the visit log at all — they never increment counters or appear in analytics. The redirect engine still serves appropriate responses (OG preview HTML for bots when custom meta tags are configured, or a standard redirect otherwise). + +Bot detection matches user agents and referers against a configurable list (Googlebot, Bingbot, Applebot, Facebook, Twitter, Slack, WhatsApp, and generic curl/http clients). Optional Googlebot reverse-DNS IP verification is available via `SHORT_URL_VERIFY_GOOGLEBOT_IP`. + +The `?bot=1` query parameter (for testing link previews) is restricted to `local`/`testing` environments or requires a matching `SHORT_URL_BOT_DEBUG_SECRET` (configurable in **Settings → Advanced**). + +--- + +## Live Activity Feed (SSE) + +The **Live Feed** tab (`/{record}/stats/live`) streams visit updates to the Filament panel over **Server-Sent Events (SSE)**. The browser opens an authenticated `EventSource` to `GET /short-url/live-feed/{id}/stream` and calls Livewire `onStreamUpdate()` only when a new visit id arrives — no full-page or `wire:poll` refresh. + +### How the cursor works (all environments) + +On every recorded visit, `TrackShortUrlVisitJob` calls `LiveFeedBroadcaster::publish()` which stores the latest visit id (TTL 24 h) and, when Redis is available, **`PUBLISH`**es it on a per-link channel. + +**Redis source priority** (first match wins — Settings override `.env` cache): + +1. **Short URL Settings → General → Queue Connection = `redis`** — uses Laravel's `queue.connections.redis` Redis connection (same infra as async visit jobs). +2. **`CACHE_STORE=redis`** — uses the default Laravel cache Redis store. +3. **Otherwise** — cursor lives in the active cache driver (`database`, `file`, …) with SSE poll + optional DB fallback. + +The SSE stream tracks a **`cursor`** (last seen visit id). The widget badge shows which transport is active: + +| Badge | Meaning | +|---|---| +| **Live · Redis push** | Redis (from Settings queue **or** cache) + PhpRedis — instant updates via `SUBSCRIBE` | +| **Live · SSE poll** | All other setups — interval check of the cursor | + +### Auto-adaptive SSE transport + +The plugin resolves Redis at runtime (`LiveFeedBroadcaster::resolveRedisSource()`): + +| Redis source | Redis client | SSE transport | DB `MAX(id)` in loop? | +|---|:---:|:---|:---:| +| Settings **Queue = redis** | **PhpRedis** | **Redis pub/sub push** | ❌ | +| Settings **Queue = redis** | Predis | **SSE poll** on Redis cursor | ❌ | +| `CACHE_STORE=redis` (queue ≠ redis) | **PhpRedis** | **Redis pub/sub push** | ❌ | +| `CACHE_STORE=redis` | Predis | **SSE poll** on Redis cursor | ❌ | +| No Redis (sync/database queue + database cache) | — | **SSE poll** on cache; DB fallback when cursor cold | Only on cold cursor | + +**Typical production setup:** set **Queue Connection = `redis`** in **Settings → General** (no need to change `CACHE_STORE` if it stays `database`). Ensure `REDIS_CLIENT=phpredis` and a running `queue:work` for async visit jobs. + +**Predis note:** cursor + `PUBLISH` still work, but SSE uses polling because Predis cannot reliably time out `SUBSCRIBE` for heartbeats. + +**Queue timing:** with **`sync`** queue, the cursor updates on the redirect thread. With **`redis`** queue, it updates when the worker runs `TrackShortUrlVisitJob` (typically sub-second). + +### Configuration + +| Setting / env | Default | Purpose | +|---|---|---| +| **Settings → Queue Connection** | `sync` | **`redis` enables live-feed Redis** (wins over `CACHE_STORE`) | +| `CACHE_STORE` | `database` | Fallback Redis source when queue ≠ redis | +| `REDIS_CLIENT` | `phpredis` | Required for pub/sub push; Predis → SSE poll | +| `SHORT_URL_LIVE_FEED_SSE_INTERVAL` | `3` | Heartbeat / poll interval (seconds) | +| `SHORT_URL_LIVE_FEED_SSE_MAX_DURATION` | `120` | Max SSE connection length before client auto-reconnects | +| `SHORT_URL_LIVE_FEED_REDIS_PUSH` | `true` | Set `false` to force SSE poll even with PhpRedis + Redis | + +Filament badge and Alpine client **reconnect automatically** after the stream closes (every ~120 s), passing the updated `cursor` so no visits are skipped. + +--- + +## Public Stats (Shareable Analytics) + +Each link can expose a **read-only public stats page** and matching **JSON API** without API key authentication — useful for sharing campaign dashboards or embedding click counts on landing pages. + +> **Upgrade note:** requires migration `2026_06_08_000001` (`public_stats_enabled`, `public_stats_password`). Run `php artisan migrate` after updating the package. + +### Enable in Filament + +Open the link row **⋮ menu → Public stats** (`P`): + +| State | What you see | +|---|---| +| **Off** | Toggle + hint; save to generate the shareable URL | +| **On** | Status badge, copyable link, **Open** button, optional password field | +| **Password** | Leave blank to keep the current hash; disable public stats to clear it | + +The public URL is built from the link’s `url_key` (and custom domain when configured), e.g. `https://yourdomain.com/s/public-stats/promo26`. + +### Public HTML page (browser) + +A normal `GET` in the browser renders a branded stats dashboard: + +- Summary cards: total / unique visits, today, this week, this month, QR scans +- Visits-by-day table with optional **date_from** / **date_to** filters +- **Site name** from **Settings → General → Site Name Override** (same as password/expiry pages); logo from host `setting('logo_path')` when available +- Optional password form (session unlock, same pattern as `/s-auth/{key}`) + +Legacy alias: `GET /short-url/public-stats/{url_key}` behaves the same. + +### JSON API (embeds & curl) + +Request JSON explicitly when integrating programmatically: + +```bash +# Accept header +curl -H "Accept: application/json" "https://yourdomain.com/s/public-stats/promo26" + +# or query flag +curl "https://yourdomain.com/s/public-stats/promo26?format=json&date_from=2026-06-01&date_to=2026-06-30" +``` + +If a password is set, send it in the **POST body** or **`Authorization: Bearer …`** header (not in the query string): + +```bash +curl -X POST -H "Content-Type: application/json" \ + -d '{"password":"secret"}' \ + "https://yourdomain.com/s/public-stats/promo26" + +curl -H "Authorization: Bearer secret" \ + -H "Accept: application/json" \ + "https://yourdomain.com/s/public-stats/promo26" +``` + +You can also enable or configure public stats via the REST API (`public_stats_enabled`, `public_stats_password` on create/update). + +**Response:** `200 OK` with a **public-safe stats subset** (`totalVisits`, `uniqueVisits`, `visitsToday`, `visitsThisWeek`, `visitsThisMonth`, `qrScans`, `visitsByDay` — no raw visit rows or sensitive metadata). Returns **`404`** for unknown keys **and** disabled public stats (prevents slug enumeration). Returns **`403`** when password is missing, wrong, or supplied in the query string. Rate limited to **10 requests/minute per IP per link**. + +--- + +## SEO, Open Graph & Link Cloaking + +Control how each short link appears on social networks, in search engines, and in the browser address bar — all from the **SEO & Social** tab: + +| Toggle / field | Config key | What it does | +|---|---|---| +| **Search Engine Indexing** | `do_index` | When **off** (default), every redirect and interstitial sends `X-Robots-Tag: noindex, nofollow` and ``. When **on**, a canonical URL is emitted and bots may index the short link. | +| **Link Cloaking** | `is_cloaked` | When **on**, human visitors see the destination embedded in an iframe on your short-link domain — the address bar keeps your branded URL. Social crawlers receive a standalone OG HTML page instead of the iframe. | +| **OG Title** | `og_title` | Custom Open Graph / Twitter Card title (falls back to link title or URL key). | +| **OG Description** | `og_description` | Custom social preview description. | +| **OG Image** | `og_image` | Upload or auto-import a preview image (WebP, stored on disk). Rendered with `og:image:width` / `og:image:height` for large-card previews. | + +### How it works on redirect + +1. **Social bots & crawlers** (Facebook, LinkedIn, X, Slack, WhatsApp, Googlebot, …) hit your short URL and receive a dedicated HTML page with full OG/Twitter meta tags — **without** `meta refresh` or JavaScript redirects, so preview scrapers can read the tags reliably. +2. **Human visitors** on a cloaked link see the destination inside an iframe; on a normal link they are redirected instantly. +3. **Search indexing off** applies `noindex` headers on every response type: direct redirects, password prompts, warning pages, expiry pages, pixel loaders, and OG preview pages. + +### Auto-scrape & live preview + +- Click **Scrape from destination** in the Filament form to pull `og:title`, `og:description`, and `og:image` from the target URL (SSRF-safe, streaming fetch). +- Imported OG images are processed to WebP and promoted to permanent storage on save. +- A live **Social Preview** sidebar shows exactly how the link card will look before you publish. + +### Iframeable pre-check (cloaking) + +Before enabling **Link Cloaking**, verify the destination allows iframe embedding: + +```bash +curl -X POST https://yourdomain.com/short-url/check-iframeable \ + -H "Cookie: ..." \ + -H "Content-Type: application/json" \ + -d '{"url": "https://destination.example.com/page"}' +``` + +**Response:** +```json +{ + "url": "https://destination.example.com/page", + "iframeable": true +} +``` + +Sites that send `X-Frame-Options: DENY/SAMEORIGIN` or restrictive `Content-Security-Policy: frame-ancestors` headers will return `"iframeable": false`. + +OG metadata can also be fetched manually via the authenticated scrape endpoint: `GET /short-url/scrape-meta?url=...` --- @@ -1414,15 +1959,195 @@ All migrations are compatible with **SQLite**, **MySQL**, and **PostgreSQL**: - No MySQL-specific `ENUM` types — `device_type` uses `VARCHAR(20)` validated at PHP level. - No `->after()` column ordering hints — fully cross-database. - JSON columns work natively on MySQL 5.7.8+ and are stored as TEXT on SQLite. +- **Upgrade-safe schema changes** — New columns are nullable or have safe defaults; indexes are additive. See [Upgrading](#upgrading) for the v5.2.0 migration details. --- ## Changelog +Recent releases are summarized below. For the full version history (v1.2.0–v4.0.0), see **[CHANGELOG.md](CHANGELOG.md)**. + +### v5.2.3 + +> **Production Redis mode, stats scaling, and Settings diagnostics.** + +#### Settings → Redis & queue + +- **Redis in Settings GUI** — Host, port, password, DB index, key prefix when Queue Connection = `redis`; overrides `database.redis` and `queue.connections.redis` at runtime. +- **Test Redis connection** — Health check with PING + INCR/SADD probe (preview unsaved form values). +- **Test queue worker** — Probe job + 12 s wait; reports exact `php artisan queue:work {connection} --queue={name}` if no worker. +- **Unified queue callout** — One box per async driver with dynamic worker command; hidden for `sync`. + +#### Stats scaling (high traffic) + +- **`StatsScalingProfile`**, **`PluginRedisConnection`**, **`VisitCounterBuffer`**, **`TodayStatsBuffer`**, **`StatsVisitRecorder`** — Dedicated Redis counters/stats when Settings queue is `redis`; historical stats cache no longer busted on every visit. +- **Auto counter buffering** — Forced on when queue = `redis`. +- **Live feed** — Shares queue Redis infra with counters/stats when configured. + +#### Public stats UI + +- **Filament row action** — **⋮ → Public stats** (`P`): toggle, optional password, copy/open shareable URL. +- **Public HTML dashboard** — Browser `GET /s/public-stats/{key}` with branded layout (Settings site name + optional host logo). +- **Dual format** — Same route serves HTML by default; JSON via `Accept: application/json`, `?format=json`, or authenticated POST. +- **Password flows** — HTML uses session unlock after form POST; JSON uses POST body or `Authorization: Bearer`. Query-string passwords rejected (`403`). + +#### Tests + +- `StatsScalingTest`, `RedisSettingsTest`, `PluginRedisConnectionTesterTest`, `PublicStatsUiTest`. + +### v5.2.2 + +> **Enterprise hardening, airtight counters, and maintainability.** + +#### Security (P0 / P1) + +- **Stored XSS fixes** — Pixel interstitial and Alpine stats widgets use `@json()` instead of `addslashes()` for JavaScript context output. +- **Atomic `max_visits`** — `VisitSlotReservation` reserves slots at redirect time with fast-path `UPDATE … WHERE total_visits < max_visits` and pessimistic locking near the cap; parallel bursts cannot exceed the limit. +- **Single-use + bots** — Social preview bots no longer consume single-use links. +- **Counter buffer integrity** — Cache increments are reverted before DB fallback so `SyncBufferedCountersCommand` cannot double-count. +- **API tenancy** — Keys without `owner_user_id` are rejected when `scope_links_to_user` is enabled; folder/tag/pixel ownership validated on API writes (IDOR fix). +- **Redirect order** — `single_use` and `max_visits` run before `auto_open_app_mobile` interstitial and visit tracking. +- **`max_visits` without tracking** — Visit caps enforced even when `track_visits=false` (counters still increment). + +#### Performance (P1) + +- **`max_visits_pessimistic_remaining`** — Configurable threshold (default 5) for when pessimistic locking kicks in. +- **Live feed adaptive SSE** — Redis cache + PhpRedis → pub/sub push (instant); Predis or non-Redis cache → SSE poll on cache cursor; auto-reconnect with cursor; see [Live Activity Feed](#live-activity-feed-sse). +- **Stats cache lock** — `Cache::lock` on cold misses prevents thundering herd across widgets. +- **List table batching** — `preloadBufferedCountersForIds()` loads buffered counters in one round-trip. + +#### Quality (P2) + +- **Public stats** — Uniform `404` for unknown/disabled keys; response limited to a public-safe subset. +- **Octane-safe A/B variant** — `resolved_ab_variant` stored on `request()->attributes`, not `app()->instance()`. +- **Filtered uniques** — Daily rollup `unique_visits_count` merged in `FilteredStatsCollector`. +- **`findByKey()`** — Queries by `domain_scope_id` for better index use. + +#### Code quality + +- **`HasStats` split** — Facade trait composing `HasVisitCounters`, `HasStatsCache`, `HasStatsQueries`, and `HasSecurityStats`; atomic reservation centralized in `VisitSlotReservation`. +- **i18n audit** — Filament panel, visitor interstitials, and API/redirect error messages use `__('filament-short-url::default.*')` translation keys (EN + PL). + +### v5.2.1 + +> **Policy, API stats, SSE live feed, and load baselines.** + +#### Authorization & API + +- **`ShortUrlPolicy`** — Per-link authorization for view, update, delete, and settings access. Aligns with `scope_links_to_user` and `user_id`. Override in your app by registering your own policy. +- **API stats filters** — `GET /api/short-url/links/{idOrKey}/stats` supports the same cross-filters as the Filament dashboard (country, device, date range, UTM, and more). Query aliases: `country`, `device`. Response includes `meta.filters`. + +#### Live feed + +- **SSE stream** — Authenticated endpoint pushes visit updates to the admin panel without Livewire polling. +- **Configurable timing** — `live_feed.sse_interval_seconds` and `live_feed.sse_max_duration_seconds`. +- **Adaptive transport (v5.2.2+)** — Auto-detects Redis cache + PhpRedis for pub/sub push; falls back to cache-cursor polling (Predis, file, database drivers). See [Live Activity Feed](#live-activity-feed-sse). + +#### Load testing + +- **`php artisan short-url:stress-redirect {key}`** — Quick in-process redirect timing (avg / min / p95 / max). +- **`scripts/k6/redirect-baseline.js`** — Concurrent HTTP load test for staging or production-like environments (requires [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/)). + +### v5.2.0 + +> **Main release theme: SEO & Social** — First-class per-link Open Graph metadata, Search Engine Indexing control, and Link Cloaking. None of these existed in v5.1.x or earlier. + +#### SEO, Open Graph & Link Cloaking *(headline)* + +- **New SEO & Social tab** — Dedicated Filament form tab for `og_title`, `og_description`, `og_image`, **Search Engine Indexing** (`do_index`), and **Link Cloaking** (`is_cloaked`). Live social preview sidebar in the admin panel. +- **Custom Open Graph meta** — Per-link OG/Twitter tags (`og:site_name`, image dimensions, canonical URL, HTTPS-normalized images) served to Facebook, LinkedIn, X, Slack, WhatsApp, and other preview bots. +- **Search Engine Indexing (`do_index`)** — Per-link toggle (default: off). When disabled, all response types emit `X-Robots-Tag: noindex, nofollow` and matching `` tags — redirects, password prompts, warning pages, expiry pages, pixel loaders, and OG HTML. +- **Link Cloaking (`is_cloaked`)** — Embeds the destination in an iframe on your short-link domain so the browser address bar shows your branded URL. Human visitors see the iframe; crawlers receive a standalone OG page without iframe/JS redirect. +- **Bot-safe OG pages** — Social crawlers get full meta tags without `meta refresh` or JavaScript redirects, so link previews scrape reliably. +- **OG meta scraper** — SSRF-safe streaming fetch from the destination URL; auto-populates title, description, and image in the Filament form (`GET /short-url/scrape-meta`). +- **OG image pipeline** — Download, WebP conversion (Imagick → GD → original fallback), temp-bucket storage with automatic promotion to `short-urls/og/` on save. +- **Iframeable pre-check** — Authenticated `POST /short-url/check-iframeable` endpoint validates `X-Frame-Options` / CSP `frame-ancestors` before enabling cloaking. +- **`OgMetaPresenter` & `redirect-html` view** — Centralized OG rendering for cloaked links and bot previews. + +#### Security +- **Password hashing** — Link passwords are hashed at rest (bcrypt/argon). Legacy plain-text passwords remain verifiable and are re-hashed automatically on save. New model helpers: `hasPassword()` and `verifyPassword()`. +- **API password privacy** — REST responses expose `password_protected: bool` instead of the raw password/hash. +- **Webhook SSRF guard** — Outbound webhook URLs are validated against private IP, localhost, and cloud metadata blocklists before queueing and execution. +- **Bot query hardening** — The `?bot=1` debug parameter is limited to `local`/`testing` environments or a configured `SHORT_URL_BOT_DEBUG_SECRET`. +- **Optional Googlebot IP verification** — Reverse-DNS + forward IP check for Googlebot user agents (`SHORT_URL_VERIFY_GOOGLEBOT_IP`). + +#### Analytics & Bot Detection +- **Bot visit exclusion** — Crawlers and preview bots no longer create visit records or increment counters. +- **Click deduplication (opt-in)** — Configurable per-IP dedup window (`SHORT_URL_CLICK_DEDUP`, default off) to suppress repeat-click inflation. +- **`StatsSqlHelper`** — Cross-database date/month SQL expressions; stats logic split into focused traits (`HasVisitCounters`, `HasStatsCache`, `HasStatsQueries`, `HasSecurityStats`) with `VisitSlotReservation` for atomic caps. + +#### REST API +- **Expanded serializer** — Responses now include `password_protected`, `is_cloaked`, `do_index`, `og_*`, `qr_scans`, `single_use`, `forward_query_params`, `external_id`, UTM fields, `folder_id`, `tags`, and `public_stats_enabled`. +- **`GET /links/{idOrKey}/visits`** — Paginated raw visit log export. +- **`GET /links/{idOrKey}/visits/export`** — CSV export of visit logs. +- **`POST /links/bulk-delete`** — Bulk delete by database IDs or URL keys (max 100). +- **`PATCH /links/bulk-update`** — Bulk update `is_enabled`, `notes`, `expires_at`, `max_visits`. +- **`POST /links/bulk`** — Bulk create up to 100 links. +- **`PUT /links/upsert`** — Create or update by `external_id` (or `url_key` + `destination_url` match). +- **`GET /links/exists`**, **`GET /links/random`**, **`GET /links/info`** — Slug availability, random link, and lightweight lookup helpers. +- **Tags & Folders API** — Full CRUD at `/api/short-url/tags` and `/api/short-url/folders`. +- **Per-key owner scoping** — `owner_user_id` on API keys limits access to links created by that user (required when `scope_links_to_user` is enabled — see [v5.2.2](#v522)). +- **Link-level UTM params** — `utm_*` and `ref` stored on the link and merged into the destination on redirect. + +#### Public Stats +- **Shareable stats endpoint** — `GET` or `POST /short-url/public-stats/{url_key}` (and prefixed `/s/public-stats/{url_key}`) returns aggregated analytics when `public_stats_enabled` is on. **v5.2.3+** adds a public HTML dashboard and Filament **⋮ → Public stats** modal. +- **Password auth** — If `public_stats_password` is set, send the password in the **POST body** or **`Authorization: Bearer …`** header for JSON; HTML uses a password form with session unlock. Passwords in the query string are **rejected** (403). +- **Host scope** — Stats are scoped to the request host (default app domain vs verified custom domain). + +#### Security & Reliability Hardening +- **DNS-aware SSRF protection** — Outbound URLs (webhooks, OG scraper, iframe checker redirects) resolve hostnames and block targets that resolve to private/metadata IPs. +- **`OutboundUrl` validation rule** — Reusable SSRF guard for form requests and services. +- **Iframeable redirect re-validation** — Redirect chains during cloaking pre-check are followed manually without `allow_redirects`, re-validating each hop. +- **Safe Browsing fail-closed** — When Safe Browsing is enabled but the API is unreachable, URLs are rejected instead of silently allowed. +- **VPN detection fail-closed** — When `block_with_403` is enabled, API timeout/error treats the client as proxy (403). +- **Custom domain legacy URLs** — `/s/{key}` on a verified custom domain returns **301** to `/{key}`. +- **Global webhook toggle respected** — `global_webhook_enabled` in Settings/config must be on before the global webhook URL fires. +- **`log-error` endpoint hardened** — Authenticated, throttled, and validated; no longer accepts anonymous error dumps. +- **Single-use link fix** — Expired single-use links return `410` without recording a visit or incrementing counters. +- **Redis counter fix** — Buffered counter sync no longer fails when Redis is the cache driver. + +#### Settings GUI +- **Analytics & Bot Detection section** — Click deduplication, Googlebot IP verification, and bot debug secret are editable in **Settings → Advanced** (previously `.env`-only). + +#### Database +- **Additive migrations** — `2026_06_08_000001`, `2026_06_09_000001`, and `2026_06_10_000001` — safe upgrade from v5.1.x with no data loss. Cross-database: SQLite, MySQL, PostgreSQL. + +#### Performance & reliability +- **`domain_scope_id`** — Composite unique `(url_key, domain_scope_id)` so the same slug can exist on the default domain and on custom domains independently. +- **Custom domain URLs** — Links on verified custom domains resolve at `https://domain.com/{key}` (no `/s/` prefix); password auth at `/auth/{key}`; legacy `/s/{key}` → 301 → `/{key}`. +- **Real-time visit limits** — `getRealTimeTotalVisits()` merges DB totals with counter-buffer cache; `max_visits` enforced correctly even when redirect model is cached. +- **Counter buffering fixes** — Redis dirty-set prefix, scoped buffer loads, `SyncBufferedCountersCommand` batch flush, incremental aggregation with `last_aggregation_date`. +- **Stats accuracy & performance** — Unique visits via `COUNT(DISTINCT ip_hash)`; bot/proxy filtering in human aggregation; daily rollups for all chart dimensions; **`FilteredStatsCollector`** (single raw scan with filters); world map and security widget routed through cached stats layers. +- **API hardening** — DB transactions on bulk/upsert, per-link cache invalidation, scoped `exists`, hashed-only API keys, public stats rate limit + password not in query string. +- **Custom domain cron** — `short-url:verify-custom-domains` re-checks DNS for active domains. +- **Central cache invalidation** — `ShortUrlCacheInvalidator`, `HostNormalizer`, reserved path segments on custom domains. +- **Settings** — VPN timeout/TTL in GUI; settings cache TTL 3600s; route cache cleared on prefix change; `set()` surfaces errors to the admin UI. +- **Default queue: `sync`** — No background worker required by default; opt in to `database`/`redis` when you run a worker. + +#### Enterprise hardening + +- **`Ga4MeasurementProtocolService`** — Centralized GA4 MP with `page_view`, `click`, `short_url_visit`, `session_id`, `timestamp_micros`, `engagement_time_msec`; Measurement ID format validation; debug-collector credential test with real stream ID. +- **Geo-IP foolproofing** — Headers driver auto-enables CDN trust on save; offline fallback chain (MaxMind → ip-api); ip-api rate limit guard (40/min). +- **Custom domain DNS gate** — Multi A-record + CNAME verification; activation blocked until DNS passes (`enforce_dns_on_activate`); removed `SERVER_ADDR` fallback. +- **API upsert lock** — `lock_url_key` now blocks `url_key` and `custom_domain_id` changes via upsert (parity with panel/API update). +- **Stats reliability fixes** — Cross-filter aggregation, public stats rate limiting, webhook SSRF protection, and Octane-safe memory flushing. +- **Stateless redirect default** — `/s/{key}` without `web` middleware; password unlock on `/s-auth/{key}`. +- **Filament link scoping** — `LinkUserScope` + `scope_links_to_user`. +- **API domain scope** — `exists` respects custom domains; store/update validate domain ownership. +- **Locked URL keys** — Server-side enforcement when `lock_url_key` is enabled. +- **Safe Browsing on redirect** — Cached re-check when enabled. +- **Webhook signing** — Signing secret required for global webhooks by default. + +#### Refactoring & Validation +- **`ShortUrlRedirectHandler`** — Redirect logic extracted from the controller; `RobotsTagApplicator` applied consistently. +- **`RedirectUrlResolver`** — Shared redirect Location resolution for meta scraper and OG image importer. +- **`SafeUrl` rule** — Google Safe Browsing validation reused in Form Requests. +- **Config defaults** — Explicit `api_enabled`, `vpn_detection`, `safe_browsing`, and `click_deduplication` sections in published config. + ### v5.1.0 #### Request Lifecycle & Middleware Optimization -- **Bypassed `web` middleware group** — Standard redirect paths (`s/{key}` and fallback routes) bypass session/cookie instantiation entirely. Under active caching, redirects resolve statelessly in `< 15ms`. +- **Stateless redirect middleware** — Standard redirect paths (`/s/{key}`) use rate limiting only (no `web` session middleware by default). Password auth uses `/s-auth/{key}` with sessions. HTML interstitials and sync visit tracking add latency. - **Dynamic Stateful Route Transition** — Password-protected redirects are dynamically routed to a stateful `/s-auth/{key}` route wrapped under the `web` group. Handles session verification, password prompts, brute force rate-limiting, and warning interstitials. - **Robust Integration Testing** — Refactored the entire package test suite and main application feature tests to accommodate the dual stateful/stateless redirect routing architectures without coverage loss. @@ -1437,7 +2162,7 @@ All migrations are compatible with **SQLite**, **MySQL**, and **PostgreSQL**: - **Country flags via `flagcdn.com`** — Flag images are rendered as `` instead of emoji. The URL is precomputed once in PHP (`getViewData()`), not per-row in Blade. Alpine.js `x-on:error` handles broken images without CSP-unsafe inline `onerror` handlers. - **Blade performance** — `time_ago` is precomputed once in PHP using `$visit->visited_at->diffForHumans()` (no `Carbon::parse()` double-instantiation). All per-row presentation values are plain PHP arrays, not Eloquent model calls in Blade. - **Optimized DB query** — Selects only 12 needed columns (`id`, `visited_at`, `country`, `country_code`, `city`, `browser`, `operating_system`, `device_type`, `referer_host`, `referer_url`, `ip_address`, `is_qr_scan`, `selected_variant`) instead of `SELECT *`. Limit reduced to 25 rows. -- **Polling stops when hidden** — `wire:poll.5s.visible` pauses polling completely when the widget is scrolled out of viewport. +- **Live updates (v5.2.1+)** — SSE stream + `EventSource` replaced `wire:poll` for multi-admin live feed (see [v5.2.1](#v521)). #### Link Organization - **Folders** — Each short URL can be assigned to one folder. Clicking a folder in the sidebar navigates to a filtered link list showing only that folder's links. Link count displayed per folder. @@ -1448,106 +2173,14 @@ All migrations are compatible with **SQLite**, **MySQL**, and **PostgreSQL**: - **Per-key API scopes** — Each API key now supports either `links:read-only` (GET only) or `links:read-write` (full CRUD) scope. Read-only keys return `403 Forbidden` on any write attempt. - **Per-key rate limiting** — Each API key can have its own individual rate limit (requests per minute), independent of the global route throttle. Configure in **Settings → API & Webhooks → Developer API Keys**. -### v4.0.0 -- **Custom Domains Branding** — Let users connect branded custom domains with real-time CNAME/A record DNS verification and automatic prefix-free root routing. -- **Cache Leak Fix** — Solved a critical caching bug where single-use links could be visited multiple times during the cache TTL by passing the host-specific key suffix to cache invalidations. -- **Aggregator Performance Tuning** — Restructured daily aggregation SQL queries to filter only active URL IDs, leveraging composite indexes and running native DB aggregations. -- **Logo Upload Authorization** — Secured the logo upload API with resource-level permission and policy checks. -- **DB-Free Registration Phase** — Deferred settings overrides to the service provider boot phase to prevent database errors during offline configuration caching. -- **Conditional Redirection Fallback** — Added configurations to optionally disable global fallback route registration. - -### v3.5.0 -- **User Attribution** — Short URLs now record the creator. Their avatar and hover card (name + email) are displayed in the list table. -- **Relative Date Badges** — Creation dates show compact relative strings (`2h`, `5d`, `3mo`). Hover to see the precise date and timezone. -- **Keyboard Shortcuts** — Hover any table row and press `E` (edit), `Q` (QR code), `I` (share), `S` (statistics), or `X` (delete). -- **Unified Action Dropdown** — All per-row actions consolidated under a single 3-dot button with keyboard shortcut badges. -- **Row Click → Statistics** — Clicking a table row navigates directly to the link's Statistics page. -- **A/B Split Testing** — Weighted traffic rotation in root-level links and nested targeting rules, with drag-based sliders and a one-click "Balance weights" action. -- **Variant Analytics** — Tracks which A/B variant was served and shows a "Variant Clicks Distribution" chart in the analytics dashboard. -- **Query Optimization** — Composite DB index on the visits table; queries filter by indexed `country_code`; Eloquent hydration bypassed with `toBase()->get()` for large sets. -- **GDPR IP Anonymization** — Opt-in IP masking (IPv4/IPv6) with SHA-256 hashing for unique visitor identification, managed via Settings. -- **Google Safe Browsing in REST API** — Safety checks now apply to all incoming URLs in the API (destination, A/B variants, targeting rules). -- **Settings URL Cleanup** — Tab query parameters are now human-readable (e.g. `?tab=qr` instead of `?tab=qr-defaults::data::tab`). -- **Analytics Enhancements** — QR Scan Conversion Rate card with period-over-period delta, browser/OS version subtexts, A/B click-share vs. weight comparison. - -### v3.3.0 -- **Advanced Multi-Filter Targeting Engine** — Replaced the legacy single-strategy selection panel with a highly flexible rule engine supporting custom `AND` / `OR` logic matching. -- **Granular Filter Categories** — Added support for filtering by devices (Desktop, Mobile, Tablet), platform operating systems (Windows, macOS, Linux, iOS, Android, Fire OS), countries (multiselect with search), and browser languages (multiselect with search). -- **Client-Side Flag Icons** — Integrated high-quality country flag icons dynamically loaded from a trusted CDN (`flagcdn.com`) inside both the Filament form targeting dropdown and the analytics country statistics widget. -- **Redirection Performance Boost** — Bypassed full user agent parsing (which involves regular expression scanning for versions) during redirections by introducing specialized fast-path `getDeviceType` and `getOs` helper functions. -- **Dynamic Legacy Data Adapter** — Added automatic on-the-fly hydration and upgrade of legacy database records to the new rule format when loaded in the Filament form. - -### v3.2.0 -- **Expanded Developer REST API** — Added new endpoints to inspect single short URLs (`GET /api/short-url/links/{idOrKey}`), fetch real-time click metrics (`GET /api/short-url/links/{idOrKey}/stats`), and fully update links programmatically (`PUT/PATCH /api/short-url/links/{idOrKey}`). Enabled flexible lookup dynamically by ID or URL key. -- **REST API Throttling & Rate Limiting** — Configured built-in route middleware to rate limit the Developer REST API to 60 requests per minute (`throttle:60,1`) to protect from abuse. -- **Unified Validation System** — Cleaned up and unified API and admin panel form validation, supporting advanced fields like granular logging parameters (`track_visits`, `track_browser_language`, etc.), custom GA4 tracking IDs (`ga_tracking_id`), and auto-open deep linking options (`auto_open_app_mobile`). -- **Media Controller Separation** — Refactored internal logo uploads and serving routes out of the public REST API controller into a dedicated `ShortUrlLogoController` (Single Responsibility compliance). -- **Alpine.js Webhook Payload Preview** — Replaced the Filament package CodeEditor component with a custom, high-reliability dark-mode HTML/CSS component featuring live code highlighting and copy-to-clipboard functionality powered by Alpine.js. - -### v3.1.0 -- **Database-Backed & Cached Settings** — Relocated user configuration from local JSON files to the database (`short_url_settings` table) with automatic caching and zero-downtime migration of legacy settings. -- **High-Performance Aggregations** — Completely refactored the statistics aggregator command to run optimized GROUP BY queries directly in the database, reducing memory usage (OOM protection) to near zero. -- **Secure Hashed API Keys** — API keys are now stored securely as SHA-256 hashes, verified using constant-time comparisons (`hash_equals()`), and displayed only once upon generation. -- **HMAC Signed Webhooks** — Webhook payloads are now optionally signed with a configured secret key and verified via the `X-ShortUrl-Signature` header. -- **Privacy-Safe GA4 Client IDs** — Replaced random UUIDs with deterministic client IDs based on hashed visitor IP and User Agent, ensuring session integrity in Google Analytics without storing raw visitor data. -- **Browser Cache Prevention for Limited Links** — Force temporary `302` redirects automatically for single-use, max-visit, or expiring links to prevent browsers from caching redirects and bypassing tracking logic. -- **Proxy Detection Optimization** — Implemented an aggressive 800ms timeout for external proxy checkers and reduced cache time for transient rate-limit failures to 60 seconds. - -### v3.0.0 -- **Native App Linking (Mobile Auto-Open)** — Automatically match and redirect mobile visitors directly inside 24+ native mobile apps (such as WhatsApp, YouTube, TikTok, Instagram, Spotify, etc.) using custom schemes, complete with a glassmorphic redirect page and a live interactive matching preview widget. -- **Global Deep Linking (Universal Links & App Links)** — Easily serve iOS `apple-app-site-association` and Android `.well-known/assetlinks.json` configuration files directly from your root domain to support OS-level native integration (disabled by default, managed via Settings). -- **Central Retargeting Pixel Registry** — Introduced a premium Many-to-Many pixel management registry. Define pixels centrally (Meta Pixel, Google Tag, LinkedIn Insight, TikTok Pixel, Pinterest Tag) and easily associate them with short links via the Filament panel or the REST API. -- **Standalone Settings Page** — Relocated the Settings interface from a resource header sub-action to a standalone sidebar navigation page under the default plugin group. -- **Retargeting Pixel API** — The REST API now fully exposes the `pixels` relationship array parameter for registering and linking retargeting pixels programmatically. -- **Enhanced Browser Language Redirection** — Robust double-pass language targeting logic matching exact locales first (e.g. `en-US`, `zh-CN`) and falling back to base language codes (e.g. `en`, `zh`). -- **Custom Branded Expiry Pages** — Replaced raw 410 HTTP errors with a beautiful, fully localized, dark-mode compatible HTML expiry page displaying the Site Name, expired link details, and a homepage button. - -### v2.0.0 -- **Interactive QR Code Designer Branding Logo** — Upload custom brand logos inside the QR designer canvas in Filament. Configure logo sizing, margins, shapes (square/circle), and toggle dot backing removal to prevent dots overlapping with the logo. -- **Dedicated QR Code Scan Tracking** — Differentiates visitor clicks from physical QR code scans by dynamically appending source tags (`?source=qr`). Added a new database tracking column (`is_qr_scan` on visits, `qr_scans` on short URLs, and `qr_visits_count` on daily stats). Displays a dedicated scans counter badge in the Filament list table. -- **Browser Language Detection & Statistics** — Captures visitor browser preferred language headers (`browser_language` field) and aggregates them into the daily stats table. Displays a new "Top Languages" widget breakdown in the link statistics dashboard. -- **High-Traffic Performance Safeguards & Robust Rollbacks** — Atomic database transactions for buffered counter updates with fail-safe rollback that restores cache values in case of DB connection failures. Prevents N+1 queries by preloading request-wide counters in a single batch cache lookup. -- **Support for Empty Route Prefix (Root-Level URLs)** — Enhanced `getShortUrl()` to support empty route prefixes without generating double slashes (e.g. `domain.com/abc123` instead of `domain.com//abc123`). - -### v1.7.0 -- **Role-based Settings Access Control** — New `authorizeSettingsUsing(Closure)` method on the plugin to restrict who can access the Settings page. Supports any callable returning a `bool`. Also auto-detects a `manageSettings` method on a registered `ShortUrl` policy. The Settings button in the table header is hidden automatically when access is denied. - -### v1.6.0 -- **Google Safe Browsing Integration** — Automatic safety checks against Google's API during link creation or modification. Includes bypass settings, asynchronous checking option, and alert badges. -- **VPN / Proxy / Bot Filtering** — Detect and filter out VPN/proxy traffic and Tor nodes using external proxy detection APIs to keep traffic analytics clean. -- **Visitor World Map Widget** — Live interactive SVG world map showing clicks distribution per country, custom intensity highlighting, and hover details. - -### v1.5.1 -- **REST API On/Off Toggle** — Enable or disable the entire developer REST API from Settings → API & Webhooks without touching code. Returns `503 Service Unavailable` when disabled. - -### v1.5.0 -- **Social Retargeting Pixels** — Attach Meta Pixel, Google Tag (GA4/Ads), and LinkedIn Insight Tag to any short URL. A premium glassmorphic interstitial page executes pixel scripts in the visitor's browser before forwarding them to the destination. Enables building remarketing audiences even on external domains. -- **Developer REST API** — Full `GET /api/short-url/links`, `POST /api/short-url/links`, and `DELETE /api/short-url/links/{id}` endpoints with API Key authentication (managed via Settings UI). Supports creating links with all available attributes including pixels and webhooks. -- **Webhook System** — Real-time HTTP POST notifications on `visited`, `created`, `expired`, and `limit_reached` events. Configurable per-link or globally. Dispatched asynchronously via `SendWebhookJob` with 3-attempt retry policy and 10-second backoff. -- **Settings: API & Webhooks Tab** — New settings tab to manage global webhook URL, monitored event types, and developer API key management with name labels and per-key activation toggles. - -### v1.4.0 -- **Validity Date Ranges (From-To)** — Set activation dates (`activated_at` and `expires_at`) to control exactly when a short link is active. -- **Custom Visit Limit Counters** — Define a custom maximum visit limit (`max_visits`) after which a link automatically expires (e.g., active for 3 hits). -- **Custom Expiration Fallbacks** — Redirect expired/inactive visitors to a custom `expiration_redirect_url` rather than showing a static 410 Gone error page. -- **Fluent Builder APIs** — Fluent method additions (`activatedAt()`, `deactivatedAt()`, `maxVisits()`, `expirationRedirectUrl()`) in the developer query builder. - -### v1.3.0 -- **Automatic Scheduler Registration** — Zero-configuration task registration within the ServiceProvider booted phase (dynamically honors Settings toggles). -- **Interactive Settings Validators** — Adds real-time "Test connection" verify action for GA4 Measurement Protocol and "Verify file" action for MaxMind database paths. -- **Robust Table Row Copy Action** — High-reliability, conflict-free click-to-copy in table rows with built-in fallback helper for non-HTTPS (secure context) browser environments. - -### v1.2.0 -- **Password-protected links** — Session-based unlock flow with a styled prompt page. -- **Redirect warning pages** — Interstitial security screen before external redirects. -- **Smart targeting** — Device-based, Country/Geo-based, and A/B weighted rotation rules per link. -- **Rate limiting** — Configurable per-IP redirect throttling with 429 responses. -- **Daily stats aggregation** — Nightly `short-url:aggregate-and-prune` command for scalable log management. -- **Extended Settings GUI** — New "Performance & Security" tab for aggregation and rate limiting configuration. -- **Polish translations** — Full `pl` locale support for all new features. +Older releases (v1.2.0–v4.0.0) are documented in **[CHANGELOG.md](CHANGELOG.md)**. --- ## License -Custom Source-Available License. Please see the [LICENSE](LICENSE) file for the full text. +**Source-Available & Dual-Commercial License — version 1.3.** See the [LICENSE](LICENSE) file for the full text. + +Permitted free use covers internal/personal use and single-client freelance deployment within defined limits. Commercial distribution, bundling, high-volume use (>1M qualified events/month), software transfer to third parties, and competing products require a separate written Commercial License from the Copyright Holder. + +Section 9 of the license excludes liability of the Copyright Holder toward licensees (B2B) and does not limit enforcement against license violators. diff --git a/art/diagrams/01-system-overview.svg b/art/diagrams/01-system-overview.svg new file mode 100644 index 0000000..cd6a010 --- /dev/null +++ b/art/diagrams/01-system-overview.svg @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + System overview + Filament panel, public routes, core services, persistence + + + + + + Filament admin + Panel layer + ShortUrlResource + Settings, domains, QR + Analytics and live feed + scope_links_to_user + + + + + + Public HTTP + Edge of Laravel app + GET /s/[key] + GET /s-auth/[key] + GET /api/short-url/* + /[key] on custom domain + STATELESS + + + + + + Package core + Runtime services + RedirectHandler + TrackShortUrlVisitJob + SendWebhookJob (HMAC) + HasStats and rollups + + + + + + Persistence layer + short_urls, visits, daily_stats, cache, optional Redis buffer + SQL DB + Redis + Safe Browsing + Link cache + + + + + Self-hosted Laravel plugin, not edge SaaS + + diff --git a/art/diagrams/02-redirect-lifecycle.svg b/art/diagrams/02-redirect-lifecycle.svg new file mode 100644 index 0000000..216dd43 --- /dev/null +++ b/art/diagrams/02-redirect-lifecycle.svg @@ -0,0 +1,109 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Redirect lifecycle + GET /s/[key] from click to destination + + + + + + Visitor click + Start of request + + + + + + Throttle only + No web middleware, no session cookie + + + + + + findByKey(host) + Cache hit or single DB resolve + + + + + + Safe Browsing cache + isSafeCached(destination) when enabled + + + + + + max_visits lock + lockForUpdate plus isActive() + Response path + + + + + Password + /s-auth/[key] + web + + + + + + Interstitial + warning, pixels, cloak, OG + + + + + + Simple 302 + TrackJob then redirect + + + + + TrackShortUrlVisitJob: GeoIP, VPN, visit, webhook, GA4 (sync default or async queue) + + diff --git a/art/diagrams/03-stateless-vs-stateful.svg b/art/diagrams/03-stateless-vs-stateful.svg new file mode 100644 index 0000000..1e5a2e0 --- /dev/null +++ b/art/diagrams/03-stateless-vs-stateful.svg @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Stateless vs stateful routes + Lean clicks vs session-backed password unlock + + + + + + Every redirect click + STATELESS + GET /s/[key] + GET /[key] on custom domain + Middleware: throttle:120,1 + No cookies, no CSRF + Fixed in v5.2 + + + + + + Password unlock only + SESSION + GET /s-auth/[key] + GET /auth/[key] on custom domain + Middleware: web + throttle + Unlock remembered per session + By design + + + + + + Old audit: web on /s/[key] sent cookies on every click. Republish config if middleware still includes web. + + diff --git a/art/diagrams/04-custom-domain-routing.svg b/art/diagrams/04-custom-domain-routing.svg new file mode 100644 index 0000000..e4f2f64 --- /dev/null +++ b/art/diagrams/04-custom-domain-routing.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom domains and domain_scope_id + Same slug on different domains without collision + + + + + + Default app domain + app.test/s/promo + domain_scope_id = 0 + + + + + + Verified custom domain + links.brand.com/promo + scope = custom_domain_id + + + + + + Composite unique: (url_key, domain_scope_id) + GET /links/exists?url_key=promo&custom_domain_id=3 + + + + + + Ownership enforcement + Filament domains scoped by user_id + API: CustomDomainValidator blocks foreign domain IDs + Fixed: exists scope + ownership (v5.2) + + diff --git a/art/diagrams/05-targeting-and-ab.svg b/art/diagrams/05-targeting-and-ab.svg new file mode 100644 index 0000000..6778d15 --- /dev/null +++ b/art/diagrams/05-targeting-and-ab.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Smart targeting and A/B split + RedirectUrlResolver evaluates rules top to bottom + + + + + + GET /s/[key] + + + + + + RedirectUrlResolver + UA, Geo, language, rules + First matching rule? + + + + + Rule matched + Use rule URL + Nested A/B in rule + + + + + + Root A/B + Weighted 2-5 URLs + Variant on visit row + + + + + + No match + destination_url + UTM merge + query forward + + diff --git a/art/diagrams/06-webhooks-and-security.svg b/art/diagrams/06-webhooks-and-security.svg new file mode 100644 index 0000000..8e0e2a7 --- /dev/null +++ b/art/diagrams/06-webhooks-and-security.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webhooks and HMAC signing + Global events, per-link URL override, SSRF-safe dispatch + + + + + + Global webhook + Settings: URL, enabled, events + visited | created | expired | limit + Signing secret required (v5.2) + Events are global, not per-link + + + + + + Per-link webhook URL + short_urls.webhook_url override + SSRF re-check in SendWebhookJob + allow_redirects=false + + + + + + SendWebhookJob + Header: X-Short-Url-Signature sha256=... (HMAC body) + + + + + + Audit fixes (v5.2) + Before: optional secret allowed unsigned webhooks + After: Settings rejects enable without secret + Safe Browsing on save + redirect | lock_url_key server-side + + diff --git a/art/diagrams/07-stats-and-queue.svg b/art/diagrams/07-stats-and-queue.svg new file mode 100644 index 0000000..37a9128 --- /dev/null +++ b/art/diagrams/07-stats-and-queue.svg @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Stats pipeline and queue modes + Visits, aggregation cron, panel vs API stats gap +Visit tracking + + + + + Redirect + + + + + + TrackJob + + + + + + visits table + + + + + + Buffer + + + + + + sync (default) + Job runs inside redirect + Works without queue worker + GeoIP, webhook, GA4 add latency + + + + + + redis / database + 302 sent first + Requires queue:work + Settings - General + Aggregation (schedule:run) + + + + + Raw visits 90d + + + + + + AggregateAndPrune + + + + + + daily_stats + + + + + + Filament: FilteredStatsCollector + cross-dimensional rollups + API stats: date_from / date_to only (cross-filters = open gap) + Long-range reports use rollups after raw prune + + diff --git a/art/diagrams/08-audit-trust-summary.svg b/art/diagrams/08-audit-trust-summary.svg new file mode 100644 index 0000000..c9bfb79 --- /dev/null +++ b/art/diagrams/08-audit-trust-summary.svg @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + v5.2 audit trust summary + Promise vs old reality vs current status + + + + + + Audit promise + Old reality + v5.2 status + + + + + + Sub-15ms / stateless + web on /s/[key] + FIXED: throttle only + + + + + + Multi-tenant panel + All admins see all links + FIXED: LinkUserScope + + + + + + API exists + Ignored domain scope + FIXED: domain_scope_id + + + + + + Domain ownership + Foreign domain ID + FIXED: CustomDomainValidator + + + + + + Safe Browsing + Save only + FIXED: redirect cache + + + + + + Webhook HMAC + Optional secret + FIXED: required when on + + + + + + lock_url_key + UI disabled only + FIXED: LockedUrlKeyGuard + + + + + + Per-link webhook events + Documented per-link + OPEN: global list + + + + + + Default queue async + README vs sync + BY DESIGN: sync default + + + + + + Dub parity table + Overstated + FIXED: scoped compare + + + + + + Product boundary: Laravel Filament plugin, not Dub clone + Open: OpenAPI, API stats filters, workspaces, conversion, load tests + + diff --git a/art/diagrams/generate-glass-diagrams.py b/art/diagrams/generate-glass-diagrams.py new file mode 100644 index 0000000..9840261 --- /dev/null +++ b/art/diagrams/generate-glass-diagrams.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 +"""Generate liquid-glass architecture SVG diagrams for README.""" + +from pathlib import Path + +OUT = Path(__file__).parent + +GLASS_HEAD = """ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {title} + {subtitle} +""" + +GLASS_CARD = """ + + + + {accent}{content} + """ + +ACCENT_TOP = '\n ' + +FOOT = """ + +""" + + +def card(x, y, w, h, content, rx=26, accent=None): + accent_html = "" + if accent: + accent_html = ACCENT_TOP.format(x=x, y=y, w=w, color=accent) + return GLASS_CARD.format(x=x, y=y, w=w, h=h, rx=rx, accent=accent_html, content=content) + + +def t(x, y, text, size=12, weight="400", fill="#e2e8f0", anchor="start", mono=False): + fam = "ui-monospace,Menlo,monospace" if mono else "system-ui,-apple-system,Segoe UI,sans-serif" + w = f' font-weight="{weight}"' if weight != "400" else "" + return f'{text}' + + +def line(x1, y1, x2, y2): + return f'' + + +def pill(x, y, w, h, text, stroke="#ffffff"): + return f'{text}' + + +def diagram_01(): + h, oy = 600, 520 + body = GLASS_HEAD.format(height=h, label="System overview", title="System overview", subtitle="Filament panel, public routes, core services, persistence", orb_y=oy) + body += card(48, 100, 272, 220, "\n ".join([ + t(72, 136, "Filament admin", 16, "700"), + t(72, 158, "Panel layer", 11, fill="#94a3b8"), + t(72, 188, "ShortUrlResource", 12), + t(72, 210, "Settings, domains, QR", 12), + t(72, 232, "Analytics and live feed", 12), + pill(72, 258, 170, 24, "scope_links_to_user"), + ]), accent="#c084fc") + body += card(344, 100, 272, 220, "\n ".join([ + t(368, 136, "Public HTTP", 16, "700"), + t(368, 158, "Edge of Laravel app", 11, fill="#94a3b8"), + t(368, 188, "GET /s/[key]", 11, mono=True, fill="#bae6fd"), + t(368, 210, "GET /s-auth/[key]", 11, mono=True, fill="#bae6fd"), + t(368, 232, "GET /api/short-url/*", 11, mono=True, fill="#bae6fd"), + t(368, 254, "/[key] on custom domain", 11, mono=True, fill="#bae6fd"), + pill(368, 278, 92, 22, "STATELESS", stroke="#38bdf8"), + ]), accent="#38bdf8") + body += card(640, 100, 272, 220, "\n ".join([ + t(664, 136, "Package core", 16, "700"), + t(664, 158, "Runtime services", 11, fill="#94a3b8"), + t(664, 188, "RedirectHandler", 12), + t(664, 210, "TrackShortUrlVisitJob", 12), + t(664, 232, "SendWebhookJob (HMAC)", 12), + t(664, 254, "HasStats and rollups", 12), + ]), accent="#2dd4bf") + body += line(184, 320, 184, 352) + line(480, 320, 480, 352) + line(776, 320, 776, 352) + body += card(120, 360, 720, 130, "\n ".join([ + t(480, 396, "Persistence layer", 17, "700", anchor="middle"), + t(480, 422, "short_urls, visits, daily_stats, cache, optional Redis buffer", 12, anchor="middle", fill="#94a3b8"), + pill(200, 448, 130, 24, "SQL DB"), + pill(350, 448, 110, 24, "Redis"), + pill(480, 448, 130, 24, "Safe Browsing"), + pill(630, 448, 110, 24, "Link cache"), + ]), accent="#fb923c", rx=28) + body += card(48, 512, 864, 56, t(480, 546, "Self-hosted Laravel plugin, not edge SaaS", 13, anchor="middle", fill="#e9d5ff"), rx=20) + return body + FOOT + + +def diagram_02(): + h, oy = 680, 600 + body = GLASS_HEAD.format(height=h, label="Redirect lifecycle", title="Redirect lifecycle", subtitle="GET /s/[key] from click to destination", orb_y=oy) + steps = [ + ("Visitor click", "Start of request", "#818cf8"), + ("Throttle only", "No web middleware, no session cookie", "#fbbf24"), + ("findByKey(host)", "Cache hit or single DB resolve", "#a78bfa"), + ("Safe Browsing cache", "isSafeCached(destination) when enabled", "#f87171"), + ("max_visits lock", "lockForUpdate plus isActive()", "#34d399"), + ] + y = 96 + for i, (title, sub, color) in enumerate(steps): + body += card(280, y, 400, 54, "\n ".join([ + t(480, y + 24, title, 13, "600", anchor="middle"), + t(480, y + 42, sub, 11, anchor="middle", fill="#94a3b8"), + ]), accent=color, rx=18) + if i < len(steps) - 1: + body += line(480, y + 54, 480, y + 72) + y += 72 + body += line(480, y, 480, y + 20) + body += ''.format(y + 20, y + 58) + body += t(480, y + 48, "Response path", 12, "600", anchor="middle") + y2 = y + 72 + branches = [ + (48, "Password", "/s-auth/[key] + web", "#fb923c"), + (344, "Interstitial", "warning, pixels, cloak, OG", "#facc15"), + (640, "Simple 302", "TrackJob then redirect", "#4ade80"), + ] + for x, title, sub, color in branches: + body += card(x, y2, 272, 88, "\n ".join([ + t(x + 136, y2 + 28, title, 13, "700", anchor="middle"), + t(x + 136, y2 + 48, sub, 10, anchor="middle", fill="#94a3b8"), + ]), accent=color, rx=20) + body += card(120, y2 + 108, 720, 48, t(480, y2 + 136, "TrackShortUrlVisitJob: GeoIP, VPN, visit, webhook, GA4 (sync default or async queue)", 11, anchor="middle", fill="#cbd5e1"), rx=16) + return body + FOOT + + +def diagram_03(): + h, oy = 460, 400 + body = GLASS_HEAD.format(height=h, label="Stateless vs stateful", title="Stateless vs stateful routes", subtitle="Lean clicks vs session-backed password unlock", orb_y=oy) + for x, title, badge, lines, accent in [ + (48, "Every redirect click", "STATELESS", ["GET /s/[key]", "GET /[key] on custom domain", "Middleware: throttle:120,1", "No cookies, no CSRF"], "#34d399"), + (496, "Password unlock only", "SESSION", ["GET /s-auth/[key]", "GET /auth/[key] on custom domain", "Middleware: web + throttle", "Unlock remembered per session"], "#60a5fa"), + ]: + content = [t(x + 28, 132, title, 17, "700"), pill(x + 28, 148, 88, 22, badge)] + cy = 188 + for ln in lines: + content.append(t(x + 28, cy, ln, 12, mono=("/" in ln))) + cy += 22 + content.append(pill(x + 80, cy + 8, 200, 24, "Fixed in v5.2" if x == 48 else "By design")) + body += card(x, 96, 416, 280, "\n ".join(content), accent=accent, rx=28) + body += card(48, 396, 864, 44, t(480, 424, "Old audit: web on /s/[key] sent cookies on every click. Republish config if middleware still includes web.", 11, anchor="middle", fill="#fecaca"), rx=16, accent="#f87171") + return body + FOOT + + +def diagram_04(): + h, oy = 540, 480 + body = GLASS_HEAD.format(height=h, label="Custom domain routing", title="Custom domains and domain_scope_id", subtitle="Same slug on different domains without collision", orb_y=oy) + body += card(64, 96, 380, 150, "\n ".join([ + t(254, 132, "Default app domain", 16, "700", anchor="middle"), + t(254, 160, "app.test/s/promo", 12, anchor="middle", mono=True, fill="#ddd6fe"), + pill(154, 178, 200, 26, "domain_scope_id = 0"), + ]), accent="#c084fc", rx=24) + body += card(516, 96, 380, 150, "\n ".join([ + t(706, 132, "Verified custom domain", 16, "700", anchor="middle"), + t(706, 160, "links.brand.com/promo", 12, anchor="middle", mono=True, fill="#bae6fd"), + pill(586, 178, 240, 26, "scope = custom_domain_id"), + ]), accent="#38bdf8", rx=24) + body += line(254, 246, 254, 276) + line(706, 246, 706, 276) + body += card(180, 276, 600, 62, "\n ".join([ + t(480, 304, "Composite unique: (url_key, domain_scope_id)", 13, "700", anchor="middle", fill="#a7f3d0"), + t(480, 324, "GET /links/exists?url_key=promo&custom_domain_id=3", 11, anchor="middle", mono=True, fill="#94a3b8"), + ]), accent="#34d399", rx=20) + body += card(80, 360, 800, 120, "\n ".join([ + t(480, 392, "Ownership enforcement", 16, "700", anchor="middle", fill="#fed7aa"), + t(480, 418, "Filament domains scoped by user_id", 12, anchor="middle"), + t(480, 442, "API: CustomDomainValidator blocks foreign domain IDs", 12, anchor="middle"), + pill(320, 458, 320, 24, "Fixed: exists scope + ownership (v5.2)"), + ]), accent="#fb923c", rx=24) + return body + FOOT + + +def diagram_05(): + h, oy = 460, 400 + body = GLASS_HEAD.format(height=h, label="Targeting and A/B", title="Smart targeting and A/B split", subtitle="RedirectUrlResolver evaluates rules top to bottom", orb_y=oy) + body += card(360, 92, 240, 46, t(480, 122, "GET /s/[key]", 13, "600", anchor="middle", mono=True), rx=23, accent="#a78bfa") + body += line(480, 138, 480, 158) + body += card(300, 158, 360, 56, "\n ".join([ + t(480, 184, "RedirectUrlResolver", 13, "600", anchor="middle"), + t(480, 204, "UA, Geo, language, rules", 11, anchor="middle", fill="#94a3b8"), + ]), rx=18, accent="#38bdf8") + body += line(480, 214, 480, 238) + body += '' + body += t(480, 262, "First matching rule?", 12, "600", anchor="middle") + body += line(400, 272, 160, 310) + line(560, 272, 800, 310) + for x, title, lines, color in [ + (48, "Rule matched", ["Use rule URL", "Nested A/B in rule"], "#4ade80"), + (344, "Root A/B", ["Weighted 2-5 URLs", "Variant on visit row"], "#facc15"), + (640, "No match", ["destination_url", "UTM merge + query forward"], "#f87171"), + ]: + body += card(x, 310, 272, 100, "\n ".join([t(x + 136, 338, title, 12, "700", anchor="middle")] + [t(x + 136, 358 + i * 18, ln, 10, anchor="middle", fill="#94a3b8") for i, ln in enumerate(lines)]), accent=color, rx=20) + return body + FOOT + + +def diagram_06(): + h, oy = 500, 440 + body = GLASS_HEAD.format(height=h, label="Webhooks and HMAC", title="Webhooks and HMAC signing", subtitle="Global events, per-link URL override, SSRF-safe dispatch", orb_y=oy) + body += card(60, 96, 360, 140, "\n ".join([ + t(240, 128, "Global webhook", 15, "700", anchor="middle"), + t(240, 152, "Settings: URL, enabled, events", 11, anchor="middle", fill="#94a3b8"), + t(240, 174, "visited | created | expired | limit", 10, anchor="middle", mono=True), + t(240, 196, "Signing secret required (v5.2)", 11, anchor="middle", fill="#ddd6fe"), + pill(120, 208, 240, 22, "Events are global, not per-link"), + ]), accent="#c084fc", rx=22) + body += card(540, 96, 360, 140, "\n ".join([ + t(720, 128, "Per-link webhook URL", 15, "700", anchor="middle"), + t(720, 152, "short_urls.webhook_url override", 11, anchor="middle", fill="#94a3b8"), + t(720, 174, "SSRF re-check in SendWebhookJob", 11, anchor="middle"), + t(720, 196, "allow_redirects=false", 10, anchor="middle", mono=True), + ]), accent="#38bdf8", rx=22) + body += line(480, 236, 480, 262) + body += card(200, 262, 560, 54, "\n ".join([ + t(480, 286, "SendWebhookJob", 14, "700", anchor="middle"), + t(480, 304, "Header: X-Short-Url-Signature sha256=... (HMAC body)", 10, anchor="middle", mono=True, fill="#94a3b8"), + ]), accent="#34d399", rx=18) + body += card(80, 336, 800, 120, "\n ".join([ + t(480, 368, "Audit fixes (v5.2)", 15, "700", anchor="middle", fill="#fed7aa"), + t(480, 394, "Before: optional secret allowed unsigned webhooks", 12, anchor="middle"), + t(480, 418, "After: Settings rejects enable without secret", 12, anchor="middle"), + t(480, 442, "Safe Browsing on save + redirect | lock_url_key server-side", 11, anchor="middle", fill="#94a3b8"), + ]), accent="#fb923c", rx=24) + return body + FOOT + + +def diagram_07(): + h, oy = 540, 480 + body = GLASS_HEAD.format(height=h, label="Stats and queue", title="Stats pipeline and queue modes", subtitle="Visits, aggregation cron, panel vs API stats gap", orb_y=oy) + body += t(480, 98, "Visit tracking", 15, "700", anchor="middle") + boxes = [(60, "Redirect"), (280, "TrackJob"), (540, "visits table"), (740, "Buffer")] + for i, (x, label) in enumerate(boxes): + body += card(x, 112, 160 if x < 740 else 120, 48, t(x + (80 if x < 740 else 60), 142, label, 11, "600", anchor="middle"), rx=16, accent="#818cf8" if i == 1 else "#38bdf8") + if i < len(boxes) - 1: + nx = boxes[i + 1][0] + body += line(x + (160 if x < 740 else 120), 136, nx, 136) + body += card(120, 180, 340, 110, "\n ".join([ + t(290, 210, "sync (default)", 15, "700", anchor="middle", fill="#6ee7b7"), + t(290, 234, "Job runs inside redirect", 11, anchor="middle", fill="#94a3b8"), + t(290, 256, "Works without queue worker", 11, anchor="middle", fill="#94a3b8"), + t(290, 278, "GeoIP, webhook, GA4 add latency", 10, anchor="middle", fill="#64748b"), + ]), accent="#34d399", rx=22) + body += card(500, 180, 340, 110, "\n ".join([ + t(670, 210, "redis / database", 15, "700", anchor="middle", fill="#93c5fd"), + t(670, 234, "302 sent first", 11, anchor="middle", fill="#94a3b8"), + t(670, 256, "Requires queue:work", 11, anchor="middle", fill="#94a3b8"), + t(670, 278, "Settings - General", 10, anchor="middle", fill="#64748b"), + ]), accent="#60a5fa", rx=22) + body += t(480, 318, "Aggregation (schedule:run)", 14, "700", anchor="middle") + body += card(140, 332, 200, 44, t(240, 360, "Raw visits 90d", 11, anchor="middle"), rx=14, accent="#f87171") + body += line(340, 354, 400, 354) + body += card(400, 332, 240, 44, t(520, 360, "AggregateAndPrune", 11, anchor="middle"), rx=14, accent="#fbbf24") + body += line(640, 354, 700, 354) + body += card(700, 332, 200, 44, t(800, 360, "daily_stats", 11, anchor="middle"), rx=14, accent="#4ade80") + body += card(80, 400, 800, 90, "\n ".join([ + t(480, 430, "Filament: FilteredStatsCollector + cross-dimensional rollups", 13, "600", anchor="middle"), + t(480, 454, "API stats: date_from / date_to only (cross-filters = open gap)", 11, anchor="middle", fill="#94a3b8"), + t(480, 476, "Long-range reports use rollups after raw prune", 10, anchor="middle", fill="#64748b"), + ]), accent="#818cf8", rx=24) + return body + FOOT + + +def diagram_08(): + h, oy = 560, 500 + body = GLASS_HEAD.format(height=h, label="Audit trust summary", title="v5.2 audit trust summary", subtitle="Promise vs old reality vs current status", orb_y=oy) + rows = [ + ("Sub-15ms / stateless", "web on /s/[key]", "FIXED: throttle only", "#34d399"), + ("Multi-tenant panel", "All admins see all links", "FIXED: LinkUserScope", "#34d399"), + ("API exists", "Ignored domain scope", "FIXED: domain_scope_id", "#34d399"), + ("Domain ownership", "Foreign domain ID", "FIXED: CustomDomainValidator", "#34d399"), + ("Safe Browsing", "Save only", "FIXED: redirect cache", "#34d399"), + ("Webhook HMAC", "Optional secret", "FIXED: required when on", "#34d399"), + ("lock_url_key", "UI disabled only", "FIXED: LockedUrlKeyGuard", "#34d399"), + ("Per-link webhook events", "Documented per-link", "OPEN: global list", "#fbbf24"), + ("Default queue async", "README vs sync", "BY DESIGN: sync default", "#fbbf24"), + ("Dub parity table", "Overstated", "FIXED: scoped compare", "#34d399"), + ] + y = 92 + body += card(48, y, 864, 36, "\n ".join([ + t(180, y + 24, "Audit promise", 12, "700", anchor="middle"), + t(440, y + 24, "Old reality", 12, "700", anchor="middle"), + t(730, y + 24, "v5.2 status", 12, "700", anchor="middle"), + ]), rx=14, accent="#ffffff") + y += 48 + for promise, old, status, color in rows: + body += card(48, y, 864, 34, "\n ".join([ + t(64, y + 22, promise, 11), + t(330, y + 22, old, 11, fill="#94a3b8"), + t(600, y + 22, status, 11, "600", fill=color), + ]), rx=10, accent=color if "FIXED" in status else "#fbbf24") + y += 40 + body += card(48, y + 8, 864, 72, "\n ".join([ + t(480, y + 36, "Product boundary: Laravel Filament plugin, not Dub clone", 13, "600", anchor="middle", fill="#e9d5ff"), + t(480, y + 58, "Open: OpenAPI, API stats filters, workspaces, conversion, load tests", 11, anchor="middle", fill="#94a3b8"), + ]), rx=20, accent="#818cf8") + return body + FOOT + + +DIAGRAMS = [ + ("01-system-overview.svg", diagram_01), + ("02-redirect-lifecycle.svg", diagram_02), + ("03-stateless-vs-stateful.svg", diagram_03), + ("04-custom-domain-routing.svg", diagram_04), + ("05-targeting-and-ab.svg", diagram_05), + ("06-webhooks-and-security.svg", diagram_06), + ("07-stats-and-queue.svg", diagram_07), + ("08-audit-trust-summary.svg", diagram_08), +] + +if __name__ == "__main__": + for name, fn in DIAGRAMS: + path = OUT / name + path.write_text(fn(), encoding="utf-8") + print(f"Wrote {name}") diff --git a/art/v3_1.png b/art/v3_1.png deleted file mode 100644 index 785a6b2539a05977869d4ddf5b8a4fe8c706fd84..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 246922 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

Yi`{@YsDz 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