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,

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

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

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.

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 @@ - + @@ -90,7 +90,7 @@

Sieh, was deine Apps im Netz tun.
brew install --cask xinbetween/tap/flowlight

-

v0.15.10macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.15.11macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -572,7 +572,7 @@

Wisse, was deinen Mac verlässt.

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 @@

Kontakt

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

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.

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

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.

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 @@ - + @@ -90,7 +90,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.10macOS 15+UniversalGPL-3.0Sin telemetría

+

v0.15.11macOS 15+UniversalGPL-3.0Sin telemetría

@@ -572,7 +572,7 @@

Ten claro qué sale de tu Mac.

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 @@

Contacto

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

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

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.

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 @@ - + @@ -90,7 +90,7 @@

Voyez l’activité réseau de vos apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.10macOS 15+UniverselGPL-3.0Sans télémétrie

+

v0.15.11macOS 15+UniverselGPL-3.0Sans télémétrie

@@ -572,7 +572,7 @@

Sachez ce qui quitte votre Mac.

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 @@

Contact

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é

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 @@ - + @@ -90,7 +90,7 @@

See your apps' network activity.
brew install --cask xinbetween/tap/flowlight

-

v0.15.10macOS 15+UniversalGPL-3.0No telemetry

+

v0.15.11macOS 15+UniversalGPL-3.0No 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.

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

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.

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 @@ - + @@ -90,7 +90,7 @@

Vedi la rete delle tue app.
Ca Metti una stella su GitHub

brew install --cask xinbetween/tap/flowlight

-

v0.15.10macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.15.11macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -572,7 +572,7 @@

Sappi che cosa esce dal tuo Mac.

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 @@

Contatti

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à

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 @@

謝辞

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 以降。

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.10macOS 15+ユニバーサルGPL-3.0テレメトリなし

+

v0.15.11macOS 15+ユニバーサルGPL-3.0テレメトリなし

@@ -571,7 +571,7 @@

Mac から何が出ていくのかを知る。

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 @@

連絡先

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 @@

脆弱性を報告する

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 @@

감사

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 이상.

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.10macOS 15+유니버설GPL-3.0텔레메트리 없음

+

v0.15.11macOS 15+유니버설GPL-3.0텔레메트리 없음

@@ -572,7 +572,7 @@

내 Mac에서 무엇이 나가는지 아세요.

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 @@

문의

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 @@

취약점 신고하기

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

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 @@

Contact

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

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.

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 @@ - + @@ -90,7 +90,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.15.10macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.15.11macOS 15+UniversalGPL-3.0Sem telemetria

@@ -572,7 +572,7 @@

Saiba o que sai do seu Mac.

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 @@

Contacto

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

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

Latest
+

0.15.11

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.
  • +
+
+ +
+

0.15.10

  • 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

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

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 @@

致谢

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 或更高版本。

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.10macOS 15+通用架构GPL-3.0无遥测

+

v0.15.11macOS 15+通用架构GPL-3.0无遥测

@@ -569,7 +569,7 @@

知道什么离开了你的 Mac。

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 @@

联系方式

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 @@

报告漏洞

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 @@

致謝

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 或以上版本。

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.10macOS 15+通用架構GPL-3.0無遙測

+

v0.15.11macOS 15+通用架構GPL-3.0無遙測

@@ -570,7 +570,7 @@

知道有什麼離開了你的 Mac。

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 @@

聯絡

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 @@

回報漏洞

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)}