From 0ecf956754723fe3d5e94d6f6f9dd773dc8e30b5 Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Thu, 29 Oct 2026 00:37:09 -0700
Subject: [PATCH] Keep analytics products reachable
Replace broad analytics-vendor blocking with reviewed collector endpoints, add exact-host ordinary rules with safe legacy handling, and require the matching extension build before publishing them. Harden signed catalog policy and make release-site dates deterministic.
Co-Authored-By: Claude Code
---
.github/workflows/ci.yml | 3 +-
.github/workflows/pages.yml | 2 +-
Flowlight.xcodeproj/project.pbxproj | 12 +-
Flowlight/Analysis/BuiltInRulePacks.swift | 67 ++++-
Flowlight/Analysis/RuleStore.swift | 2 +
Flowlight/Analysis/RuleTemplates.swift | 89 ++++---
.../Capture/ExtensionTrafficSource.swift | 14 ++
.../Inspection/InspectionController.swift | 2 +-
Flowlight/Resources/RulePacks/official.json | 228 +++++++++++++-----
Flowlight/UI/RuleEditor.swift | 20 +-
FlowlightExtension/IPCServer.swift | 4 +-
FlowlightExtension/Info.plist | 6 +-
FlowlightTests/ExtensionSourceTests.swift | 8 +
FlowlightTests/InspectionTests.swift | 10 +
FlowlightTests/RulePackCatalogTests.swift | 4 +-
.../RulePackCatalogUpdaterTests.swift | 2 +-
FlowlightTests/RulePackTests.swift | 43 ++++
FlowlightTests/RuleTemplateTests.swift | 35 ++-
FlowlightTests/RuleTests.swift | 95 +++++++-
README.md | 11 +-
Shared/Rule.swift | 96 +++++++-
docs/404.html | 2 +-
docs/DEVELOPMENT.md | 8 +-
docs/about/index.html | 2 +-
docs/de/about/index.html | 2 +-
docs/de/docs/index.html | 4 +-
docs/de/index.html | 6 +-
docs/de/privacy/index.html | 2 +-
docs/de/threat-model/index.html | 2 +-
docs/docs/index.html | 10 +-
docs/es/about/index.html | 2 +-
docs/es/docs/index.html | 4 +-
docs/es/index.html | 6 +-
docs/es/privacy/index.html | 2 +-
docs/es/threat-model/index.html | 2 +-
docs/fr/about/index.html | 2 +-
docs/fr/docs/index.html | 4 +-
docs/fr/index.html | 6 +-
docs/fr/privacy/index.html | 2 +-
docs/fr/threat-model/index.html | 2 +-
docs/index.html | 13 +-
docs/it/about/index.html | 2 +-
docs/it/docs/index.html | 4 +-
docs/it/index.html | 6 +-
docs/it/privacy/index.html | 2 +-
docs/it/threat-model/index.html | 2 +-
docs/ja/about/index.html | 2 +-
docs/ja/docs/index.html | 4 +-
docs/ja/index.html | 6 +-
docs/ja/privacy/index.html | 2 +-
docs/ja/threat-model/index.html | 2 +-
docs/ko/about/index.html | 2 +-
docs/ko/docs/index.html | 4 +-
docs/ko/index.html | 6 +-
docs/ko/privacy/index.html | 2 +-
docs/ko/threat-model/index.html | 2 +-
docs/llms-full.txt | 33 ++-
docs/llms.txt | 2 +-
docs/packs/v1/official.json | 228 +++++++++++++-----
docs/plugins/index.html | 2 +-
docs/privacy/index.html | 2 +-
docs/pt-PT/about/index.html | 2 +-
docs/pt-PT/docs/index.html | 4 +-
docs/pt-PT/index.html | 6 +-
docs/pt-PT/privacy/index.html | 2 +-
docs/pt-PT/threat-model/index.html | 2 +-
docs/releases/index.html | 17 +-
docs/threat-model/index.html | 2 +-
docs/zh-Hans/about/index.html | 2 +-
docs/zh-Hans/docs/index.html | 4 +-
docs/zh-Hans/index.html | 6 +-
docs/zh-Hans/privacy/index.html | 2 +-
docs/zh-Hans/threat-model/index.html | 2 +-
docs/zh-Hant/about/index.html | 2 +-
docs/zh-Hant/docs/index.html | 4 +-
docs/zh-Hant/index.html | 6 +-
docs/zh-Hant/privacy/index.html | 2 +-
docs/zh-Hant/threat-model/index.html | 2 +-
project.yml | 4 +-
rule-packs/official-source.json | 84 ++++---
scripts/build_site.py | 27 ++-
scripts/rule_pack_catalog.py | 90 +++++--
scripts/test-rule-pack-policy.sh | 48 ++++
site/packs/v1/official.json | 228 +++++++++++++-----
site/pages/docs.html | 6 +
site/pages/index.html | 7 +-
site/pages/releases.html | 15 +-
87 files changed, 1346 insertions(+), 381 deletions(-)
create mode 100755 scripts/test-rule-pack-policy.sh
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7e09b965..110d77c4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -178,10 +178,11 @@ jobs:
- name: Generate the project and reject drift
run: scripts/check_xcode_project.sh
- - name: Check bounded notarization retries and warning gate
+ - name: Check bounded notarization retries, catalog policy, and warning gate
if: matrix.xcode == 'newest'
run: |
scripts/test-notarize.sh
+ scripts/test-rule-pack-policy.sh
scripts/test-swift-warnings.sh
# This compiles a different product graph and activation path from the Developer ID build. The build clears
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
index 7ab2104d..ffbcd405 100644
--- a/.github/workflows/pages.yml
+++ b/.github/workflows/pages.yml
@@ -90,7 +90,7 @@ jobs:
curl --fail --silent --show-error --connect-timeout 10 --max-time 30 --max-filesize 8388608 \
--output "$PREVIOUS_CATALOG" "$LIVE_CATALOG_URL"
fi
- python3 -I scripts/rule_pack_catalog.py verify "$PREVIOUS_CATALOG" \
+ python3 -I scripts/rule_pack_catalog.py verify "$PREVIOUS_CATALOG" --historical \
--public-key Flowlight/Resources/RulePacks/official-rule-packs-v1-public.pem \
--expected-key-id flowlight-rule-packs-2026-01
python3 -I scripts/rule_pack_catalog.py compare-previous site/packs/v1/official.json \
diff --git a/Flowlight.xcodeproj/project.pbxproj b/Flowlight.xcodeproj/project.pbxproj
index a3943ffe..890f38a8 100644
--- a/Flowlight.xcodeproj/project.pbxproj
+++ b/Flowlight.xcodeproj/project.pbxproj
@@ -2184,7 +2184,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 57;
+ CURRENT_PROJECT_VERSION = 58;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
@@ -2199,7 +2199,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
- MARKETING_VERSION = 0.15.10;
+ MARKETING_VERSION = 0.15.11;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2245,7 +2245,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 57;
+ CURRENT_PROJECT_VERSION = 58;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = 38RJUJHKZS;
ENABLE_HARDENED_RUNTIME = YES;
@@ -2260,7 +2260,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
- MARKETING_VERSION = 0.15.10;
+ MARKETING_VERSION = 0.15.11;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2325,7 +2325,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
- CURRENT_PROJECT_VERSION = 57;
+ CURRENT_PROJECT_VERSION = 58;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
@@ -2346,7 +2346,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
- MARKETING_VERSION = 0.15.10;
+ MARKETING_VERSION = 0.15.11;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
diff --git a/Flowlight/Analysis/BuiltInRulePacks.swift b/Flowlight/Analysis/BuiltInRulePacks.swift
index 24627b2b..62c78390 100644
--- a/Flowlight/Analysis/BuiltInRulePacks.swift
+++ b/Flowlight/Analysis/BuiltInRulePacks.swift
@@ -19,11 +19,56 @@ enum BuiltInRulePacks {
static let tracking = definition(
id: "official.tracking",
+ version: "2",
+ generatedAt: 1_793_253_368,
title: "Cross-site tracking",
- summary: "Common analytics, session replay, and cross-site measurement hosts.",
- caveat: "Some apps use the same providers for feature flags. They may fall back to built-in defaults when a host is unavailable.",
- category: .tracking,
- hosts: RuleTemplate.analyticsHosts)
+ summary: "Dedicated analytics collection, session replay, and tracker bootstrap hosts.",
+ caveat: "Product dashboards stay reachable. Shared-host collection paths and first-party tracking proxies cannot be separated by hostname blocking.",
+ entries: [
+ entry(1, "api-eu.mixpanel.com"),
+ entry(2, "api-in.mixpanel.com"),
+ entry(3, "api-js.mixpanel.com"),
+ entry(4, "api.eu.amplitude.com"),
+ entry(5, "api.mixpanel.com"),
+ entry(6, "api.segment.io"),
+ entry(7, "api2.amplitude.com"),
+ entry(8, "browser-intake-ap1-datadoghq.com"),
+ entry(9, "browser-intake-ap2-datadoghq.com"),
+ entry(10, "browser-intake-datadoghq.com", match: .suffix),
+ entry(11, "browser-intake-datadoghq.eu"),
+ entry(12, "browser-intake-ddog-gov.com"),
+ entry(13, "browser-intake-uk1-datadoghq.com"),
+ entry(14, "browser-intake-us2-ddog-gov.com"),
+ entry(15, "browser-intake-us3-datadoghq.com"),
+ entry(16, "browser-intake-us5-datadoghq.com"),
+ entry(17, "c.eu.heap-api.com"),
+ entry(18, "c.us.heap-api.com"),
+ entry(19, "cdn.eu.heap-api.com"),
+ entry(20, "cdn.heapanalytics.com"),
+ entry(21, "cdn.matomo.cloud"),
+ entry(22, "cdn.mxpnl.com"),
+ entry(23, "cdn.us.heap-api.com"),
+ entry(24, "clarity.ms", match: .suffix),
+ entry(25, "edge.eu1.fullstory.com"),
+ entry(26, "edge.fullstory.com"),
+ entry(27, "eu-assets.i.posthog.com"),
+ entry(28, "eu.i.posthog.com"),
+ entry(29, "events.eu1.segmentapis.com"),
+ entry(30, "events.statsigapi.net"),
+ entry(31, "google-analytics.com", match: .suffix),
+ entry(32, "googletagmanager.com", match: .suffix),
+ entry(33, "lr-ingest.com", match: .suffix),
+ entry(34, "lr-ingest.io", match: .suffix),
+ entry(35, "lr-intake.com", match: .suffix),
+ entry(36, "lrkt-in.com", match: .suffix),
+ entry(37, "prodregistryv2.org", match: .suffix),
+ entry(38, "r.logrocket.com"),
+ entry(39, "rs.eu1.fullstory.com"),
+ entry(40, "rs.fullstory.com"),
+ entry(41, "services.fullstory.com"),
+ entry(42, "us-assets.i.posthog.com"),
+ entry(43, "us.i.posthog.com"),
+ ])
static let telemetry = definition(
id: "official.telemetry",
@@ -39,11 +84,23 @@ enum BuiltInRulePacks {
let entries = canonical.enumerated().map {
RulePackEntry(id: UInt32($0.offset + 1), host: $0.element, match: .suffix, category: category)
}
+ return definition(id: id, version: "1", generatedAt: 1_792_220_400,
+ title: title, summary: summary, caveat: caveat, entries: entries)
+ }
+
+ private static func definition(id: String, version: String, generatedAt: TimeInterval,
+ title: String, summary: String, caveat: String,
+ entries: [RulePackEntry]) -> RulePackDefinition {
let manifest = RulePackManifest(
- id: id, version: "1", generatedAt: Date(timeIntervalSince1970: 1_792_220_400),
+ id: id, version: version, generatedAt: Date(timeIntervalSince1970: generatedAt),
entryCount: entries.count, contentSHA256: RulePackValidator.digest(entries),
titleKey: title, summaryKey: summary, caveatKey: caveat,
source: "Flowlight curated built-in list", license: "GPL-3.0")
return RulePackDefinition(manifest: manifest, entries: entries)
}
+
+ private static func entry(_ id: UInt32, _ host: String,
+ match: RulePackEntry.Match = .exact) -> RulePackEntry {
+ RulePackEntry(id: id, host: host, match: match, category: .tracking)
+ }
}
diff --git a/Flowlight/Analysis/RuleStore.swift b/Flowlight/Analysis/RuleStore.swift
index c4cae245..4b67c275 100644
--- a/Flowlight/Analysis/RuleStore.swift
+++ b/Flowlight/Analysis/RuleStore.swift
@@ -69,7 +69,9 @@ final class RuleStore: ObservableObject {
func save(_ rule: Rule) {
var rule = rule
+ let enteredWildcard = rule.destination.trimmingCharacters(in: .whitespacesAndNewlines).hasPrefix("*.")
rule.destination = Self.cleaned(rule.destination)
+ if enteredWildcard { rule.destinationMatch = .suffix }
if let index = rules.firstIndex(where: { $0.id == rule.id }) {
rules[index] = rule
} else {
diff --git a/Flowlight/Analysis/RuleTemplates.swift b/Flowlight/Analysis/RuleTemplates.swift
index 6923d38d..67857ec6 100644
--- a/Flowlight/Analysis/RuleTemplates.swift
+++ b/Flowlight/Analysis/RuleTemplates.swift
@@ -210,8 +210,8 @@ extension RuleTemplate {
guard !destination.isEmpty else { return false }
let isAddress = AgentPolicy.isIPAddress(destination)
return rules().contains {
- AgentPolicy.matches($0.destination, host: isAddress ? "" : destination,
- ip: isAddress ? destination : "")
+ $0.destinationMatches(host: isAddress ? "" : destination,
+ ip: isAddress ? destination : "")
}
}
@@ -220,10 +220,13 @@ extension RuleTemplate {
/// One block per destination, all sharing a name so the Rules screen reads as a set rather than as nine
/// unrelated rules someone wrote on the same afternoon.
private static func blocks(_ destinations: [String], app: Subject, label: String,
- schedule: Rule.Schedule = Rule.Schedule()) -> [Rule] {
+ schedule: Rule.Schedule = Rule.Schedule(),
+ destinationMatch: Rule.DestinationMatch = .suffix) -> [Rule] {
destinations.map { destination in
- var rule = Rule(action: .block, app: app.app, destination: destination, origin: .preset)
+ var rule = Rule(action: .block, app: app.app, destination: destination,
+ destinationMatch: destinationMatch, origin: .preset)
rule.schedule = schedule
+ if label == "no product analytics" { rule.presetID = "telemetry.product-analytics" }
rule.name = app.hasApp ? "\(app.appName): \(label)" : label
return rule
}
@@ -570,12 +573,15 @@ extension RuleTemplate {
summary: "The usage-measurement services Mac apps and developer tools send events to. Refusing them "
+ "costs you nothing the app does for you.",
category: .telemetry, needs: .nothing,
- caveat: "Statsig serves feature flags from the same domains as its analytics, so an app that uses it "
- + "may fall back to its built-in defaults rather than simply measuring less. And some products "
- + "route analytics through their own domain, which is indistinguishable from the rest of what "
- + "they do and so can't be on a list like this at all.",
+ caveat: "Provider dashboards and shared feature-configuration hosts stay reachable. Collection paths "
+ + "that share a product hostname, plus first-party tracking proxies, are indistinguishable at the "
+ + "connection layer and cannot be on a hostname list like this.",
scopesToApp: true,
- build: { subject in blocks(analyticsHosts, app: subject, label: "no product analytics") }),
+ build: { subject in
+ blocks(analyticsHosts, app: subject, label: "no product analytics")
+ + blocks(analyticsExactHosts, app: subject, label: "no product analytics",
+ destinationMatch: .exact)
+ }),
RuleTemplate(
id: "telemetry.crash-reporting",
@@ -616,32 +622,55 @@ extension RuleTemplate {
build: { subject in blocks(attributionHosts, app: subject, label: "no install attribution") }),
]
- /// Statsig is the reason a list like this is worth having at all: five of its ingestion domains are named
- /// after nothing, so nobody scanning Live would ever pick them out.
+ /// Dedicated families where every subdomain is part of collection rather than the provider's product surface.
static let analyticsHosts = [
"google-analytics.com",
- "analytics.google.com",
"googletagmanager.com",
- "segment.io", // api.segment.io
- "segment.com",
- "segmentapis.com", // the EU workspaces
- "mixpanel.com",
- "mxpnl.com",
- "amplitude.com", // api2.amplitude.com is the ingest host
- "posthog.com",
- "heapanalytics.com",
- "fullstory.com",
- "lr-ingest.io", // LogRocket
- "matomo.cloud", // the hosted one; self-hosted is any host at all
- "clarity.ms",
"browser-intake-datadoghq.com",
- "statsig.com",
- "statsigapi.net",
- "featuregates.org",
- "featureassets.org",
+ "clarity.ms",
+ "lr-ingest.com",
+ "lr-ingest.io",
+ "lr-intake.com",
+ "lrkt-in.com",
"prodregistryv2.org",
- "assetsconfigcdn.org",
- "beyondwickedmapping.org",
+ ]
+
+ /// Reviewed collector and tracker-bootstrap names that cannot safely widen to their neighboring product hosts.
+ static let analyticsExactHosts = [
+ "api-eu.mixpanel.com",
+ "api-in.mixpanel.com",
+ "api-js.mixpanel.com",
+ "api.eu.amplitude.com",
+ "api.mixpanel.com",
+ "api.segment.io",
+ "api2.amplitude.com",
+ "browser-intake-ap1-datadoghq.com",
+ "browser-intake-ap2-datadoghq.com",
+ "browser-intake-datadoghq.eu",
+ "browser-intake-ddog-gov.com",
+ "browser-intake-uk1-datadoghq.com",
+ "browser-intake-us2-ddog-gov.com",
+ "browser-intake-us3-datadoghq.com",
+ "browser-intake-us5-datadoghq.com",
+ "c.eu.heap-api.com",
+ "c.us.heap-api.com",
+ "cdn.eu.heap-api.com",
+ "cdn.heapanalytics.com",
+ "cdn.matomo.cloud",
+ "cdn.mxpnl.com",
+ "cdn.us.heap-api.com",
+ "edge.eu1.fullstory.com",
+ "edge.fullstory.com",
+ "eu-assets.i.posthog.com",
+ "eu.i.posthog.com",
+ "events.eu1.segmentapis.com",
+ "events.statsigapi.net",
+ "r.logrocket.com",
+ "rs.eu1.fullstory.com",
+ "rs.fullstory.com",
+ "services.fullstory.com",
+ "us-assets.i.posthog.com",
+ "us.i.posthog.com",
]
static let crashReportingHosts = [
diff --git a/Flowlight/Capture/ExtensionTrafficSource.swift b/Flowlight/Capture/ExtensionTrafficSource.swift
index 97b37639..3cfb199f 100644
--- a/Flowlight/Capture/ExtensionTrafficSource.swift
+++ b/Flowlight/Capture/ExtensionTrafficSource.swift
@@ -30,6 +30,9 @@ struct RulePackPushGate {
/// Receives per-second batches from the system extension over XPC.
final class ExtensionTrafficSource: NSObject, TrafficSource, FlowlightAppXPC, @unchecked Sendable {
+ static func acceptsRegisteredVersion(_ actual: String, expected: String) -> Bool {
+ !expected.isEmpty && actual == expected
+ }
var displayName: String { L("Network Extension") }
private var connection: NSXPCConnection?
private var sink: (([TrafficBatch]) -> Void)?
@@ -136,6 +139,17 @@ final class ExtensionTrafficSource: NSObject, TrafficSource, FlowlightAppXPC, @u
// A refusal used to end here, with a line of status text and no timer: connected, registered at
// nothing, and never going to try again. It is a failure like any other, so it goes on the ladder.
guard ok else { self.scheduleReconnect(L("Extension refused registration"), from: connection); return }
+ // Exact ordinary rules are encoded as an additive Rule field. An older filter would ignore that field
+ // and widen a collector host to its whole product domain, so publish nothing until the full embedded
+ // version/build is running. Registration reset the old filter to fail-open before this check.
+ let expected = ExtensionManager.expectedExtensionVersion
+ guard Self.acceptsRegisteredVersion(version, expected: expected) else {
+ self.status?(L("The installed filter extension is %@ and this app is %@ — macOS is replacing it. Capture resumes once it has.",
+ version, expected))
+ self.onRepairRequested?()
+ self.scheduleReconnect(L("Trying the filter extension again…"), from: connection)
+ return
+ }
self.recovery.succeeded()
self.status?(self.sawTraffic ? L("Connected to filter extension %@", version)
: L("Connected to filter extension %@ — no traffic from it yet", version))
diff --git a/Flowlight/Inspection/InspectionController.swift b/Flowlight/Inspection/InspectionController.swift
index 47734054..2e34efcf 100644
--- a/Flowlight/Inspection/InspectionController.swift
+++ b/Flowlight/Inspection/InspectionController.swift
@@ -189,7 +189,7 @@ final class InspectionController: ObservableObject {
let destination = Self.proxyDestination(host)
var refusals = (self?.requestRules() ?? []).filter {
$0.action == .block && ($0.destination.isEmpty
- || AgentPolicy.matches($0.destination, host: destination.host, ip: destination.ip))
+ || $0.destinationMatches(host: destination.host, ip: destination.ip))
}
if refusals.contains(where: { !$0.app.isEmpty }) {
// Only then is it worth asking who opened this connection: naming an app is the uncommon case,
diff --git a/Flowlight/Resources/RulePacks/official.json b/Flowlight/Resources/RulePacks/official.json
index ea4a1c66..de463d5f 100644
--- a/Flowlight/Resources/RulePacks/official.json
+++ b/Flowlight/Resources/RulePacks/official.json
@@ -4,9 +4,9 @@
"catalogID": "flowlight.official",
"channel": "official",
"sourceURL": "https://flowlight.xinbetween.com/packs/v1/official.json",
- "sequence": 1,
- "issuedAt": 1792368000,
- "expiresAt": 1823904000,
+ "sequence": 2,
+ "issuedAt": 1793253368,
+ "expiresAt": 1824789368,
"publisher": "Flowlight",
"packs": [
{
@@ -314,15 +314,15 @@
{
"manifest": {
"id": "official.tracking",
- "version": "1",
- "generatedAt": 1792220400,
- "entryCount": 23,
- "contentSHA256": "57bdd11c751ebc24a785d429afd7d6a9282bb93e4cbe42f6725bd387fef64386",
+ "version": "2",
+ "generatedAt": 1793253368,
+ "entryCount": 43,
+ "contentSHA256": "2fa7a05ad9aba7c8ffe5fea8f7f33ca63e32a5759a4f8efe976497d6132d47b9",
"minimumAppVersion": "0.15.0",
"minimumExtensionProtocol": 1,
"titleKey": "Cross-site tracking",
- "summaryKey": "Common analytics, session replay, and cross-site measurement hosts.",
- "caveatKey": "Some apps use the same providers for feature flags. They may fall back to built-in defaults when a host is unavailable.",
+ "summaryKey": "Dedicated analytics collection, session replay, and tracker bootstrap hosts.",
+ "caveatKey": "Product dashboards stay reachable. Shared-host collection paths and first-party tracking proxies cannot be separated by hostname blocking.",
"source": "Flowlight curated built-in list",
"license": "GPL-3.0",
"format": 1
@@ -330,141 +330,261 @@
"entries": [
{
"id": 1,
- "host": "amplitude.com",
- "match": "suffix",
+ "host": "api-eu.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 2,
- "host": "analytics.google.com",
- "match": "suffix",
+ "host": "api-in.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 3,
- "host": "assetsconfigcdn.org",
- "match": "suffix",
+ "host": "api-js.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 4,
- "host": "beyondwickedmapping.org",
- "match": "suffix",
+ "host": "api.eu.amplitude.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 5,
- "host": "browser-intake-datadoghq.com",
- "match": "suffix",
+ "host": "api.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 6,
- "host": "clarity.ms",
- "match": "suffix",
+ "host": "api.segment.io",
+ "match": "exact",
"category": "tracking"
},
{
"id": 7,
- "host": "featureassets.org",
- "match": "suffix",
+ "host": "api2.amplitude.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 8,
- "host": "featuregates.org",
- "match": "suffix",
+ "host": "browser-intake-ap1-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 9,
- "host": "fullstory.com",
- "match": "suffix",
+ "host": "browser-intake-ap2-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 10,
- "host": "google-analytics.com",
+ "host": "browser-intake-datadoghq.com",
"match": "suffix",
"category": "tracking"
},
{
"id": 11,
- "host": "googletagmanager.com",
- "match": "suffix",
+ "host": "browser-intake-datadoghq.eu",
+ "match": "exact",
"category": "tracking"
},
{
"id": 12,
- "host": "heapanalytics.com",
- "match": "suffix",
+ "host": "browser-intake-ddog-gov.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 13,
- "host": "lr-ingest.io",
- "match": "suffix",
+ "host": "browser-intake-uk1-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 14,
- "host": "matomo.cloud",
- "match": "suffix",
+ "host": "browser-intake-us2-ddog-gov.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 15,
- "host": "mixpanel.com",
- "match": "suffix",
+ "host": "browser-intake-us3-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 16,
- "host": "mxpnl.com",
- "match": "suffix",
+ "host": "browser-intake-us5-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 17,
- "host": "posthog.com",
- "match": "suffix",
+ "host": "c.eu.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 18,
- "host": "prodregistryv2.org",
- "match": "suffix",
+ "host": "c.us.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 19,
- "host": "segment.com",
- "match": "suffix",
+ "host": "cdn.eu.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 20,
- "host": "segment.io",
- "match": "suffix",
+ "host": "cdn.heapanalytics.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 21,
- "host": "segmentapis.com",
- "match": "suffix",
+ "host": "cdn.matomo.cloud",
+ "match": "exact",
"category": "tracking"
},
{
"id": 22,
- "host": "statsig.com",
- "match": "suffix",
+ "host": "cdn.mxpnl.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 23,
- "host": "statsigapi.net",
+ "host": "cdn.us.heap-api.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 24,
+ "host": "clarity.ms",
"match": "suffix",
"category": "tracking"
+ },
+ {
+ "id": 25,
+ "host": "edge.eu1.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 26,
+ "host": "edge.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 27,
+ "host": "eu-assets.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 28,
+ "host": "eu.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 29,
+ "host": "events.eu1.segmentapis.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 30,
+ "host": "events.statsigapi.net",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 31,
+ "host": "google-analytics.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 32,
+ "host": "googletagmanager.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 33,
+ "host": "lr-ingest.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 34,
+ "host": "lr-ingest.io",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 35,
+ "host": "lr-intake.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 36,
+ "host": "lrkt-in.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 37,
+ "host": "prodregistryv2.org",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 38,
+ "host": "r.logrocket.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 39,
+ "host": "rs.eu1.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 40,
+ "host": "rs.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 41,
+ "host": "services.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 42,
+ "host": "us-assets.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 43,
+ "host": "us.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
}
]
}
@@ -474,7 +594,7 @@
{
"algorithm": "p256-sha256",
"keyID": "flowlight-rule-packs-2026-01",
- "value": "MEQCIHugfQ8odXRdT6TKDoM7j22LzsvRMKUrCAqOSAs4eB1GAiBR15TV4oaid3NW/qkq9QXoF++0G4IrckUmod1ClDhdTg=="
+ "value": "MEUCIGfUrVoocSM9MzBY8lza2UgWgJzS6o7fMS8A5MU0FoqkAiEA65tGvNTF6Lzmc/tyvTcOLDWZb9v9QIy3RpxsDYoL+6g="
}
]
}
diff --git a/Flowlight/UI/RuleEditor.swift b/Flowlight/UI/RuleEditor.swift
index 0218ae4a..b7730295 100644
--- a/Flowlight/UI/RuleEditor.swift
+++ b/Flowlight/UI/RuleEditor.swift
@@ -14,6 +14,17 @@ struct RuleEditor: View {
@State private var showAdvanced = false
@State private var apps: [InstalledApps.App] = []
+ private var isHostDestination: Bool {
+ !rule.destination.isEmpty && !AgentPolicy.isIPAddress(rule.destination) && !AgentPolicy.isCIDR(rule.destination)
+ }
+
+ private var destinationHelp: String {
+ if isHostDestination, rule.destinationMatch == .exact {
+ return L("Exactly that host. Write *.example.com to cover the domain and its subdomains.")
+ }
+ return L("A domain carries its subdomains with it: `example.com` covers `api.example.com`. Naming an agent covers the tools and MCP servers it started.")
+ }
+
var body: some View {
VStack(alignment: .leading, spacing: 0) {
header
@@ -32,7 +43,14 @@ struct RuleEditor: View {
}
TextField(L("Destination"), text: $rule.destination,
prompt: Text(L("Anywhere — or a domain, an IP address, or a range")))
- Text(L("A domain carries its subdomains with it: `example.com` covers `api.example.com`. Naming an agent covers the tools and MCP servers it started."))
+ if isHostDestination {
+ Picker(L("Destination"), selection: $rule.destinationMatch) {
+ Text(L("Host")).tag(Rule.DestinationMatch.exact)
+ Text(L("Hostnames")).tag(Rule.DestinationMatch.suffix)
+ }
+ .pickerStyle(.segmented)
+ }
+ Text(destinationHelp)
.font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true)
}
diff --git a/FlowlightExtension/IPCServer.swift b/FlowlightExtension/IPCServer.swift
index acccc170..d791da38 100644
--- a/FlowlightExtension/IPCServer.swift
+++ b/FlowlightExtension/IPCServer.swift
@@ -142,7 +142,9 @@ final class IPCServer: NSObject, NSXPCListenerDelegate, FlowlightProviderXPC, @u
self.flushDecisionsLocked()
self.flushPackEventsLocked()
// Registration is the synchronization barrier: the app may publish rules only after this reset is done.
- reply(true, Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "")
+ let shortVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? ""
+ let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? ""
+ reply(true, build.isEmpty ? shortVersion : "\(shortVersion)/\(build)")
}
}
diff --git a/FlowlightExtension/Info.plist b/FlowlightExtension/Info.plist
index 485a777c..ecb15968 100644
--- a/FlowlightExtension/Info.plist
+++ b/FlowlightExtension/Info.plist
@@ -20,12 +20,12 @@
content filter at a time and holds superseded copies until a restart, so changing the extension's version
on every app release forced a reboot each time. Kept fixed, an app-only update ships the version macOS
already runs and replaces nothing. Bump these two ONLY when the FlowlightExtension binary itself changes
- (and expect one replacement when you do). 0.15.4 keeps app-version admission app-side while the extension
- validates the independently versioned snapshot protocol. -->
+ (and expect one replacement when you do). Build 27 adds exact-host ordinary rules and reports the full
+ version/build identity before the app publishes those rules. -->
CFBundleShortVersionString
0.15.4
CFBundleVersion
- 26
+ 27
LSMinimumSystemVersion
$(MACOSX_DEPLOYMENT_TARGET)
FLAppGroupIdentifier
diff --git a/FlowlightTests/ExtensionSourceTests.swift b/FlowlightTests/ExtensionSourceTests.swift
index 73a3f2e4..88d18261 100644
--- a/FlowlightTests/ExtensionSourceTests.swift
+++ b/FlowlightTests/ExtensionSourceTests.swift
@@ -40,6 +40,14 @@ final class ExtensionSourceTests: XCTestCase {
XCTAssertEqual(count, 1)
}
+ func testRulePublicationRequiresTheExactExtensionBuildIdentity() {
+ XCTAssertTrue(ExtensionTrafficSource.acceptsRegisteredVersion("0.15.4/27", expected: "0.15.4/27"))
+ XCTAssertFalse(ExtensionTrafficSource.acceptsRegisteredVersion("0.15.4", expected: "0.15.4/27"))
+ XCTAssertFalse(ExtensionTrafficSource.acceptsRegisteredVersion("0.15.4/26", expected: "0.15.4/27"))
+ XCTAssertFalse(ExtensionTrafficSource.acceptsRegisteredVersion("", expected: "0.15.4/27"))
+ XCTAssertFalse(ExtensionTrafficSource.acceptsRegisteredVersion("0.15.4/27", expected: ""))
+ }
+
func testRulePackPushGateAbsorbsDuplicateStartupPush() {
var gate = RulePackPushGate()
diff --git a/FlowlightTests/InspectionTests.swift b/FlowlightTests/InspectionTests.swift
index bedb382d..f7ffbbb0 100644
--- a/FlowlightTests/InspectionTests.swift
+++ b/FlowlightTests/InspectionTests.swift
@@ -296,6 +296,16 @@ final class InspectionStorageTests: XCTestCase {
XCTAssertTrue(Rule(action: .block, destination: "2001:db8::/32").matches(v6))
}
+ func testRequestLevelExactRuleDoesNotWidenToAChildHost() {
+ var rule = Rule(action: .block, destination: "api.example.com", destinationMatch: .exact)
+ rule.path = "/v1/*"
+ let exact = InspectionController.proxyDestination("api.example.com")
+ let child = InspectionController.proxyDestination("child.api.example.com")
+
+ XCTAssertTrue(rule.destinationMatches(host: exact.host, ip: exact.ip))
+ XCTAssertFalse(rule.destinationMatches(host: child.host, ip: child.ip))
+ }
+
func testProxyFactsKeepNamesAndExemptLocalAddresses() {
let owner = InspectionRecorder.Owner(bundleID: "com.example.app")
let named = InspectionController.proxyFlowFacts(host: "API.Example.com", port: 443, owner: owner)
diff --git a/FlowlightTests/RulePackCatalogTests.swift b/FlowlightTests/RulePackCatalogTests.swift
index a4519253..4c057a29 100644
--- a/FlowlightTests/RulePackCatalogTests.swift
+++ b/FlowlightTests/RulePackCatalogTests.swift
@@ -347,8 +347,8 @@ final class RulePackCatalogTests: XCTestCase {
currentAppVersion: "0.15.3"
)
XCTAssertEqual(verified.signerKeyID, "flowlight-rule-packs-2026-01")
- XCTAssertEqual(verified.sequence, 1)
- XCTAssertEqual(verified.digest, "61e413b20d155f6698ee6d928b40bda33de7aa1fdb1ae590fad8869071569670")
+ XCTAssertEqual(verified.sequence, 2)
+ XCTAssertEqual(verified.digest, "2d8694c6874cb7458e2f22aa419f1cc3eb592e4541da77801c503aec53759eaf")
XCTAssertEqual(
verified.definitions.sorted { $0.id < $1.id },
BuiltInRulePacks.all.sorted { $0.id < $1.id }
diff --git a/FlowlightTests/RulePackCatalogUpdaterTests.swift b/FlowlightTests/RulePackCatalogUpdaterTests.swift
index 98151c05..6b22a6e1 100644
--- a/FlowlightTests/RulePackCatalogUpdaterTests.swift
+++ b/FlowlightTests/RulePackCatalogUpdaterTests.swift
@@ -22,7 +22,7 @@ final class RulePackCatalogUpdaterTests: XCTestCase {
await updater.bootstrap()
XCTAssertEqual(counter.calls, 0)
- XCTAssertEqual(updater.receipt?.sequence, 1)
+ XCTAssertEqual(updater.receipt?.sequence, 2)
XCTAssertEqual(updater.status, .current)
XCTAssertEqual(store.definitions, BuiltInRulePacks.all.sorted { $0.id < $1.id })
XCTAssertFalse(updater.isDownloadedUpdate(BuiltInRulePacks.advertising))
diff --git a/FlowlightTests/RulePackTests.swift b/FlowlightTests/RulePackTests.swift
index 26e12059..8f6ffb42 100644
--- a/FlowlightTests/RulePackTests.swift
+++ b/FlowlightTests/RulePackTests.swift
@@ -21,6 +21,49 @@ final class RulePackTests: XCTestCase {
XCTAssertTrue(matcher.matches(host: "sub.exact.example", bundleID: "com.example.app").isEmpty)
}
+ func testOfficialTrackingPackBlocksCollectorsWithoutBlockingProducts() throws {
+ let definition = BuiltInRulePacks.all.first { $0.id == "official.tracking" }
+ let tracking = try XCTUnwrap(definition)
+ let matcher = try RulePackMatcher(snapshot: snapshot(tracking))
+ let bundleID = "com.apple.Safari"
+
+ let productHosts = [
+ "analytics.google.com",
+ "analytics.amplitude.com",
+ "app.fullstory.com",
+ "heapanalytics.com",
+ "app.heapanalytics.com",
+ "customer.matomo.cloud",
+ "mixpanel.com",
+ "app.mixpanel.com",
+ "app.posthog.com",
+ "app.segment.com",
+ "console.statsig.com",
+ "api.statsig.com",
+ ]
+ for host in productHosts {
+ XCTAssertTrue(matcher.matches(host: host, bundleID: bundleID).isEmpty,
+ "the tracking pack must leave product host \(host) reachable")
+ }
+
+ let collectorHosts = [
+ "api2.amplitude.com",
+ "browser-intake-us3-datadoghq.com",
+ "www.google-analytics.com",
+ "rs.fullstory.com",
+ "c.us.heap-api.com",
+ "r.logrocket.com",
+ "api-eu.mixpanel.com",
+ "eu.i.posthog.com",
+ "events.eu1.segmentapis.com",
+ "events.statsigapi.net",
+ ]
+ for host in collectorHosts {
+ XCTAssertEqual(matcher.matches(host: host, bundleID: bundleID).count, 1,
+ "the tracking pack must still block collector host \(host)")
+ }
+ }
+
func testSelectedAppScopeUsesConnectionOpeningBundleID() throws {
let definition = pack(entries: [RulePackEntry(id: 1, host: "ads.example", category: .advertising)])
let value = RulePackSnapshot.make(
diff --git a/FlowlightTests/RuleTemplateTests.swift b/FlowlightTests/RuleTemplateTests.swift
index 1f10dd71..654bc624 100644
--- a/FlowlightTests/RuleTemplateTests.swift
+++ b/FlowlightTests/RuleTemplateTests.swift
@@ -216,7 +216,7 @@ final class RuleTemplateTests: XCTestCase {
/// neither — that is how "read but don't write" and "not at night or at the weekend" are said.
private func signature(_ rule: Rule) -> String {
let schedule = rule.schedule
- return [rule.action.rawValue, rule.app, rule.destination, rule.method, rule.path,
+ return [rule.action.rawValue, rule.app, rule.destination, rule.destinationMatch.rawValue, rule.method, rule.path,
schedule.kind.rawValue, schedule.days.map(String.init).joined(separator: ","),
"\(schedule.start)", "\(schedule.end)"].joined(separator: "|")
}
@@ -274,8 +274,8 @@ final class RuleTemplateTests: XCTestCase {
/// The reason a whole-Mac template shows up on a row at all: the row is where someone noticed the category.
func testADestinationRowIsOfferedTheTemplateThatAlreadyCoversIt() {
- let segment = RuleTemplate.Subject(destination: "api.segment.io")
- let offered = RuleTemplate.applicable(to: segment).map(\.id)
+ let tracker = RuleTemplate.Subject(destination: "cdn.lr-ingest.io")
+ let offered = RuleTemplate.applicable(to: tracker).map(\.id)
XCTAssertTrue(offered.contains("telemetry.product-analytics"), offered.description)
XCTAssertFalse(offered.contains("exfil.paste-sites"), "a paste-site template on an analytics row")
}
@@ -288,6 +288,35 @@ final class RuleTemplateTests: XCTestCase {
}
}
+ func testProductAnalyticsTemplateMatchesTheReviewedTrackingPackInventory() throws {
+ let template = try XCTUnwrap(RuleTemplate.all.first { $0.id == "telemetry.product-analytics" })
+ let ordinary = Set(template.rules().map { "\($0.destination)|\($0.destinationMatch.rawValue)" })
+ let managed = Set(BuiltInRulePacks.tracking.entries.map { "\($0.host)|\($0.match.rawValue)" })
+ XCTAssertEqual(ordinary, managed)
+ }
+
+ func testProductAnalyticsTemplateLeavesProviderProductsReachable() throws {
+ let template = try XCTUnwrap(RuleTemplate.all.first { $0.id == "telemetry.product-analytics" })
+ let rules = template.rules(for: subject)
+ for host in ["analytics.google.com", "analytics.amplitude.com", "app.fullstory.com",
+ "customer.matomo.cloud", "app.mixpanel.com", "app.posthog.com",
+ "app.segment.com", "console.statsig.com"] {
+ XCTAssertEqual(RuleBook.decide(facts(host: host), rules: rules, session: session).verdict, .allow,
+ "the template must leave product host \(host) reachable")
+ }
+ for host in ["www.google-analytics.com", "cdn.lr-ingest.io", "events.prodregistryv2.org",
+ "api2.amplitude.com", "api.mixpanel.com", "api.segment.io", "events.statsigapi.net",
+ "rs.fullstory.com", "eu.i.posthog.com", "cdn.heapanalytics.com", "cdn.matomo.cloud"] {
+ XCTAssertEqual(RuleBook.decide(facts(host: host), rules: rules, session: session).verdict, .block,
+ "the template must still block dedicated tracking host \(host)")
+ }
+ for host in ["child.api2.amplitude.com", "child.api.mixpanel.com", "child.api.segment.io",
+ "child.events.statsigapi.net", "child.rs.fullstory.com", "child.eu.i.posthog.com"] {
+ XCTAssertEqual(RuleBook.decide(facts(host: host), rules: rules, session: session).verdict, .allow,
+ "an exact collector rule must not widen to \(host)")
+ }
+ }
+
func testEveryTemplateOfferedToARowCanBeFilledFromIt() {
for candidate in [RuleTemplate.Subject(app: "com.example.agent", appName: "Example Agent"),
RuleTemplate.Subject(destination: "example.com"),
diff --git a/FlowlightTests/RuleTests.swift b/FlowlightTests/RuleTests.swift
index e44b4bf3..bf1d3797 100644
--- a/FlowlightTests/RuleTests.swift
+++ b/FlowlightTests/RuleTests.swift
@@ -39,6 +39,68 @@ final class RuleTests: XCTestCase {
XCTAssertFalse(rule.matches(facts(host: "notexample.com")))
}
+ func testAnExactHostDoesNotCarryItsSubdomains() {
+ let rule = Rule(action: .block, destination: "api.example.com", destinationMatch: .exact)
+ XCTAssertTrue(rule.matches(facts(host: "API.EXAMPLE.COM.")))
+ XCTAssertFalse(rule.matches(facts(host: "child.api.example.com")))
+ XCTAssertFalse(rule.matches(facts(host: "example.com")))
+ }
+
+ func testAnExactAllowBeatsABroaderSuffixBlock() {
+ let block = Rule(action: .block, destination: "example.com")
+ let allow = Rule(action: .allow, destination: "api.example.com", destinationMatch: .exact)
+ XCTAssertEqual(RuleBook.decide(facts(), rules: [block, allow], session: session).verdict, .allow)
+ }
+
+ func testLegacyProductAnalyticsPresetRuleStopsBlockingTheProduct() throws {
+ let json = Data(#"{"action":"block","destination":"mixpanel.com","origin":"preset","name":"no product analytics"}"#.utf8)
+ var legacy = try JSONDecoder().decode(Rule.self, from: json)
+ XCTAssertEqual(legacy.presetID, "telemetry.product-analytics")
+ XCTAssertFalse(legacy.matches(facts(host: "mixpanel.com")))
+ XCTAssertFalse(legacy.matches(facts(host: "app.mixpanel.com")))
+ XCTAssertTrue(legacy.matches(facts(host: "api.mixpanel.com")))
+ XCTAssertFalse(legacy.matches(facts(host: "child.api.mixpanel.com")))
+ legacy.name = "My analytics policy"
+ XCTAssertFalse(legacy.matches(facts(host: "app.mixpanel.com")), "renaming must not restore the broad block")
+ XCTAssertTrue(legacy.matches(facts(host: "api.mixpanel.com")))
+ }
+
+ func testLegacyAppScopedProductAnalyticsRuleKeepsItsScope() throws {
+ let json = Data(#"{"action":"block","app":"com.example.app","destination":"amplitude.com","origin":"preset","name":"Example: no product analytics"}"#.utf8)
+ let legacy = try JSONDecoder().decode(Rule.self, from: json)
+ XCTAssertTrue(legacy.matches(facts(host: "api2.amplitude.com")))
+ XCTAssertFalse(legacy.matches(facts(app: "com.example.other", agent: "com.example.other",
+ host: "api2.amplitude.com")))
+ }
+
+ func testTypedAndRenamedBroadRulesKeepTheirAuthoredMeaning() {
+ let typed = Rule(action: .block, destination: "mixpanel.com")
+ let renamed = Rule(action: .block, destination: "mixpanel.com", origin: .preset, name: "My block")
+ XCTAssertTrue(typed.matches(facts(host: "app.mixpanel.com")))
+ XCTAssertTrue(renamed.matches(facts(host: "app.mixpanel.com")))
+ }
+
+ func testLegacySharedProductRuleWithNoSafeCollectorIsRetired() throws {
+ let json = Data(#"{"action":"block","destination":"segment.com","origin":"preset","name":"no product analytics"}"#.utf8)
+ let legacy = try JSONDecoder().decode(Rule.self, from: json)
+ XCTAssertFalse(legacy.matches(facts(host: "segment.com")))
+ XCTAssertFalse(legacy.matches(facts(host: "app.segment.com")))
+ }
+
+ func testEveryLegacyProductAnalyticsMappingLeavesTheBroadProductDomainReachable() {
+ for (domain, collectors) in Rule.legacyProductAnalyticsReplacements {
+ let legacy = Rule(action: .block, destination: domain, origin: .preset,
+ presetID: "telemetry.product-analytics", name: "no product analytics")
+ XCTAssertFalse(legacy.matches(facts(host: domain)), domain)
+ XCTAssertFalse(legacy.matches(facts(host: "app.\(domain)")), domain)
+ for collector in collectors {
+ XCTAssertTrue(legacy.matches(facts(host: collector)), "\(domain) must retain \(collector)")
+ XCTAssertFalse(legacy.matches(facts(host: "child.\(collector)")),
+ "legacy collector \(collector) must be exact")
+ }
+ }
+ }
+
func testNamingAnAgentCoversTheToolsItStarted() {
let rule = Rule(action: .block, app: "claude")
// curl, started by Claude Code: its own bundle id is curl's, its agent key is the agent's.
@@ -757,13 +819,44 @@ final class RuleTests: XCTestCase {
// MARK: Storing them
func testARuleSurvivesARoundTrip() throws {
- var rule = Rule(action: .block, app: "claude", destination: "example.com")
+ var rule = Rule(action: .block, app: "claude", destination: "example.com", destinationMatch: .exact)
rule.schedule = Rule.Schedule(kind: .window, days: [2, 3], start: 60, end: 120)
rule.hits = 4
let decoded = try JSONDecoder().decode(Rule.self, from: JSONEncoder().encode(rule))
XCTAssertEqual(decoded, rule)
}
+ func testARuleStoredBeforeExactMatchingDefaultsToSuffix() throws {
+ let json = Data(#"{"destination":"example.com","action":"block"}"#.utf8)
+ let decoded = try JSONDecoder().decode(Rule.self, from: json)
+ XCTAssertEqual(decoded.destinationMatch, .suffix)
+ XCTAssertTrue(decoded.matches(facts(host: "api.example.com")))
+ }
+
+ func testSavingAWildcardDestinationSelectsSuffixMatching() async throws {
+ let url = FileManager.default.temporaryDirectory
+ .appendingPathComponent("flowlight-rule-wildcard-\(UUID().uuidString).sqlite")
+ let db = try TrafficDatabase(url: url)
+ let store = RuleStore()
+ store.attach(db: db)
+ try await waitUntil { !store.rules.isEmpty || store.ruleSet.generation > 0 }
+ let rule = Rule(action: .block, destination: "*.example.com", destinationMatch: .exact)
+
+ store.save(rule)
+
+ let saved = try XCTUnwrap(store.rules.first(where: { $0.id == rule.id }))
+ XCTAssertEqual(saved.destination, "example.com")
+ XCTAssertEqual(saved.destinationMatch, .suffix)
+ XCTAssertTrue(saved.matches(facts(host: "api.example.com")))
+ }
+
+ func testRuleSetTransportPreservesExactMatching() throws {
+ let set = RuleSet(rules: [Rule(action: .block, destination: "api.example.com", destinationMatch: .exact)])
+ let decoded = try XCTUnwrap(BlockCoding.decode(RuleSet.self, from: BlockCoding.encode(set)))
+ XCTAssertEqual(decoded, set)
+ XCTAssertEqual(decoded.rules.first?.destinationMatch, .exact)
+ }
+
func testARefusalCarriesAReadableAnswer() {
var rule = Rule(action: .block, destination: "example.com")
rule.path = "/v1/*"
diff --git a/README.md b/README.md
index fc1bc212..020b933a 100644
--- a/README.md
+++ b/README.md
@@ -168,10 +168,13 @@ From any grouped event, add an exception for that app and destination in that pa
universal escape hatch, global **Pause Blocking** pauses packs too, and essential system traffic for accounts, push,
updates, certificates and iCloud remains allowed.
-These are hostname-level network blocks, not page or window cleaners. They cannot remove first-party ads, DOM or layout
-gaps, browser overlays, scripted popups, or native app windows. ECH, QUIC, encrypted DNS, direct-IP connections and
-shared CDNs can hide or blur the hostname and limit coverage. Cosmetic popup filtering needs a later browser extension
-that can see and change page content; the Network Extension cannot do that job.
+These are hostname-level network blocks, not page or window cleaners. The tracking pack blocks documented dedicated
+collection, replay and tracker-bootstrap hosts while keeping provider dashboards, login, billing, documentation and
+administration reachable. When collection shares a product hostname—such as `analytics.google.com/g/collect` or a
+Matomo tenant's `/matomo.php`—the hostname layer must allow it; HTTPS inspection may still apply an explicit URL rule
+when the path is visible. First-party tracking proxies, ECH, QUIC, encrypted DNS, direct-IP connections and shared CDNs
+can also limit coverage. Packs cannot remove first-party ads, DOM gaps, overlays, scripted popups or native app windows;
+cosmetic filtering needs a browser extension that can see and change page content.
### Agent guardrails
*Which tools may this agent use* is a different question from *which hosts may it reach*, so it gets its own tab in
diff --git a/Shared/Rule.swift b/Shared/Rule.swift
index da22d7cd..c8b75b85 100644
--- a/Shared/Rule.swift
+++ b/Shared/Rule.swift
@@ -17,8 +17,10 @@ struct Rule: Codable, Identifiable, Equatable, Sendable {
/// A bundle identifier or a process name; empty means any app. Matched against the process that opened the
/// connection *and* the agent it belongs to, so naming Claude Code also covers the `curl` it ran.
var app = ""
- /// A domain (subdomains included), an IP address or a CIDR range; empty means anywhere.
+ /// A domain, an IP address or a CIDR range; empty means anywhere. Host rules include subdomains unless
+ /// `destinationMatch` says this is one exact collector host.
var destination = ""
+ var destinationMatch: DestinationMatch = .suffix
/// A path glob (`/v1/*`), empty for any path. A path can only be matched where the request can be read, which
/// is the HTTPS inspection proxy — `engine` says so and the list shows it.
var path = ""
@@ -29,6 +31,8 @@ struct Rule: Codable, Identifiable, Equatable, Sendable {
var schedule = Schedule()
var origin: Origin = .typed
+ /// Stable identity for a library-generated policy. A display-name edit must never change what the rule matches.
+ var presetID = ""
/// What the person called it. Empty falls back to a sentence built from the subject.
var name = ""
var created = Date()
@@ -51,6 +55,10 @@ struct Rule: Codable, Identifiable, Equatable, Sendable {
case typed, preset, alert, allowOnce
}
+ enum DestinationMatch: String, Codable, Sendable {
+ case suffix, exact
+ }
+
/// Which part of Flowlight can carry this rule out.
enum Engine: String, Codable, Sendable {
/// The Network Extension, which refuses whole connections. It knows host, IP and port and nothing about
@@ -86,7 +94,8 @@ struct Rule: Codable, Identifiable, Equatable, Sendable {
if !app.isEmpty { score += 4 }
if !destination.isEmpty {
// An exact address or host is narrower than a domain that carries its subdomains with it.
- score += AgentPolicy.isIPAddress(destination) ? 5 : (AgentPolicy.isCIDR(destination) ? 3 : 4)
+ score += AgentPolicy.isIPAddress(destination) ? 5
+ : (AgentPolicy.isCIDR(destination) ? 3 : (destinationMatch == .exact ? 5 : 4))
}
if !path.isEmpty { score += path.contains("*") ? 1 : 2 }
if !method.isEmpty { score += 1 }
@@ -98,6 +107,37 @@ struct Rule: Codable, Identifiable, Equatable, Sendable {
var isComplete: Bool { !app.isEmpty || !destination.isEmpty }
}
+extension Rule {
+ /// Written out so rules saved before exact-host matching existed keep their original suffix semantics.
+ /// Synthesized Codable does not use property defaults when a key is absent.
+ init(from decoder: Decoder) throws {
+ let container = try decoder.container(keyedBy: CodingKeys.self)
+ id = try container.decodeIfPresent(UUID.self, forKey: .id) ?? UUID()
+ enabled = try container.decodeIfPresent(Bool.self, forKey: .enabled) ?? true
+ action = try container.decodeIfPresent(Action.self, forKey: .action) ?? .block
+ app = try container.decodeIfPresent(String.self, forKey: .app) ?? ""
+ destination = try container.decodeIfPresent(String.self, forKey: .destination) ?? ""
+ destinationMatch = try container.decodeIfPresent(DestinationMatch.self, forKey: .destinationMatch) ?? .suffix
+ path = try container.decodeIfPresent(String.self, forKey: .path) ?? ""
+ method = try container.decodeIfPresent(String.self, forKey: .method) ?? ""
+ status = try container.decodeIfPresent(Int.self, forKey: .status) ?? 403
+ schedule = try container.decodeIfPresent(Schedule.self, forKey: .schedule) ?? Schedule()
+ origin = try container.decodeIfPresent(Origin.self, forKey: .origin) ?? .typed
+ presetID = try container.decodeIfPresent(String.self, forKey: .presetID) ?? ""
+ name = try container.decodeIfPresent(String.self, forKey: .name) ?? ""
+ if presetID.isEmpty,
+ origin == .preset,
+ name == "no product analytics" || name.hasSuffix(": no product analytics"),
+ Self.legacyProductAnalyticsReplacements[destination] != nil {
+ presetID = "telemetry.product-analytics"
+ }
+ created = try container.decodeIfPresent(Date.self, forKey: .created) ?? Date()
+ hits = try container.decodeIfPresent(Int.self, forKey: .hits) ?? 0
+ lastHit = try container.decodeIfPresent(Date.self, forKey: .lastHit)
+ maxHits = try container.decodeIfPresent(Int.self, forKey: .maxHits) ?? 0
+ }
+}
+
// MARK: When a rule applies
extension Rule {
@@ -195,11 +235,61 @@ extension Rule {
if !destination.isEmpty {
// An unnamed destination can't be ruled out yet; the hostname may still arrive.
guard !facts.host.isEmpty || facts.hostSettled else { return false }
- guard AgentPolicy.matches(destination, host: facts.host, ip: facts.ip) else { return false }
+ guard destinationMatches(host: facts.host, ip: facts.ip) else { return false }
}
return true
}
+ func destinationMatches(host: String, ip: String) -> Bool {
+ // 0.15.0–0.15.10 wrote broad suffix rules for this preset. Interpret only that exact generated shape as
+ // the reviewed collectors it meant, without rewriting the person's database or changing a typed/renamed rule.
+ // An empty target list deliberately retires a shared product/configuration host with no safe replacement.
+ if let collectors = legacyProductAnalyticsCollectors {
+ guard let actual = HostCanonicalizer.host(host) else { return false }
+ return collectors.contains { HostCanonicalizer.host($0) == actual }
+ }
+ if destinationMatch == .exact,
+ !AgentPolicy.isIPAddress(destination),
+ !AgentPolicy.isCIDR(destination) {
+ guard let expected = HostCanonicalizer.host(destination),
+ let actual = HostCanonicalizer.host(host) else { return false }
+ return expected == actual
+ }
+ return AgentPolicy.matches(destination, host: host, ip: ip)
+ }
+
+ private var legacyProductAnalyticsCollectors: [String]? {
+ guard origin == .preset,
+ presetID == "telemetry.product-analytics",
+ action == .block,
+ destinationMatch == .suffix,
+ path.isEmpty,
+ method.isEmpty,
+ maxHits == 0 else { return nil }
+ return Self.legacyProductAnalyticsReplacements[destination]
+ }
+
+ static let legacyProductAnalyticsReplacements: [String: [String]] = [
+ "analytics.google.com": [],
+ "segment.io": ["api.segment.io"],
+ "segment.com": [],
+ "segmentapis.com": ["events.eu1.segmentapis.com"],
+ "mixpanel.com": ["api-eu.mixpanel.com", "api-in.mixpanel.com", "api-js.mixpanel.com", "api.mixpanel.com"],
+ "mxpnl.com": ["cdn.mxpnl.com"],
+ "amplitude.com": ["api.eu.amplitude.com", "api2.amplitude.com"],
+ "posthog.com": ["eu-assets.i.posthog.com", "eu.i.posthog.com", "us-assets.i.posthog.com", "us.i.posthog.com"],
+ "heapanalytics.com": ["cdn.heapanalytics.com"],
+ "fullstory.com": ["edge.eu1.fullstory.com", "edge.fullstory.com", "rs.eu1.fullstory.com",
+ "rs.fullstory.com", "services.fullstory.com"],
+ "matomo.cloud": ["cdn.matomo.cloud"],
+ "statsig.com": [],
+ "statsigapi.net": ["events.statsigapi.net"],
+ "featuregates.org": [],
+ "featureassets.org": [],
+ "assetsconfigcdn.org": [],
+ "beyondwickedmapping.org": [],
+ ]
+
/// Whether this rule answers one request, once the connection it arrived on already matches. Only the proxy
/// ever asks.
func matches(path target: String, method verb: String) -> Bool {
diff --git a/docs/404.html b/docs/404.html
index 0dd00678..8b599c96 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -86,7 +86,7 @@ That page isn't here Try the home page ,
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 92006130..20628f17 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -303,9 +303,13 @@ serves at `https://flowlight.xinbetween.com/packs/v1/official.json`. CI and Page
P-256 signature against `Flowlight/Resources/RulePacks/official-rule-packs-v1-public.pem`. They never sign it and have
no private key.
-Validate edits before asking the offline signer to sign them:
+Validate edits before asking the offline signer to sign them. The catalog validator also enforces an explicit,
+evidence-backed allowlist for `official.tracking`: dedicated collection, replay and tracker-bootstrap hosts are allowed,
+while product/shared domains fail before signing. Add a new endpoint only with vendor documentation and regression
+coverage showing both its collector behavior and the product surfaces that must remain reachable.
```sh
+scripts/test-rule-pack-policy.sh
python3 -I scripts/rule_pack_catalog.py validate rule-packs/official-source.json
python3 -I scripts/rule_pack_catalog.py digest rule-packs/official-source.json
```
@@ -323,6 +327,8 @@ python3 -I scripts/rule_pack_catalog.py compare \
rule-packs/official-source.json site/packs/v1/official.json
python3 -I scripts/rule_pack_catalog.py compare-previous site/packs/v1/official.json \
--previous-ref origin/main
+# Use `verify --historical` only for an immutable prior catalog; it still verifies structure and signature but does
+# not retroactively apply today's endpoint allowlist to content published before that policy existed.
python3 -I scripts/rule_pack_catalog.py verify site/packs/v1/official.json \
--public-key Flowlight/Resources/RulePacks/official-rule-packs-v1-public.pem \
--expected-key-id flowlight-rule-packs-2026-01
diff --git a/docs/about/index.html b/docs/about/index.html
index b27b030c..a060cc51 100644
--- a/docs/about/index.html
+++ b/docs/about/index.html
@@ -153,7 +153,7 @@ Thanks
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/de/about/index.html b/docs/de/about/index.html
index 41fc4f22..9e5ef1e2 100644
--- a/docs/de/about/index.html
+++ b/docs/de/about/index.html
@@ -152,7 +152,7 @@ Dank
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.15.10 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.11 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html
index 89148c1f..ea227c3c 100644
--- a/docs/de/docs/index.html
+++ b/docs/de/docs/index.html
@@ -76,7 +76,7 @@
Dokumentation
Flowlight benutzen
-
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.10, macOS 15 oder neuer.
+
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.15.11, macOS 15 oder neuer.
@@ -755,7 +755,7 @@ Grenzen
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.15.10 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.11 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/index.html b/docs/de/index.html
index 06708fb3..3a233659 100644
--- a/docs/de/index.html
+++ b/docs/de/index.html
@@ -48,7 +48,7 @@
-
+
Zum Inhalt springen
@@ -90,7 +90,7 @@ Sieh, was deine Apps im Netz tun.brew install --cask xinbetween/tap/flowlightKopieren
- v0.15.10 macOS 15+ Universal GPL-3.0 Keine Telemetrie
+ v0.15.11 macOS 15+ Universal GPL-3.0 Keine Telemetrie
@@ -572,7 +572,7 @@ Wisse, was deinen Mac verlässt.
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.15.10 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.11 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html
index b2771699..37a4b30c 100644
--- a/docs/de/privacy/index.html
+++ b/docs/de/privacy/index.html
@@ -189,7 +189,7 @@
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.15.10 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.11 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html
index b662f66b..177a51e2 100644
--- a/docs/de/threat-model/index.html
+++ b/docs/de/threat-model/index.html
@@ -225,7 +225,7 @@ Eine Schwachstelle melden
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.15.10 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.15.11 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/docs/index.html b/docs/docs/index.html
index e1c6d9a5..461ee9cf 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -76,7 +76,7 @@
Documentation
Using Flowlight
-
Everything from the first launch to tuning the agent rules. Flowlight 0.15.10, macOS 15 or later.
+
Everything from the first launch to tuning the agent rules. Flowlight 0.15.11, macOS 15 or later.
@@ -486,6 +486,12 @@ Managed ad and tracker packs
pack would otherwise refuse. The global Pause Blocking control pauses managed packs along with the rest of
connection blocking. Pack exceptions are narrower conveniences; ordinary rules remain the place for a deliberate policy that
should apply beyond one pack.
+ Block collection, not the product. Cross-site tracking entries use documented dedicated collection, replay and
+ tracker-bootstrap hosts, while provider dashboards, login, billing, documentation and administration stay reachable. When a
+ collection path shares the product hostname — for example analytics.google.com/g/collect or a Matomo tenant's
+ /matomo.php — hostname blocking must allow it. HTTPS inspection can apply an explicit URL rule only when that path is
+ visible; a pinned or otherwise opaque TLS connection cannot be separated safely. First-party tracking proxies are not discoverable
+ from a vendor-host list.
This is hostname-level network blocking, not content filtering. A Network Extension can refuse a connection,
but it cannot edit what a browser or app draws. Packs therefore cannot remove first-party ads, DOM or layout gaps left behind by
blocked content, browser overlays, scripted popups, or native app windows. ECH, QUIC, encrypted DNS, direct-IP connections and
@@ -793,7 +799,7 @@
Limitations
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/es/about/index.html b/docs/es/about/index.html
index 488e9274..7abbf2d8 100644
--- a/docs/es/about/index.html
+++ b/docs/es/about/index.html
@@ -152,7 +152,7 @@ Agradecimientos
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.15.10 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.11 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html
index 851e7d8c..f650a699 100644
--- a/docs/es/docs/index.html
+++ b/docs/es/docs/index.html
@@ -76,7 +76,7 @@
Documentación
Usar Flowlight
-
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.10, macOS 15 o posterior.
+
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.15.11, macOS 15 o posterior.
@@ -749,7 +749,7 @@ Limitaciones
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.15.10 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.11 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/index.html b/docs/es/index.html
index 732e1826..e41fc63f 100644
--- a/docs/es/index.html
+++ b/docs/es/index.html
@@ -48,7 +48,7 @@
-
+
Ir al contenido
@@ -90,7 +90,7 @@ Observa la actividad de red de tus apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.15.10 macOS 15+ Universal GPL-3.0 Sin telemetría
+ v0.15.11 macOS 15+ Universal GPL-3.0 Sin telemetría
@@ -572,7 +572,7 @@ Ten claro qué sale de tu Mac.
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.15.10 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.11 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html
index 466c5952..0ea108c8 100644
--- a/docs/es/privacy/index.html
+++ b/docs/es/privacy/index.html
@@ -189,7 +189,7 @@
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.15.10 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.11 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html
index 6475b6b1..bf759a39 100644
--- a/docs/es/threat-model/index.html
+++ b/docs/es/threat-model/index.html
@@ -221,7 +221,7 @@ Informar de una vulnerabilidad
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.15.10 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.15.11 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html
index c64470a6..ef23578a 100644
--- a/docs/fr/about/index.html
+++ b/docs/fr/about/index.html
@@ -152,7 +152,7 @@ Remerciements
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.15.10 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.11 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html
index 1718e633..15c3578b 100644
--- a/docs/fr/docs/index.html
+++ b/docs/fr/docs/index.html
@@ -76,7 +76,7 @@
Documentation
Utiliser Flowlight
-
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.10, macOS 15 ou version ultérieure.
+
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.15.11, macOS 15 ou version ultérieure.
@@ -755,7 +755,7 @@ Limites
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.15.10 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.11 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/index.html b/docs/fr/index.html
index c3176953..f8d14582 100644
--- a/docs/fr/index.html
+++ b/docs/fr/index.html
@@ -48,7 +48,7 @@
-
+
Aller au contenu
@@ -90,7 +90,7 @@ Voyez l’activité réseau de vos apps.
brew install --cask xinbetween/tap/flowlightCopier
- v0.15.10 macOS 15+ Universel GPL-3.0 Sans télémétrie
+ v0.15.11 macOS 15+ Universel GPL-3.0 Sans télémétrie
@@ -572,7 +572,7 @@ Sachez ce qui quitte votre Mac.
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.15.10 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.11 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html
index 035f91bc..91869b2c 100644
--- a/docs/fr/privacy/index.html
+++ b/docs/fr/privacy/index.html
@@ -189,7 +189,7 @@
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.15.10 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.11 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html
index 32466174..c9a3ffe6 100644
--- a/docs/fr/threat-model/index.html
+++ b/docs/fr/threat-model/index.html
@@ -224,7 +224,7 @@ Signaler une vulnérabilité
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.15.10 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.15.11 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/index.html b/docs/index.html
index d9cffeab..46232212 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -48,7 +48,7 @@
-
+
Skip to content
@@ -90,7 +90,7 @@ See your apps' network activity.brew install --cask xinbetween/tap/flowlightCopy
- v0.15.10 macOS 15+ Universal GPL-3.0 No telemetry
+ v0.15.11 macOS 15+ Universal GPL-3.0 No telemetry
@@ -253,9 +253,10 @@ Monitor how AI agents use the network
With the Network Extension, apply one to every app or only selected apps; preview it against retained traffic,
see grouped, explainable activity, and add pack-specific exceptions for an app. Ordinary allow rules remain the universal
escape hatch, global Pause Blocking applies, and essential system traffic stays allowed.
- They block by hostname, so they cannot remove first-party ads, DOM or layout gaps, browser overlays, scripted popups, or
- native app windows. ECH, QUIC, encrypted DNS, direct IPs and shared CDNs can limit coverage. Cosmetic popup filtering needs
- a later browser extension that can change the page.
+ They block dedicated collection hosts while keeping analytics dashboards and administration reachable. Shared-host
+ collection paths and first-party tracking proxies cannot always be separated safely; ECH, QUIC, encrypted DNS, direct IPs
+ and shared CDNs also limit coverage. Packs cannot remove first-party ads, page gaps, overlays or popups — cosmetic filtering
+ needs a browser extension that can change the page.
Safari · Advertising matched doubleclick.net · 18 connections grouped
@@ -594,7 +595,7 @@
Know what leaves your Mac.
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/it/about/index.html b/docs/it/about/index.html
index b4409f79..0593467a 100644
--- a/docs/it/about/index.html
+++ b/docs/it/about/index.html
@@ -152,7 +152,7 @@ Ringraziamenti
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.15.10 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.11 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html
index cf2a6b19..59e9e1a5 100644
--- a/docs/it/docs/index.html
+++ b/docs/it/docs/index.html
@@ -76,7 +76,7 @@
Documentazione
Usare Flowlight
-
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.10, macOS 15 o successivo.
+
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.15.11, macOS 15 o successivo.
@@ -753,7 +753,7 @@ Limitazioni
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.15.10 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.11 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/index.html b/docs/it/index.html
index ba53f718..d7b6182f 100644
--- a/docs/it/index.html
+++ b/docs/it/index.html
@@ -48,7 +48,7 @@
-
+
Vai al contenuto
@@ -90,7 +90,7 @@ Vedi la rete delle tue app.Ca
Metti una stella su GitHub
brew install --cask xinbetween/tap/flowlightCopia
- v0.15.10 macOS 15+ Universale GPL-3.0 Nessuna telemetria
+ v0.15.11 macOS 15+ Universale GPL-3.0 Nessuna telemetria
@@ -572,7 +572,7 @@ Sappi che cosa esce dal tuo Mac.
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.15.10 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.11 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html
index bf4aa410..fb70ad0b 100644
--- a/docs/it/privacy/index.html
+++ b/docs/it/privacy/index.html
@@ -189,7 +189,7 @@
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.15.10 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.11 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html
index 46864583..bc393481 100644
--- a/docs/it/threat-model/index.html
+++ b/docs/it/threat-model/index.html
@@ -223,7 +223,7 @@ Segnalare una vulnerabilità
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.15.10 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.15.11 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html
index c3620996..8b229e7d 100644
--- a/docs/ja/about/index.html
+++ b/docs/ja/about/index.html
@@ -153,7 +153,7 @@ 謝辞
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.15.10 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.11 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html
index 049377b8..368d4d6a 100644
--- a/docs/ja/docs/index.html
+++ b/docs/ja/docs/index.html
@@ -76,7 +76,7 @@
ドキュメント
Flowlight の使い方
-
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.10、macOS 15 以降。
+
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.15.11、macOS 15 以降。
@@ -703,7 +703,7 @@ 制限事項
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.15.10 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.11 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/index.html b/docs/ja/index.html
index c2447728..81c4d7b6 100644
--- a/docs/ja/index.html
+++ b/docs/ja/index.html
@@ -48,7 +48,7 @@
-
+
本文へスキップ
@@ -90,7 +90,7 @@ アプリの通信が見える。brew install --cask xinbetween/tap/flowlightコピー
- v0.15.10 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
+ v0.15.11 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
@@ -571,7 +571,7 @@ Mac から何が出ていくのかを知る。
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.15.10 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.11 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html
index fabffc5f..63e9384f 100644
--- a/docs/ja/privacy/index.html
+++ b/docs/ja/privacy/index.html
@@ -187,7 +187,7 @@
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.15.10 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.11 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html
index f1d41a7f..8e6b8720 100644
--- a/docs/ja/threat-model/index.html
+++ b/docs/ja/threat-model/index.html
@@ -222,7 +222,7 @@ 脆弱性を報告する
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.15.10 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.15.11 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html
index 683ea45f..78102ec0 100644
--- a/docs/ko/about/index.html
+++ b/docs/ko/about/index.html
@@ -153,7 +153,7 @@ 감사
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.15.10 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.11 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html
index e751d54b..4e809c56 100644
--- a/docs/ko/docs/index.html
+++ b/docs/ko/docs/index.html
@@ -76,7 +76,7 @@
문서
Flowlight 사용하기
-
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.10, macOS 15 이상.
+
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.15.11, macOS 15 이상.
@@ -744,7 +744,7 @@ 한계
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.15.10 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.11 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/index.html b/docs/ko/index.html
index 60f2f941..371600b3 100644
--- a/docs/ko/index.html
+++ b/docs/ko/index.html
@@ -48,7 +48,7 @@
-
+
본문으로 이동
@@ -90,7 +90,7 @@ 앱이 무엇을 하는지 봅니다.
brew install --cask xinbetween/tap/flowlight복사
- v0.15.10 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
+ v0.15.11 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
@@ -572,7 +572,7 @@ 내 Mac에서 무엇이 나가는지 아세요.
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.15.10 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.11 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html
index 48a62f66..83697899 100644
--- a/docs/ko/privacy/index.html
+++ b/docs/ko/privacy/index.html
@@ -190,7 +190,7 @@
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.15.10 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.11 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html
index a1d2e0cb..90f62391 100644
--- a/docs/ko/threat-model/index.html
+++ b/docs/ko/threat-model/index.html
@@ -221,7 +221,7 @@ 취약점 신고하기
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.15.10 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.15.11 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index 16efb147..b830300d 100644
--- a/docs/llms-full.txt
+++ b/docs/llms-full.txt
@@ -62,7 +62,7 @@ Documentation
Using Flowlight
- Everything from the first launch to tuning the agent rules. Flowlight 0.15.10, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.15.11, macOS 15 or later.
On this page
@@ -547,6 +547,13 @@ open build/Build/Products/Release/Flowlight.app
connection blocking. Pack exceptions are narrower conveniences; ordinary rules remain the place for a deliberate policy that
should apply beyond one pack.
+ Block collection, not the product. Cross-site tracking entries use documented dedicated collection, replay and
+ tracker-bootstrap hosts, while provider dashboards, login, billing, documentation and administration stay reachable. When a
+ collection path shares the product hostname — for example analytics.google.com/g/collect or a Matomo tenant's
+ /matomo.php — hostname blocking must allow it. HTTPS inspection can apply an explicit URL rule only when that path is
+ visible; a pinned or otherwise opaque TLS connection cannot be separated safely. First-party tracking proxies are not discoverable
+ from a vendor-host list.
+
This is hostname-level network blocking, not content filtering. A Network Extension can refuse a connection,
but it cannot edit what a browser or app draws. Packs therefore cannot remove first-party ads, DOM or layout gaps left behind by
blocked content, browser overlays, scripted popups, or native app windows. ECH, QUIC, encrypted DNS, direct-IP connections and
@@ -894,7 +901,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.15.10macOS 15+UniversalGPL-3.0No telemetry
+ v0.15.11macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1052,9 +1059,10 @@ Optional. What did the agent actually send, and which tool call made it? Decrypt
see grouped, explainable activity, and add pack-specific exceptions for an app. Ordinary allow rules remain the universal
escape hatch, global Pause Blocking applies, and essential system traffic stays allowed.
- They block by hostname, so they cannot remove first-party ads, DOM or layout gaps, browser overlays, scripted popups, or
- native app windows. ECH, QUIC, encrypted DNS, direct IPs and shared CDNs can limit coverage. Cosmetic popup filtering needs
- a later browser extension that can change the page.
+ They block dedicated collection hosts while keeping analytics dashboards and administration reachable. Shared-host
+ collection paths and first-party tracking proxies cannot always be separated safely; ECH, QUIC, encrypted DNS, direct IPs
+ and shared CDNs also limit coverage. Packs cannot remove first-party ads, page gaps, overlays or popups — cosmetic filtering
+ needs a browser extension that can change the page.
Safari · Advertising matched doubleclick.net · 18 connections grouped
@@ -1629,9 +1637,22 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
- 0.15.10
+ 0.15.11
October 28, 2026Latest
+ Block tracking traffic without blocking the product. The Tracking pack now targets documented dedicated
+ collection, replay, and tracker-bootstrap hosts while leaving provider dashboards, login, billing, documentation, and
+ administration reachable.
+
+ Keep broad vendor domains out. Exact collector rules replace shared product and feature-configuration
+ suffixes across Amplitude, FullStory, Heap, Matomo, Mixpanel, PostHog, Segment, Statsig, and other providers.
+
+ Make the boundary durable and explicit. Catalog policy fixtures reject unreviewed tracking destinations,
+ matcher tests cover product and collector hosts, and the docs explain shared-host TLS paths and first-party proxy limits.
+
+ 0.15.10
+October 28, 2026
+
Make the full test target warning-free. Concurrency captures, redundant actor awaits, and intentionally ignored
database insert IDs now compile cleanly with current Xcode.
diff --git a/docs/llms.txt b/docs/llms.txt
index 83a22d5e..09c37470 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -1,6 +1,6 @@
# Flowlight
-> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.10.
+> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.15.11.
- [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg)
- [Source code](https://github.com/xinbetween/flowlight)
diff --git a/docs/packs/v1/official.json b/docs/packs/v1/official.json
index ea4a1c66..de463d5f 100644
--- a/docs/packs/v1/official.json
+++ b/docs/packs/v1/official.json
@@ -4,9 +4,9 @@
"catalogID": "flowlight.official",
"channel": "official",
"sourceURL": "https://flowlight.xinbetween.com/packs/v1/official.json",
- "sequence": 1,
- "issuedAt": 1792368000,
- "expiresAt": 1823904000,
+ "sequence": 2,
+ "issuedAt": 1793253368,
+ "expiresAt": 1824789368,
"publisher": "Flowlight",
"packs": [
{
@@ -314,15 +314,15 @@
{
"manifest": {
"id": "official.tracking",
- "version": "1",
- "generatedAt": 1792220400,
- "entryCount": 23,
- "contentSHA256": "57bdd11c751ebc24a785d429afd7d6a9282bb93e4cbe42f6725bd387fef64386",
+ "version": "2",
+ "generatedAt": 1793253368,
+ "entryCount": 43,
+ "contentSHA256": "2fa7a05ad9aba7c8ffe5fea8f7f33ca63e32a5759a4f8efe976497d6132d47b9",
"minimumAppVersion": "0.15.0",
"minimumExtensionProtocol": 1,
"titleKey": "Cross-site tracking",
- "summaryKey": "Common analytics, session replay, and cross-site measurement hosts.",
- "caveatKey": "Some apps use the same providers for feature flags. They may fall back to built-in defaults when a host is unavailable.",
+ "summaryKey": "Dedicated analytics collection, session replay, and tracker bootstrap hosts.",
+ "caveatKey": "Product dashboards stay reachable. Shared-host collection paths and first-party tracking proxies cannot be separated by hostname blocking.",
"source": "Flowlight curated built-in list",
"license": "GPL-3.0",
"format": 1
@@ -330,141 +330,261 @@
"entries": [
{
"id": 1,
- "host": "amplitude.com",
- "match": "suffix",
+ "host": "api-eu.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 2,
- "host": "analytics.google.com",
- "match": "suffix",
+ "host": "api-in.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 3,
- "host": "assetsconfigcdn.org",
- "match": "suffix",
+ "host": "api-js.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 4,
- "host": "beyondwickedmapping.org",
- "match": "suffix",
+ "host": "api.eu.amplitude.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 5,
- "host": "browser-intake-datadoghq.com",
- "match": "suffix",
+ "host": "api.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 6,
- "host": "clarity.ms",
- "match": "suffix",
+ "host": "api.segment.io",
+ "match": "exact",
"category": "tracking"
},
{
"id": 7,
- "host": "featureassets.org",
- "match": "suffix",
+ "host": "api2.amplitude.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 8,
- "host": "featuregates.org",
- "match": "suffix",
+ "host": "browser-intake-ap1-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 9,
- "host": "fullstory.com",
- "match": "suffix",
+ "host": "browser-intake-ap2-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 10,
- "host": "google-analytics.com",
+ "host": "browser-intake-datadoghq.com",
"match": "suffix",
"category": "tracking"
},
{
"id": 11,
- "host": "googletagmanager.com",
- "match": "suffix",
+ "host": "browser-intake-datadoghq.eu",
+ "match": "exact",
"category": "tracking"
},
{
"id": 12,
- "host": "heapanalytics.com",
- "match": "suffix",
+ "host": "browser-intake-ddog-gov.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 13,
- "host": "lr-ingest.io",
- "match": "suffix",
+ "host": "browser-intake-uk1-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 14,
- "host": "matomo.cloud",
- "match": "suffix",
+ "host": "browser-intake-us2-ddog-gov.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 15,
- "host": "mixpanel.com",
- "match": "suffix",
+ "host": "browser-intake-us3-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 16,
- "host": "mxpnl.com",
- "match": "suffix",
+ "host": "browser-intake-us5-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 17,
- "host": "posthog.com",
- "match": "suffix",
+ "host": "c.eu.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 18,
- "host": "prodregistryv2.org",
- "match": "suffix",
+ "host": "c.us.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 19,
- "host": "segment.com",
- "match": "suffix",
+ "host": "cdn.eu.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 20,
- "host": "segment.io",
- "match": "suffix",
+ "host": "cdn.heapanalytics.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 21,
- "host": "segmentapis.com",
- "match": "suffix",
+ "host": "cdn.matomo.cloud",
+ "match": "exact",
"category": "tracking"
},
{
"id": 22,
- "host": "statsig.com",
- "match": "suffix",
+ "host": "cdn.mxpnl.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 23,
- "host": "statsigapi.net",
+ "host": "cdn.us.heap-api.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 24,
+ "host": "clarity.ms",
"match": "suffix",
"category": "tracking"
+ },
+ {
+ "id": 25,
+ "host": "edge.eu1.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 26,
+ "host": "edge.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 27,
+ "host": "eu-assets.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 28,
+ "host": "eu.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 29,
+ "host": "events.eu1.segmentapis.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 30,
+ "host": "events.statsigapi.net",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 31,
+ "host": "google-analytics.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 32,
+ "host": "googletagmanager.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 33,
+ "host": "lr-ingest.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 34,
+ "host": "lr-ingest.io",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 35,
+ "host": "lr-intake.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 36,
+ "host": "lrkt-in.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 37,
+ "host": "prodregistryv2.org",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 38,
+ "host": "r.logrocket.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 39,
+ "host": "rs.eu1.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 40,
+ "host": "rs.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 41,
+ "host": "services.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 42,
+ "host": "us-assets.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 43,
+ "host": "us.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
}
]
}
@@ -474,7 +594,7 @@
{
"algorithm": "p256-sha256",
"keyID": "flowlight-rule-packs-2026-01",
- "value": "MEQCIHugfQ8odXRdT6TKDoM7j22LzsvRMKUrCAqOSAs4eB1GAiBR15TV4oaid3NW/qkq9QXoF++0G4IrckUmod1ClDhdTg=="
+ "value": "MEUCIGfUrVoocSM9MzBY8lza2UgWgJzS6o7fMS8A5MU0FoqkAiEA65tGvNTF6Lzmc/tyvTcOLDWZb9v9QIy3RpxsDYoL+6g="
}
]
}
diff --git a/docs/plugins/index.html b/docs/plugins/index.html
index b244754a..a8caa84c 100644
--- a/docs/plugins/index.html
+++ b/docs/plugins/index.html
@@ -211,7 +211,7 @@ Review checklist
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index e7fee853..e1291b1e 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -196,7 +196,7 @@
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html
index 6a2fa2fe..f7396d5c 100644
--- a/docs/pt-PT/about/index.html
+++ b/docs/pt-PT/about/index.html
@@ -152,7 +152,7 @@ Agradecimentos
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.15.10 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.11 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html
index ee8560aa..ae109443 100644
--- a/docs/pt-PT/docs/index.html
+++ b/docs/pt-PT/docs/index.html
@@ -76,7 +76,7 @@
Documentação
Usar o Flowlight
-
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.10, macOS 15 ou posterior.
+
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.15.11, macOS 15 ou posterior.
@@ -749,7 +749,7 @@ Limitações
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.15.10 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.11 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html
index 50449b69..1909398d 100644
--- a/docs/pt-PT/index.html
+++ b/docs/pt-PT/index.html
@@ -48,7 +48,7 @@
-
+
Ir para o conteúdo
@@ -90,7 +90,7 @@ Veja a atividade de rede das suas apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.15.10 macOS 15+ Universal GPL-3.0 Sem telemetria
+ v0.15.11 macOS 15+ Universal GPL-3.0 Sem telemetria
@@ -572,7 +572,7 @@ Saiba o que sai do seu Mac.
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.15.10 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.11 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html
index 0e5a402c..bcc28e3f 100644
--- a/docs/pt-PT/privacy/index.html
+++ b/docs/pt-PT/privacy/index.html
@@ -189,7 +189,7 @@
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.15.10 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.11 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html
index cc31dfff..fbf7d464 100644
--- a/docs/pt-PT/threat-model/index.html
+++ b/docs/pt-PT/threat-model/index.html
@@ -222,7 +222,7 @@ Comunicar uma vulnerabilidade
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.15.10 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.15.11 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/releases/index.html b/docs/releases/index.html
index d79abfb0..5c99a528 100644
--- a/docs/releases/index.html
+++ b/docs/releases/index.html
@@ -61,7 +61,20 @@ What's new
- 0.15.10 October 28, 2026 Latest
+ 0.15.11 October 28, 2026 Latest
+
+ Block tracking traffic without blocking the product. The Tracking pack now targets documented dedicated
+ collection, replay, and tracker-bootstrap hosts while leaving provider dashboards, login, billing, documentation, and
+ administration reachable.
+ Keep broad vendor domains out. Exact collector rules replace shared product and feature-configuration
+ suffixes across Amplitude, FullStory, Heap, Matomo, Mixpanel, PostHog, Segment, Statsig, and other providers.
+ Make the boundary durable and explicit. Catalog policy fixtures reject unreviewed tracking destinations,
+ matcher tests cover product and collector hosts, and the docs explain shared-host TLS paths and first-party proxy limits.
+
+
+
+
+
Make the full test target warning-free. Concurrency captures, redundant actor awaits, and intentionally ignored
database insert IDs now compile cleanly with current Xcode.
@@ -1373,7 +1386,7 @@ What's new
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html
index 7487a5fd..5753cc1a 100644
--- a/docs/threat-model/index.html
+++ b/docs/threat-model/index.html
@@ -231,7 +231,7 @@ Reporting a vulnerability
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.15.10 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.15.11 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html
index 0e8803e4..a926fb75 100644
--- a/docs/zh-Hans/about/index.html
+++ b/docs/zh-Hans/about/index.html
@@ -152,7 +152,7 @@ 致谢
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.15.10 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.11 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html
index 8b4b6eb3..2d36b0ab 100644
--- a/docs/zh-Hans/docs/index.html
+++ b/docs/zh-Hans/docs/index.html
@@ -76,7 +76,7 @@
文档
使用 Flowlight
-
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.10,macOS 15 或更高版本。
+
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.15.11,macOS 15 或更高版本。
@@ -706,7 +706,7 @@ 限制
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.15.10 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.11 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html
index 13cf0fc0..616a4698 100644
--- a/docs/zh-Hans/index.html
+++ b/docs/zh-Hans/index.html
@@ -48,7 +48,7 @@
-
+
跳到正文
@@ -90,7 +90,7 @@ 看清应用的网络活动。
brew install --cask xinbetween/tap/flowlight拷贝
- v0.15.10 macOS 15+ 通用架构 GPL-3.0 无遥测
+ v0.15.11 macOS 15+ 通用架构 GPL-3.0 无遥测
@@ -569,7 +569,7 @@ 知道什么离开了你的 Mac。
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.15.10 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.11 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html
index 009bbca1..c780ea29 100644
--- a/docs/zh-Hans/privacy/index.html
+++ b/docs/zh-Hans/privacy/index.html
@@ -187,7 +187,7 @@
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.15.10 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.11 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html
index 255f35e5..31f959d8 100644
--- a/docs/zh-Hans/threat-model/index.html
+++ b/docs/zh-Hans/threat-model/index.html
@@ -210,7 +210,7 @@ 报告漏洞
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.15.10 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.15.11 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html
index cccccdf7..ae9e130c 100644
--- a/docs/zh-Hant/about/index.html
+++ b/docs/zh-Hant/about/index.html
@@ -152,7 +152,7 @@ 致謝
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.15.10 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.11 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html
index b710164f..469dc1d2 100644
--- a/docs/zh-Hant/docs/index.html
+++ b/docs/zh-Hant/docs/index.html
@@ -76,7 +76,7 @@
說明文件
使用 Flowlight
-
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.10,macOS 15 或以上版本。
+
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.15.11,macOS 15 或以上版本。
@@ -712,7 +712,7 @@ 限制
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.15.10 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.11 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html
index 3502cd99..a82baae8 100644
--- a/docs/zh-Hant/index.html
+++ b/docs/zh-Hant/index.html
@@ -48,7 +48,7 @@
-
+
跳至內容
@@ -90,7 +90,7 @@ 看見每個 App 的網路活動。brew install --cask xinbetween/tap/flowlight拷貝
- v0.15.10 macOS 15+ 通用架構 GPL-3.0 無遙測
+ v0.15.11 macOS 15+ 通用架構 GPL-3.0 無遙測
@@ -570,7 +570,7 @@ 知道有什麼離開了你的 Mac。
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.15.10 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.11 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html
index 88219ab1..f82d4993 100644
--- a/docs/zh-Hant/privacy/index.html
+++ b/docs/zh-Hant/privacy/index.html
@@ -187,7 +187,7 @@
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.15.10 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.11 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html
index a4802d69..289a36b2 100644
--- a/docs/zh-Hant/threat-model/index.html
+++ b/docs/zh-Hant/threat-model/index.html
@@ -211,7 +211,7 @@ 回報漏洞
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.15.10 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.15.11 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/project.yml b/project.yml
index 3c92daa2..9dd5f373 100644
--- a/project.yml
+++ b/project.yml
@@ -15,8 +15,8 @@ settings:
DEVELOPMENT_TEAM: ""
CODE_SIGN_STYLE: Automatic
ENABLE_HARDENED_RUNTIME: YES
- MARKETING_VERSION: "0.15.10"
- CURRENT_PROJECT_VERSION: "57"
+ MARKETING_VERSION: "0.15.11"
+ CURRENT_PROJECT_VERSION: "58"
configs:
AppStore:
DEVELOPMENT_TEAM: 38RJUJHKZS
diff --git a/rule-packs/official-source.json b/rule-packs/official-source.json
index 8590b339..33a9d21b 100644
--- a/rule-packs/official-source.json
+++ b/rule-packs/official-source.json
@@ -4,9 +4,9 @@
"catalogID": "flowlight.official",
"channel": "official",
"sourceURL": "https://flowlight.xinbetween.com/packs/v1/official.json",
- "sequence": 1,
- "issuedAt": 1792368000,
- "expiresAt": 1823904000,
+ "sequence": 2,
+ "issuedAt": 1793253368,
+ "expiresAt": 1824789368,
"publisher": "Flowlight",
"packs": [
{
@@ -94,43 +94,63 @@
{
"manifest": {
"id": "official.tracking",
- "version": "1",
- "generatedAt": 1792220400,
- "entryCount": 23,
- "contentSHA256": "57bdd11c751ebc24a785d429afd7d6a9282bb93e4cbe42f6725bd387fef64386",
+ "version": "2",
+ "generatedAt": 1793253368,
+ "entryCount": 43,
+ "contentSHA256": "2fa7a05ad9aba7c8ffe5fea8f7f33ca63e32a5759a4f8efe976497d6132d47b9",
"minimumAppVersion": "0.15.0",
"minimumExtensionProtocol": 1,
"titleKey": "Cross-site tracking",
- "summaryKey": "Common analytics, session replay, and cross-site measurement hosts.",
- "caveatKey": "Some apps use the same providers for feature flags. They may fall back to built-in defaults when a host is unavailable.",
+ "summaryKey": "Dedicated analytics collection, session replay, and tracker bootstrap hosts.",
+ "caveatKey": "Product dashboards stay reachable. Shared-host collection paths and first-party tracking proxies cannot be separated by hostname blocking.",
"source": "Flowlight curated built-in list",
"license": "GPL-3.0",
"format": 1
},
"entries": [
- {"id": 1, "host": "amplitude.com", "match": "suffix", "category": "tracking"},
- {"id": 2, "host": "analytics.google.com", "match": "suffix", "category": "tracking"},
- {"id": 3, "host": "assetsconfigcdn.org", "match": "suffix", "category": "tracking"},
- {"id": 4, "host": "beyondwickedmapping.org", "match": "suffix", "category": "tracking"},
- {"id": 5, "host": "browser-intake-datadoghq.com", "match": "suffix", "category": "tracking"},
- {"id": 6, "host": "clarity.ms", "match": "suffix", "category": "tracking"},
- {"id": 7, "host": "featureassets.org", "match": "suffix", "category": "tracking"},
- {"id": 8, "host": "featuregates.org", "match": "suffix", "category": "tracking"},
- {"id": 9, "host": "fullstory.com", "match": "suffix", "category": "tracking"},
- {"id": 10, "host": "google-analytics.com", "match": "suffix", "category": "tracking"},
- {"id": 11, "host": "googletagmanager.com", "match": "suffix", "category": "tracking"},
- {"id": 12, "host": "heapanalytics.com", "match": "suffix", "category": "tracking"},
- {"id": 13, "host": "lr-ingest.io", "match": "suffix", "category": "tracking"},
- {"id": 14, "host": "matomo.cloud", "match": "suffix", "category": "tracking"},
- {"id": 15, "host": "mixpanel.com", "match": "suffix", "category": "tracking"},
- {"id": 16, "host": "mxpnl.com", "match": "suffix", "category": "tracking"},
- {"id": 17, "host": "posthog.com", "match": "suffix", "category": "tracking"},
- {"id": 18, "host": "prodregistryv2.org", "match": "suffix", "category": "tracking"},
- {"id": 19, "host": "segment.com", "match": "suffix", "category": "tracking"},
- {"id": 20, "host": "segment.io", "match": "suffix", "category": "tracking"},
- {"id": 21, "host": "segmentapis.com", "match": "suffix", "category": "tracking"},
- {"id": 22, "host": "statsig.com", "match": "suffix", "category": "tracking"},
- {"id": 23, "host": "statsigapi.net", "match": "suffix", "category": "tracking"}
+ {"id": 1, "host": "api-eu.mixpanel.com", "match": "exact", "category": "tracking"},
+ {"id": 2, "host": "api-in.mixpanel.com", "match": "exact", "category": "tracking"},
+ {"id": 3, "host": "api-js.mixpanel.com", "match": "exact", "category": "tracking"},
+ {"id": 4, "host": "api.eu.amplitude.com", "match": "exact", "category": "tracking"},
+ {"id": 5, "host": "api.mixpanel.com", "match": "exact", "category": "tracking"},
+ {"id": 6, "host": "api.segment.io", "match": "exact", "category": "tracking"},
+ {"id": 7, "host": "api2.amplitude.com", "match": "exact", "category": "tracking"},
+ {"id": 8, "host": "browser-intake-ap1-datadoghq.com", "match": "exact", "category": "tracking"},
+ {"id": 9, "host": "browser-intake-ap2-datadoghq.com", "match": "exact", "category": "tracking"},
+ {"id": 10, "host": "browser-intake-datadoghq.com", "match": "suffix", "category": "tracking"},
+ {"id": 11, "host": "browser-intake-datadoghq.eu", "match": "exact", "category": "tracking"},
+ {"id": 12, "host": "browser-intake-ddog-gov.com", "match": "exact", "category": "tracking"},
+ {"id": 13, "host": "browser-intake-uk1-datadoghq.com", "match": "exact", "category": "tracking"},
+ {"id": 14, "host": "browser-intake-us2-ddog-gov.com", "match": "exact", "category": "tracking"},
+ {"id": 15, "host": "browser-intake-us3-datadoghq.com", "match": "exact", "category": "tracking"},
+ {"id": 16, "host": "browser-intake-us5-datadoghq.com", "match": "exact", "category": "tracking"},
+ {"id": 17, "host": "c.eu.heap-api.com", "match": "exact", "category": "tracking"},
+ {"id": 18, "host": "c.us.heap-api.com", "match": "exact", "category": "tracking"},
+ {"id": 19, "host": "cdn.eu.heap-api.com", "match": "exact", "category": "tracking"},
+ {"id": 20, "host": "cdn.heapanalytics.com", "match": "exact", "category": "tracking"},
+ {"id": 21, "host": "cdn.matomo.cloud", "match": "exact", "category": "tracking"},
+ {"id": 22, "host": "cdn.mxpnl.com", "match": "exact", "category": "tracking"},
+ {"id": 23, "host": "cdn.us.heap-api.com", "match": "exact", "category": "tracking"},
+ {"id": 24, "host": "clarity.ms", "match": "suffix", "category": "tracking"},
+ {"id": 25, "host": "edge.eu1.fullstory.com", "match": "exact", "category": "tracking"},
+ {"id": 26, "host": "edge.fullstory.com", "match": "exact", "category": "tracking"},
+ {"id": 27, "host": "eu-assets.i.posthog.com", "match": "exact", "category": "tracking"},
+ {"id": 28, "host": "eu.i.posthog.com", "match": "exact", "category": "tracking"},
+ {"id": 29, "host": "events.eu1.segmentapis.com", "match": "exact", "category": "tracking"},
+ {"id": 30, "host": "events.statsigapi.net", "match": "exact", "category": "tracking"},
+ {"id": 31, "host": "google-analytics.com", "match": "suffix", "category": "tracking"},
+ {"id": 32, "host": "googletagmanager.com", "match": "suffix", "category": "tracking"},
+ {"id": 33, "host": "lr-ingest.com", "match": "suffix", "category": "tracking"},
+ {"id": 34, "host": "lr-ingest.io", "match": "suffix", "category": "tracking"},
+ {"id": 35, "host": "lr-intake.com", "match": "suffix", "category": "tracking"},
+ {"id": 36, "host": "lrkt-in.com", "match": "suffix", "category": "tracking"},
+ {"id": 37, "host": "prodregistryv2.org", "match": "suffix", "category": "tracking"},
+ {"id": 38, "host": "r.logrocket.com", "match": "exact", "category": "tracking"},
+ {"id": 39, "host": "rs.eu1.fullstory.com", "match": "exact", "category": "tracking"},
+ {"id": 40, "host": "rs.fullstory.com", "match": "exact", "category": "tracking"},
+ {"id": 41, "host": "services.fullstory.com", "match": "exact", "category": "tracking"},
+ {"id": 42, "host": "us-assets.i.posthog.com", "match": "exact", "category": "tracking"},
+ {"id": 43, "host": "us.i.posthog.com", "match": "exact", "category": "tracking"}
]
}
]
diff --git a/scripts/build_site.py b/scripts/build_site.py
index 2cb5b362..04cfc85f 100755
--- a/scripts/build_site.py
+++ b/scripts/build_site.py
@@ -18,7 +18,7 @@
active nav link). Also writes sitemap.xml, robots.txt, llms.txt, llms-full.txt and 404.html, and
build/site-preview/index.html (self-contained home page).
"""
-import os, re, sys, html, json, datetime, pathlib, hashlib, subprocess
+import os, re, sys, html, json, pathlib, hashlib, subprocess
ROOT = pathlib.Path(__file__).resolve().parent.parent
SITE, OUT = ROOT / "site", ROOT / "docs"
@@ -28,6 +28,14 @@
REPO = os.environ.get("GITHUB_REPO", "xinbetween/flowlight")
DMG = f"https://github.com/{REPO}/releases/latest/download/Flowlight.dmg"
VERSION = re.search(r'MARKETING_VERSION:\s*"([^"]+)"', (ROOT / "project.yml").read_text()).group(1)
+RELEASES_SOURCE = SITE / "pages/releases.html"
+release_match = re.search(
+ rf'{re.escape(VERSION)} ',
+ RELEASES_SOURCE.read_text(),
+)
+if not release_match:
+ sys.exit(f"site/pages/releases.html has no dated release entry for {VERSION}")
+SOURCE_DATE = release_match.group(1)
def counts():
@@ -48,7 +56,7 @@ def counts():
COUNTS = counts()
-YEAR = str(datetime.date.today().year)
+YEAR = SOURCE_DATE[:4]
# The fonts are in site.css and served from this domain, so a page needs no third-party stylesheet and makes
# no request to Google to render itself. See the @font-face block at the top of site/site.css.
FONTS = ""
@@ -161,20 +169,21 @@ def one(match):
def last_changed(*paths):
- """The newest date among a generated page's inputs — or today, if any input is edited.
+ """The newest committed date among a page's inputs, bounded by the release source date.
A page is almost always rebuilt *before* the commit that changes it, so asking git alone answers with the
- previous commit's date, and the same build run after committing answers with today's. The site is then
+ previous commit's date, and the same build run after committing answers with the release date. The site is then
permanently one rebuild behind itself and CI fails on a one-line sitemap diff that nobody can see the cause
- of. An edited input is dated today, which is what the commit about to be made will say. Every page also embeds
- the version from project.yml, so a release bump is a real modification even when that page's own source is not.
+ of. An edited input therefore uses the dated release entry. Every page also embeds the version from project.yml,
+ so a release bump is a real modification even when that page's own source is not. The release date is also the
+ deterministic fallback when Git metadata is unavailable; filesystem mtimes must never affect published bytes.
"""
try:
arguments = [str(path) for path in paths]
dirty = subprocess.run(["git", "status", "--porcelain", "--", *arguments],
cwd=ROOT, capture_output=True, text=True, timeout=10)
if dirty.returncode == 0 and dirty.stdout.strip():
- return datetime.date.today().isoformat()
+ return SOURCE_DATE
dates = []
for path in paths:
out = subprocess.run(["git", "log", "-1", "--format=%cs", "--", str(path)],
@@ -182,10 +191,10 @@ def last_changed(*paths):
if out.returncode == 0 and out.stdout.strip():
dates.append(out.stdout.strip())
if dates:
- return max(dates)
+ return min(max(dates), SOURCE_DATE)
except Exception:
pass
- return max(datetime.date.fromtimestamp(pathlib.Path(path).stat().st_mtime).isoformat() for path in paths)
+ return SOURCE_DATE
def asset_hash(rel):
return hashlib.sha256((OUT / rel).read_bytes()).hexdigest()[:8]
diff --git a/scripts/rule_pack_catalog.py b/scripts/rule_pack_catalog.py
index 7fda5dc8..654246bd 100755
--- a/scripts/rule_pack_catalog.py
+++ b/scripts/rule_pack_catalog.py
@@ -43,6 +43,55 @@
PACK_ID = re.compile(r"official\.[A-Za-z0-9.-]+\Z")
HEX_SHA256 = re.compile(r"[0-9a-f]{64}\Z")
+# official.tracking is deliberately an evidence-backed allowlist. Hostname filtering cannot separate a provider's
+# product/control plane from collection paths on the same host, so every accepted entry must be a dedicated collector
+# or tracker-bootstrap endpoint reviewed alongside this policy. Adding a destination requires changing both files.
+TRACKING_ALLOWED_ENTRIES = {
+ ("api.eu.amplitude.com", "exact"),
+ ("api-eu.mixpanel.com", "exact"),
+ ("api-in.mixpanel.com", "exact"),
+ ("api-js.mixpanel.com", "exact"),
+ ("api.mixpanel.com", "exact"),
+ ("api.segment.io", "exact"),
+ ("api2.amplitude.com", "exact"),
+ ("browser-intake-ap1-datadoghq.com", "exact"),
+ ("browser-intake-ap2-datadoghq.com", "exact"),
+ ("browser-intake-datadoghq.com", "suffix"),
+ ("browser-intake-datadoghq.eu", "exact"),
+ ("browser-intake-ddog-gov.com", "exact"),
+ ("browser-intake-uk1-datadoghq.com", "exact"),
+ ("browser-intake-us2-ddog-gov.com", "exact"),
+ ("browser-intake-us3-datadoghq.com", "exact"),
+ ("browser-intake-us5-datadoghq.com", "exact"),
+ ("c.eu.heap-api.com", "exact"),
+ ("c.us.heap-api.com", "exact"),
+ ("cdn.eu.heap-api.com", "exact"),
+ ("cdn.heapanalytics.com", "exact"),
+ ("cdn.matomo.cloud", "exact"),
+ ("cdn.mxpnl.com", "exact"),
+ ("cdn.us.heap-api.com", "exact"),
+ ("clarity.ms", "suffix"),
+ ("edge.eu1.fullstory.com", "exact"),
+ ("edge.fullstory.com", "exact"),
+ ("eu-assets.i.posthog.com", "exact"),
+ ("eu.i.posthog.com", "exact"),
+ ("events.eu1.segmentapis.com", "exact"),
+ ("events.statsigapi.net", "exact"),
+ ("google-analytics.com", "suffix"),
+ ("googletagmanager.com", "suffix"),
+ ("lr-ingest.com", "suffix"),
+ ("lr-ingest.io", "suffix"),
+ ("lr-intake.com", "suffix"),
+ ("lrkt-in.com", "suffix"),
+ ("prodregistryv2.org", "suffix"),
+ ("r.logrocket.com", "exact"),
+ ("rs.eu1.fullstory.com", "exact"),
+ ("rs.fullstory.com", "exact"),
+ ("services.fullstory.com", "exact"),
+ ("us-assets.i.posthog.com", "exact"),
+ ("us.i.posthog.com", "exact"),
+}
+
class CatalogError(ValueError):
"""The catalog cannot be validated or canonicalized."""
@@ -163,7 +212,7 @@ def validate_der_signature(data, where):
fail(f"{where} has extra DER elements")
-def validate(catalog, *, require_signature=False):
+def validate(catalog, *, require_signature=False, enforce_tracking_policy=True):
exact_fields(catalog, ENVELOPE_FIELDS, "catalog")
integer(catalog["format"], "catalog.format", 1, 1)
payload = catalog["payload"]
@@ -232,6 +281,10 @@ def validate(catalog, *, require_signature=False):
match = string(entry["match"], f"{entry_where}.match")
if match not in ("exact", "suffix"):
fail(f"{entry_where}.match must be exact or suffix")
+ if (enforce_tracking_policy and pack_id == "official.tracking"
+ and (host, match) not in TRACKING_ALLOWED_ENTRIES):
+ fail(f"{entry_where}.host is not an approved dedicated tracking endpoint: "
+ f"{host!r} with {match} matching")
host_key = f"{host}|{match}"
if host_key in host_keys:
fail(f"{entry_where}.host and match are duplicated")
@@ -239,6 +292,8 @@ def validate(catalog, *, require_signature=False):
category = string(entry["category"], f"{entry_where}.category")
if category not in ("advertising", "tracking", "telemetry"):
fail(f"{entry_where}.category is unsupported")
+ if pack_id == "official.tracking" and category != "tracking":
+ fail(f"{entry_where}.category must be tracking for official.tracking")
if previous_key is not None and previous_key >= host_key:
fail(f"{pack_where}.entries are not strictly sorted by host|match")
previous_key = host_key
@@ -295,9 +350,9 @@ def text(value):
return u32(len(encoded)) + encoded
-def canonical_bytes(catalog):
+def canonical_bytes(catalog, *, enforce_tracking_policy=True):
"""Return canonical signed bytes, raising CatalogError for every invalid input."""
- validate(catalog)
+ validate(catalog, enforce_tracking_policy=enforce_tracking_policy)
payload = catalog["payload"]
out = bytearray(text(DOMAIN))
out += u16(catalog["format"])
@@ -337,7 +392,7 @@ def no_duplicate_object(pairs):
return result
-def decode(raw, source, *, require_signature=False):
+def decode(raw, source, *, require_signature=False, enforce_tracking_policy=True):
if len(raw) > MAX_FILE_BYTES:
fail(f"{source} exceeds the {MAX_FILE_BYTES}-byte limit")
try:
@@ -346,19 +401,21 @@ def decode(raw, source, *, require_signature=False):
fail(f"{source} is not UTF-8")
except json.JSONDecodeError as error:
fail(f"{source} is not valid JSON: {error.msg} at line {error.lineno}, column {error.colno}")
- return validate(catalog, require_signature=require_signature)
+ return validate(catalog, require_signature=require_signature,
+ enforce_tracking_policy=enforce_tracking_policy)
-def load(path, *, require_signature=False):
+def load(path, *, require_signature=False, enforce_tracking_policy=True):
path = pathlib.Path(path)
try:
raw = path.read_bytes()
except OSError as error:
fail(f"cannot read {path}: {error.strerror}")
- return decode(raw, str(path), require_signature=require_signature)
+ return decode(raw, str(path), require_signature=require_signature,
+ enforce_tracking_policy=enforce_tracking_policy)
-def load_git(ref, path, *, require_signature=False):
+def load_git(ref, path, *, require_signature=False, enforce_tracking_policy=True):
repository = pathlib.Path(__file__).resolve().parent.parent
try:
result = subprocess.run(
@@ -368,7 +425,8 @@ def load_git(ref, path, *, require_signature=False):
fail("git could not read the previous catalog")
if result.returncode:
fail(f"git could not read previous catalog {ref}:{path}")
- return decode(result.stdout, f"{ref}:{path}", require_signature=require_signature)
+ return decode(result.stdout, f"{ref}:{path}", require_signature=require_signature,
+ enforce_tracking_policy=enforce_tracking_policy)
def serialized(catalog):
@@ -450,12 +508,13 @@ def command_compare(args):
def command_compare_previous(args):
current = load(args.current, require_signature=True)
- previous = (load(args.previous, require_signature=True) if args.previous else
- load_git(args.previous_ref, args.previous_path, require_signature=True))
+ previous = (load(args.previous, require_signature=True, enforce_tracking_policy=False) if args.previous else
+ load_git(args.previous_ref, args.previous_path, require_signature=True,
+ enforce_tracking_policy=False))
current_sequence = current["payload"]["sequence"]
previous_sequence = previous["payload"]["sequence"]
current_digest = hashlib.sha256(canonical_bytes(current)).hexdigest()
- previous_digest = hashlib.sha256(canonical_bytes(previous)).hexdigest()
+ previous_digest = hashlib.sha256(canonical_bytes(previous, enforce_tracking_policy=False)).hexdigest()
if current_sequence < previous_sequence:
fail(f"catalog sequence rolled back from {previous_sequence} to {current_sequence}")
if current_sequence == previous_sequence and current_digest != previous_digest:
@@ -484,7 +543,8 @@ def command_sign(args):
def command_verify(args):
- catalog = load(args.input, require_signature=True)
+ catalog = load(args.input, require_signature=True,
+ enforce_tracking_policy=not args.historical)
if args.current_app_version is not None:
current = version_tuple(args.current_app_version, "--current-app-version")
for pack in catalog["payload"]["packs"]:
@@ -509,7 +569,7 @@ def command_verify(args):
fail("catalog is expired")
if len(args.public_key) != len(args.expected_key_id):
fail("each --expected-key-id must have one corresponding --public-key")
- canonical = canonical_bytes(catalog)
+ canonical = canonical_bytes(catalog, enforce_tracking_policy=not args.historical)
for expected_value, public_key in zip(args.expected_key_id, args.public_key):
expected = string(expected_value, "--expected-key-id")
matches = [signature for signature in catalog["signatures"] if signature["keyID"] == expected]
@@ -570,6 +630,8 @@ def parser():
verify.add_argument("--current-app-version")
verify.add_argument("--extension-protocol", type=int)
verify.add_argument("--now", type=int, help="also require the signed acceptance window to contain this Unix time")
+ verify.add_argument("--historical", action="store_true",
+ help="verify an immutable prior catalog without applying the current endpoint policy")
verify.set_defaults(run=command_verify)
digest = commands.add_parser("digest", help="print each validated pack's entry digest")
digest.add_argument("input", type=pathlib.Path)
diff --git a/scripts/test-rule-pack-policy.sh b/scripts/test-rule-pack-policy.sh
new file mode 100755
index 00000000..6789fdc1
--- /dev/null
+++ b/scripts/test-rule-pack-policy.sh
@@ -0,0 +1,48 @@
+#!/bin/bash
+set -euo pipefail
+
+cd "$(dirname "$0")/.."
+temporary=$(mktemp -d "${TMPDIR:-/tmp}/flowlight-pack-policy.XXXXXX")
+trap 'rm -rf "$temporary"' EXIT
+
+python3 -I scripts/rule_pack_catalog.py validate rule-packs/official-source.json > "$temporary/valid-output"
+
+python3 -I - rule-packs/official-source.json "$temporary/unreviewed-host.json" <<'PY'
+import json
+import pathlib
+import sys
+
+source = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+tracking = next(pack for pack in source["payload"]["packs"] if pack["manifest"]["id"] == "official.tracking")
+tracking["entries"][0]["host"] = "amplitude.com"
+tracking["entries"][0]["match"] = "suffix"
+pathlib.Path(sys.argv[2]).write_text(json.dumps(source, indent=2) + "\n", encoding="utf-8")
+PY
+
+if python3 -I scripts/rule_pack_catalog.py validate "$temporary/unreviewed-host.json" \
+ > "$temporary/unreviewed-output" 2>&1; then
+ echo "unreviewed tracking host was not rejected" >&2
+ exit 1
+fi
+grep -Fq "is not an approved dedicated tracking endpoint: 'amplitude.com' with suffix matching" \
+ "$temporary/unreviewed-output"
+
+python3 -I - rule-packs/official-source.json "$temporary/wrong-category.json" <<'PY'
+import json
+import pathlib
+import sys
+
+source = json.loads(pathlib.Path(sys.argv[1]).read_text(encoding="utf-8"))
+tracking = next(pack for pack in source["payload"]["packs"] if pack["manifest"]["id"] == "official.tracking")
+tracking["entries"][0]["category"] = "advertising"
+pathlib.Path(sys.argv[2]).write_text(json.dumps(source, indent=2) + "\n", encoding="utf-8")
+PY
+
+if python3 -I scripts/rule_pack_catalog.py validate "$temporary/wrong-category.json" \
+ > "$temporary/category-output" 2>&1; then
+ echo "misclassified tracking endpoint was not rejected" >&2
+ exit 1
+fi
+grep -Fq "category must be tracking for official.tracking" "$temporary/category-output"
+
+echo "Rule-pack policy fixtures passed: reviewed collectors allowed, product and unreviewed hosts rejected."
diff --git a/site/packs/v1/official.json b/site/packs/v1/official.json
index ea4a1c66..de463d5f 100644
--- a/site/packs/v1/official.json
+++ b/site/packs/v1/official.json
@@ -4,9 +4,9 @@
"catalogID": "flowlight.official",
"channel": "official",
"sourceURL": "https://flowlight.xinbetween.com/packs/v1/official.json",
- "sequence": 1,
- "issuedAt": 1792368000,
- "expiresAt": 1823904000,
+ "sequence": 2,
+ "issuedAt": 1793253368,
+ "expiresAt": 1824789368,
"publisher": "Flowlight",
"packs": [
{
@@ -314,15 +314,15 @@
{
"manifest": {
"id": "official.tracking",
- "version": "1",
- "generatedAt": 1792220400,
- "entryCount": 23,
- "contentSHA256": "57bdd11c751ebc24a785d429afd7d6a9282bb93e4cbe42f6725bd387fef64386",
+ "version": "2",
+ "generatedAt": 1793253368,
+ "entryCount": 43,
+ "contentSHA256": "2fa7a05ad9aba7c8ffe5fea8f7f33ca63e32a5759a4f8efe976497d6132d47b9",
"minimumAppVersion": "0.15.0",
"minimumExtensionProtocol": 1,
"titleKey": "Cross-site tracking",
- "summaryKey": "Common analytics, session replay, and cross-site measurement hosts.",
- "caveatKey": "Some apps use the same providers for feature flags. They may fall back to built-in defaults when a host is unavailable.",
+ "summaryKey": "Dedicated analytics collection, session replay, and tracker bootstrap hosts.",
+ "caveatKey": "Product dashboards stay reachable. Shared-host collection paths and first-party tracking proxies cannot be separated by hostname blocking.",
"source": "Flowlight curated built-in list",
"license": "GPL-3.0",
"format": 1
@@ -330,141 +330,261 @@
"entries": [
{
"id": 1,
- "host": "amplitude.com",
- "match": "suffix",
+ "host": "api-eu.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 2,
- "host": "analytics.google.com",
- "match": "suffix",
+ "host": "api-in.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 3,
- "host": "assetsconfigcdn.org",
- "match": "suffix",
+ "host": "api-js.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 4,
- "host": "beyondwickedmapping.org",
- "match": "suffix",
+ "host": "api.eu.amplitude.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 5,
- "host": "browser-intake-datadoghq.com",
- "match": "suffix",
+ "host": "api.mixpanel.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 6,
- "host": "clarity.ms",
- "match": "suffix",
+ "host": "api.segment.io",
+ "match": "exact",
"category": "tracking"
},
{
"id": 7,
- "host": "featureassets.org",
- "match": "suffix",
+ "host": "api2.amplitude.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 8,
- "host": "featuregates.org",
- "match": "suffix",
+ "host": "browser-intake-ap1-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 9,
- "host": "fullstory.com",
- "match": "suffix",
+ "host": "browser-intake-ap2-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 10,
- "host": "google-analytics.com",
+ "host": "browser-intake-datadoghq.com",
"match": "suffix",
"category": "tracking"
},
{
"id": 11,
- "host": "googletagmanager.com",
- "match": "suffix",
+ "host": "browser-intake-datadoghq.eu",
+ "match": "exact",
"category": "tracking"
},
{
"id": 12,
- "host": "heapanalytics.com",
- "match": "suffix",
+ "host": "browser-intake-ddog-gov.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 13,
- "host": "lr-ingest.io",
- "match": "suffix",
+ "host": "browser-intake-uk1-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 14,
- "host": "matomo.cloud",
- "match": "suffix",
+ "host": "browser-intake-us2-ddog-gov.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 15,
- "host": "mixpanel.com",
- "match": "suffix",
+ "host": "browser-intake-us3-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 16,
- "host": "mxpnl.com",
- "match": "suffix",
+ "host": "browser-intake-us5-datadoghq.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 17,
- "host": "posthog.com",
- "match": "suffix",
+ "host": "c.eu.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 18,
- "host": "prodregistryv2.org",
- "match": "suffix",
+ "host": "c.us.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 19,
- "host": "segment.com",
- "match": "suffix",
+ "host": "cdn.eu.heap-api.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 20,
- "host": "segment.io",
- "match": "suffix",
+ "host": "cdn.heapanalytics.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 21,
- "host": "segmentapis.com",
- "match": "suffix",
+ "host": "cdn.matomo.cloud",
+ "match": "exact",
"category": "tracking"
},
{
"id": 22,
- "host": "statsig.com",
- "match": "suffix",
+ "host": "cdn.mxpnl.com",
+ "match": "exact",
"category": "tracking"
},
{
"id": 23,
- "host": "statsigapi.net",
+ "host": "cdn.us.heap-api.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 24,
+ "host": "clarity.ms",
"match": "suffix",
"category": "tracking"
+ },
+ {
+ "id": 25,
+ "host": "edge.eu1.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 26,
+ "host": "edge.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 27,
+ "host": "eu-assets.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 28,
+ "host": "eu.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 29,
+ "host": "events.eu1.segmentapis.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 30,
+ "host": "events.statsigapi.net",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 31,
+ "host": "google-analytics.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 32,
+ "host": "googletagmanager.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 33,
+ "host": "lr-ingest.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 34,
+ "host": "lr-ingest.io",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 35,
+ "host": "lr-intake.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 36,
+ "host": "lrkt-in.com",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 37,
+ "host": "prodregistryv2.org",
+ "match": "suffix",
+ "category": "tracking"
+ },
+ {
+ "id": 38,
+ "host": "r.logrocket.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 39,
+ "host": "rs.eu1.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 40,
+ "host": "rs.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 41,
+ "host": "services.fullstory.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 42,
+ "host": "us-assets.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
+ },
+ {
+ "id": 43,
+ "host": "us.i.posthog.com",
+ "match": "exact",
+ "category": "tracking"
}
]
}
@@ -474,7 +594,7 @@
{
"algorithm": "p256-sha256",
"keyID": "flowlight-rule-packs-2026-01",
- "value": "MEQCIHugfQ8odXRdT6TKDoM7j22LzsvRMKUrCAqOSAs4eB1GAiBR15TV4oaid3NW/qkq9QXoF++0G4IrckUmod1ClDhdTg=="
+ "value": "MEUCIGfUrVoocSM9MzBY8lza2UgWgJzS6o7fMS8A5MU0FoqkAiEA65tGvNTF6Lzmc/tyvTcOLDWZb9v9QIy3RpxsDYoL+6g="
}
]
}
diff --git a/site/pages/docs.html b/site/pages/docs.html
index 6b41be8c..3e283568 100644
--- a/site/pages/docs.html
+++ b/site/pages/docs.html
@@ -418,6 +418,12 @@ Managed ad and tracker packs
pack would otherwise refuse. The global Pause Blocking control pauses managed packs along with the rest of
connection blocking. Pack exceptions are narrower conveniences; ordinary rules remain the place for a deliberate policy that
should apply beyond one pack.
+ Block collection, not the product. Cross-site tracking entries use documented dedicated collection, replay and
+ tracker-bootstrap hosts, while provider dashboards, login, billing, documentation and administration stay reachable. When a
+ collection path shares the product hostname — for example analytics.google.com/g/collect or a Matomo tenant's
+ /matomo.php — hostname blocking must allow it. HTTPS inspection can apply an explicit URL rule only when that path is
+ visible; a pinned or otherwise opaque TLS connection cannot be separated safely. First-party tracking proxies are not discoverable
+ from a vendor-host list.
This is hostname-level network blocking, not content filtering. A Network Extension can refuse a connection,
but it cannot edit what a browser or app draws. Packs therefore cannot remove first-party ads, DOM or layout gaps left behind by
blocked content, browser overlays, scripted popups, or native app windows. ECH, QUIC, encrypted DNS, direct-IP connections and
diff --git a/site/pages/index.html b/site/pages/index.html
index 5509d049..60bb2a8f 100644
--- a/site/pages/index.html
+++ b/site/pages/index.html
@@ -185,9 +185,10 @@
Monitor how AI agents use the network
With the Network Extension, apply one to every app or only selected apps; preview it against retained traffic,
see grouped, explainable activity, and add pack-specific exceptions for an app. Ordinary allow rules remain the universal
escape hatch, global Pause Blocking applies, and essential system traffic stays allowed.
- They block by hostname, so they cannot remove first-party ads, DOM or layout gaps, browser overlays, scripted popups, or
- native app windows. ECH, QUIC, encrypted DNS, direct IPs and shared CDNs can limit coverage. Cosmetic popup filtering needs
- a later browser extension that can change the page.
+ They block dedicated collection hosts while keeping analytics dashboards and administration reachable. Shared-host
+ collection paths and first-party tracking proxies cannot always be separated safely; ECH, QUIC, encrypted DNS, direct IPs
+ and shared CDNs also limit coverage. Packs cannot remove first-party ads, page gaps, overlays or popups — cosmetic filtering
+ needs a browser extension that can change the page.
Safari · Advertising matched doubleclick.net · 18 connections grouped
diff --git a/site/pages/releases.html b/site/pages/releases.html
index 3c43c86f..7e19edd4 100644
--- a/site/pages/releases.html
+++ b/site/pages/releases.html
@@ -13,7 +13,20 @@
What's new
- 0.15.10 October 28, 2026 Latest
+ 0.15.11 October 28, 2026 Latest
+
+ Block tracking traffic without blocking the product. The Tracking pack now targets documented dedicated
+ collection, replay, and tracker-bootstrap hosts while leaving provider dashboards, login, billing, documentation, and
+ administration reachable.
+ Keep broad vendor domains out. Exact collector rules replace shared product and feature-configuration
+ suffixes across Amplitude, FullStory, Heap, Matomo, Mixpanel, PostHog, Segment, Statsig, and other providers.
+ Make the boundary durable and explicit. Catalog policy fixtures reject unreviewed tracking destinations,
+ matcher tests cover product and collector hosts, and the docs explain shared-host TLS paths and first-party proxy limits.
+
+
+
+
+
Make the full test target warning-free. Concurrency captures, redundant actor awaits, and intentionally ignored
database insert IDs now compile cleanly with current Xcode.