diff --git a/Flowlight/Inspection/RewriteRule.swift b/Flowlight/Inspection/RewriteRule.swift index e0ee605..054dda3 100644 --- a/Flowlight/Inspection/RewriteRule.swift +++ b/Flowlight/Inspection/RewriteRule.swift @@ -22,9 +22,10 @@ struct HeaderEdit: Codable, Equatable, Identifiable, Sendable { } } -/// An edit to the request's JSON body, addressed by a dotted key path into objects (`metadata.user`). `set` creates -/// the path if needed; `remove` deletes the leaf. The value is parsed as JSON when it can be (`0.7`, `true`, -/// `{"a":1}`) and taken as a plain string otherwise. +/// An edit to the request's body. For a JSON body, `path` is a dotted key path into objects (`metadata.user`): +/// `set` creates the path if needed, `remove` deletes the leaf, and the value is parsed as JSON when it can be +/// (`0.7`, `true`, `{"a":1}`) and taken as a plain string otherwise. For a form body +/// (`application/x-www-form-urlencoded`), `path` is a field name taken whole and the value is used as typed. struct BodyEdit: Codable, Equatable, Identifiable, Sendable { enum Op: String, Codable, Sendable, CaseIterable { case set, remove } var id = UUID() @@ -46,8 +47,7 @@ struct BodyEdit: Codable, Equatable, Identifiable, Sendable { } } -/// A rule that rewrites a matching outgoing request before it is forwarded upstream — changing headers or the JSON -/// body. Like a mock, but it edits the request and lets it through rather than answering it: add an `Authorization` +/// A rule that rewrites a matching outgoing request before it is forwarded upstream — changing headers or the body (JSON or form). Like a mock, but it edits the request and lets it through rather than answering it: add an `Authorization` /// header, pin `model`, strip a tracking field. Matched like a mock (host / path glob / method), and applied by the /// same intervention path the guardrails use. Only requests Flowlight decrypts and can buffer (bodies up to a few MB, /// not chunked or streamed) can be rewritten. @@ -139,18 +139,35 @@ enum RewriteRules { } let bodyEdits = applicable.flatMap(\.body) - if !bodyEdits.isEmpty, !bodyData.isEmpty, - var json = (try? JSONSerialization.jsonObject(with: bodyData)) as? [String: Any] { - var changed = false - for edit in bodyEdits { - let comps = edit.path.split(separator: ".").map(String.init) - guard !comps.isEmpty else { continue } - switch edit.op { - case .set: setJSON(&json, path: comps, value: parseValue(edit.value)); changed = true; notes.append("set \(edit.path)") - case .remove: removeJSON(&json, path: comps); changed = true; notes.append("removed \(edit.path)") + if !bodyEdits.isEmpty, !bodyData.isEmpty { + if var json = (try? JSONSerialization.jsonObject(with: bodyData)) as? [String: Any] { + var changed = false + for edit in bodyEdits { + let comps = edit.path.split(separator: ".").map(String.init) + guard !comps.isEmpty else { continue } + switch edit.op { + case .set: setJSON(&json, path: comps, value: parseValue(edit.value)); changed = true; notes.append("set \(edit.path)") + case .remove: removeJSON(&json, path: comps); changed = true; notes.append("removed \(edit.path)") + } } + if changed, let out = try? JSONSerialization.data(withJSONObject: json) { bodyData = out } + } else if isFormEncoded(headerLines), var form = FormBody(bodyData) { + // A form body is flat, so a path is a field name taken whole — `a.b` means a field literally + // named `a.b`, not a nested one. The value is always text; forms have no types, so it is used + // verbatim rather than through `parseValue`. + var changed = false + for edit in bodyEdits { + let field = edit.path + guard !field.isEmpty else { continue } + switch edit.op { + case .set: + form.set(field, to: edit.value); changed = true; notes.append("set \(field)") + case .remove: + if form.remove(field) { changed = true; notes.append("removed \(field)") } + } + } + if changed { bodyData = form.encoded() } } - if changed, let out = try? JSONSerialization.data(withJSONObject: json) { bodyData = out } } guard !notes.isEmpty else { return nil } @@ -174,6 +191,19 @@ enum RewriteRules { return s } + /// Whether the request carries an `application/x-www-form-urlencoded` body, from its Content-Type header. + /// + /// A `charset` or other parameter may follow (`...; charset=utf-8`), so this matches the media type as a + /// prefix rather than the whole value. + static func isFormEncoded(_ headerLines: [String]) -> Bool { + for line in headerLines where line.lowercased().hasPrefix("content-type:") { + let value = line.drop(while: { $0 != ":" }).dropFirst() + .trimmingCharacters(in: .whitespaces).lowercased() + return value.hasPrefix("application/x-www-form-urlencoded") + } + return false + } + private static func setJSON(_ object: inout [String: Any], path: [String], value: Any) { guard let key = path.first else { return } if path.count == 1 { object[key] = value; return } @@ -190,3 +220,70 @@ enum RewriteRules { object[key] = child } } + +/// An `application/x-www-form-urlencoded` body as its ordered `name=value` pairs. +/// +/// Ordered rather than a dictionary, and able to hold the same name twice, because a form can — and a rewrite +/// that silently collapsed `tag=a&tag=b` into one field would change a request in a way nobody asked for. Set +/// edits the first pair of that name in place (or appends when there is none); remove drops every pair of that +/// name. Decoding and re-encoding follow the form rules: `+` is a space, everything else is percent-encoded. +struct FormBody { + private var pairs: [(name: String, value: String)] + + /// Parses a form body. Fails only if the bytes are not UTF-8; an empty or shapeless body parses to no pairs, + /// which is a body a `set` can still add a field to. + init?(_ data: Data) { + guard let text = String(data: data, encoding: .utf8) else { return nil } + pairs = [] + guard !text.isEmpty else { return } + for part in text.components(separatedBy: "&") where !part.isEmpty { + if let eq = part.firstIndex(of: "=") { + let name = Self.decode(String(part[part.startIndex.. Bool { + let before = pairs.count + pairs.removeAll { $0.name == name } + return pairs.count != before + } + + /// Re-encodes the pairs. Field order is preserved so a diff reads as the one change that was made. + func encoded() -> Data { + let body = pairs.map { "\(Self.encode($0.name))=\(Self.encode($0.value))" }.joined(separator: "&") + return Data(body.utf8) + } + + /// Form-decode one component: `+` is a space, then `%XX` is a byte. A malformed `%` is left as written + /// rather than dropped, so a value is never silently truncated. + private static func decode(_ s: String) -> String { + s.replacingOccurrences(of: "+", with: " ").removingPercentEncoding + ?? s.replacingOccurrences(of: "+", with: " ") + } + + /// Form-encode one component. Everything outside the unreserved set is percent-encoded and space becomes + /// `+`, which is what a browser emits — so `http://localhost/admin` becomes `http%3A%2F%2Flocalhost%2Fadmin`. + private static func encode(_ s: String) -> String { + let unreserved = CharacterSet(charactersIn: + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789*-._ ") + let percent = s.addingPercentEncoding(withAllowedCharacters: unreserved) ?? s + return percent.replacingOccurrences(of: " ", with: "+") + } +} diff --git a/Flowlight/UI/RewriteRulesView.swift b/Flowlight/UI/RewriteRulesView.swift index f3a7f02..4a11a43 100644 --- a/Flowlight/UI/RewriteRulesView.swift +++ b/Flowlight/UI/RewriteRulesView.swift @@ -1,6 +1,6 @@ import SwiftUI -/// Modify requests: rules that edit an outgoing request's headers or JSON body before it's forwarded. Sits with the +/// Modify requests: rules that edit an outgoing request's headers or body (JSON or form) before it's forwarded. Sits with the /// rest of inspection setup — a rule can only change a request Flowlight decrypts. struct RewriteRulesSection: View { @ObservedObject var inspection: InspectionController @@ -9,7 +9,7 @@ struct RewriteRulesSection: View { var body: some View { VStack(alignment: .leading, spacing: 10) { - Text(L("Change a request on its way out — add or replace a header, pin a field in the JSON body, or strip one — and let it continue to the server. Like a mock, but it edits the request instead of answering it.")) + Text(L("Change a request on its way out — add or replace a header, pin a field in the JSON or form body, or strip one — and let it continue to the server. Like a mock, but it edits the request instead of answering it.")) .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) Label(L("Only requests Flowlight decrypts can be rewritten, and only buffered ones — bodies up to a few megabytes. Tunnelled, pinned, chunked or streamed uploads pass through untouched."), systemImage: "info.circle") @@ -155,7 +155,7 @@ struct RewriteRuleEditor: View { // Body edits VStack(alignment: .leading, spacing: 6) { - Text(L("JSON body")).font(.caption.bold()).foregroundStyle(.secondary) + Text(L("Request body")).font(.caption.bold()).foregroundStyle(.secondary) ForEach($rule.body) { $edit in HStack(spacing: 6) { Picker("", selection: $edit.op) { @@ -171,7 +171,7 @@ struct RewriteRuleEditor: View { } } Button(L("Add body edit")) { rule.body.append(BodyEdit()) }.controlSize(.small) - Text(L("Dotted path into the JSON object (metadata.user). A value that is valid JSON (0.7, true, {\"a\":1}) is used as-is; anything else is a string. Only requests with a JSON body are changed.")) + Text(L("For a JSON body, a dotted path into the object (metadata.user); a value that is valid JSON (0.7, true, {\"a\":1}) is used as-is, anything else is a string. For a form body (application/x-www-form-urlencoded), the field name, taken whole, set to the text as typed. Only requests with one of those two bodies are changed.")) .font(.caption).foregroundStyle(.secondary).fixedSize(horizontal: false, vertical: true) } diff --git a/FlowlightTests/RewriteRuleTests.swift b/FlowlightTests/RewriteRuleTests.swift index bf28c02..0e869a5 100644 --- a/FlowlightTests/RewriteRuleTests.swift +++ b/FlowlightTests/RewriteRuleTests.swift @@ -103,6 +103,83 @@ final class RewriteRuleTests: XCTestCase { XCTAssertTrue(parts(out).head.contains("X-Tag: 1")) } + // MARK: Form bodies + + private let form = "Content-Type: application/x-www-form-urlencoded" + + private func bodyForm(_ data: Data) -> [String: String] { + guard let sep = data.range(of: Data("\r\n\r\n".utf8)) else { return [:] } + let body = String(decoding: data[sep.upperBound...], as: UTF8.self) + var out: [String: String] = [:] + for part in body.components(separatedBy: "&") where !part.isEmpty { + let halves = part.components(separatedBy: "=") + let name = (halves.first ?? "").removingPercentEncoding ?? "" + let value = halves.dropFirst().joined(separator: "=") + .replacingOccurrences(of: "+", with: " ").removingPercentEncoding ?? "" + out[name] = value + } + return out + } + + /// The reported case: a form field set to a URL, the value percent-encoded, Content-Length reframed. This is + /// what the "Set stockApi = http://localhost/admin" rule did nothing for, because the body is a form, not JSON. + func testFormSetEncodesValue() throws { + let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "stockApi", value: "http://localhost/admin")]) + let out = try XCTUnwrap(apply(rule, request("POST", "/product/stock", headers: [form], + body: "stockApi=http%3A%2F%2Fstock.example.net%3A8080%2Fcheck"), + path: "/product/stock")) + XCTAssertEqual(bodyForm(out)["stockApi"], "http://localhost/admin") + // The value is percent-encoded on the wire, and Content-Length agrees with the final body. + XCTAssertTrue(parts(out).body.contains("http%3A%2F%2Flocalhost%2Fadmin")) + let declared = parts(out).head.first { $0.lowercased().hasPrefix("content-length:") }? + .components(separatedBy: ": ").last.flatMap { Int($0) } + let sep = try XCTUnwrap(out.range(of: Data("\r\n\r\n".utf8))) + XCTAssertEqual(declared, out.distance(from: sep.upperBound, to: out.endIndex)) + } + + /// Set adds a field that was not there, leaving the others in place and in order. + func testFormSetAppendsNewField() throws { + let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "role", value: "admin")]) + let out = try XCTUnwrap(apply(rule, request("POST", "/login", headers: [form], body: "user=alice&pass=x"), path: "/login")) + XCTAssertEqual(bodyForm(out)["user"], "alice") + XCTAssertEqual(bodyForm(out)["pass"], "x") + XCTAssertEqual(bodyForm(out)["role"], "admin") + } + + /// Remove drops a field; removing one that is not there is no change. + func testFormRemove() throws { + let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .remove, path: "csrf")]) + let out = try XCTUnwrap(apply(rule, request("POST", "/login", headers: [form], body: "user=alice&csrf=tok"), path: "/login")) + XCTAssertNil(bodyForm(out)["csrf"]) + XCTAssertEqual(bodyForm(out)["user"], "alice") + + let miss = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .remove, path: "nope")]) + XCTAssertNil(apply(miss, request("POST", "/login", headers: [form], body: "user=alice"), path: "/login")) + } + + /// Set replaces the first pair of a repeated field and leaves the rest, rather than collapsing them. + func testFormSetReplacesInPlaceKeepingOrder() throws { + let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "tag", value: "z")]) + let out = try XCTUnwrap(apply(rule, request("POST", "/x", headers: [form], body: "tag=a&tag=b&keep=1"), path: "/x")) + let body = String(decoding: out[(out.range(of: Data("\r\n\r\n".utf8))!.upperBound)...], as: UTF8.self) + XCTAssertEqual(body, "tag=z&tag=b&keep=1") + } + + /// Without the form Content-Type, a body shaped like a form is still left untouched — the header is the signal. + func testFormBodyWithoutContentTypeIsNotTouched() { + let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "a", value: "2")]) + XCTAssertNil(apply(rule, request("POST", "/x", body: "a=1&b=2"), path: "/x")) + } + + /// A charset parameter after the media type does not stop it being recognised as a form. + func testFormContentTypeWithCharset() throws { + let rule = RewriteRule(host: "api.example.com", body: [BodyEdit(op: .set, path: "a", value: "2")]) + let out = try XCTUnwrap(apply(rule, request("POST", "/x", + headers: ["Content-Type: application/x-www-form-urlencoded; charset=utf-8"], + body: "a=1"), path: "/x")) + XCTAssertEqual(bodyForm(out)["a"], "2") + } + // MARK: Matching /// A rule for another host, method or path doesn't touch the request. diff --git a/docs/404.html b/docs/404.html index f871b1f..fa38be0 100644 --- a/docs/404.html +++ b/docs/404.html @@ -80,7 +80,7 @@

That page isn't here

Try the home page,

diff --git a/docs/about/index.html b/docs/about/index.html index 7bd1646..0cd7817 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -138,7 +138,7 @@

Thanks

diff --git a/docs/de/about/index.html b/docs/de/about/index.html index f231b8e..fdd3c5d 100644 --- a/docs/de/about/index.html +++ b/docs/de/about/index.html @@ -137,7 +137,7 @@

Dank

diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html index 435ac39..c552198 100644 --- a/docs/de/docs/index.html +++ b/docs/de/docs/index.html @@ -61,7 +61,7 @@

Dokumentation

Flowlight benutzen

-

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.13.0, macOS 15 oder neuer.

+

Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.13.1, macOS 15 oder neuer.

diff --git a/docs/de/index.html b/docs/de/index.html index 9aca252..e0d605d 100644 --- a/docs/de/index.html +++ b/docs/de/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

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

-

v0.13.0macOS 15+UniversalGPL-3.0Keine Telemetrie

+

v0.13.1macOS 15+UniversalGPL-3.0Keine Telemetrie

@@ -616,7 +616,7 @@

Wisse, was deinen Mac verlässt.

diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html index 822d7b2..328e10e 100644 --- a/docs/de/privacy/index.html +++ b/docs/de/privacy/index.html @@ -173,7 +173,7 @@

Kontakt

diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html index b01b8ab..fb0175d 100644 --- a/docs/de/threat-model/index.html +++ b/docs/de/threat-model/index.html @@ -213,7 +213,7 @@

Eine Schwachstelle melden

diff --git a/docs/docs/index.html b/docs/docs/index.html index c71c6e0..00d53c8 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -61,7 +61,7 @@

Documentation

Using Flowlight

-

Everything from the first launch to tuning the agent rules. Flowlight 0.13.0, macOS 15 or later.

+

Everything from the first launch to tuning the agent rules. Flowlight 0.13.1, macOS 15 or later.

diff --git a/docs/es/about/index.html b/docs/es/about/index.html index d689032..fd1d911 100644 --- a/docs/es/about/index.html +++ b/docs/es/about/index.html @@ -137,7 +137,7 @@

Agradecimientos

diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html index 0bf4de8..03d929e 100644 --- a/docs/es/docs/index.html +++ b/docs/es/docs/index.html @@ -61,7 +61,7 @@

Documentación

Usar Flowlight

-

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.13.0, macOS 15 o posterior.

+

Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.13.1, macOS 15 o posterior.

diff --git a/docs/es/index.html b/docs/es/index.html index ac26d22..40a81f1 100644 --- a/docs/es/index.html +++ b/docs/es/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

Observa la actividad de red de tus apps.

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -616,7 +616,7 @@

Ten claro qué sale de tu Mac.

diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html index 8ebc5f0..a1d50be 100644 --- a/docs/es/privacy/index.html +++ b/docs/es/privacy/index.html @@ -173,7 +173,7 @@

Contacto

diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html index 6968170..3acca5b 100644 --- a/docs/es/threat-model/index.html +++ b/docs/es/threat-model/index.html @@ -208,7 +208,7 @@

Informar de una vulnerabilidad

diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html index 32bdea2..029349d 100644 --- a/docs/fr/about/index.html +++ b/docs/fr/about/index.html @@ -137,7 +137,7 @@

Remerciements

diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html index fb2bb29..ac81a03 100644 --- a/docs/fr/docs/index.html +++ b/docs/fr/docs/index.html @@ -61,7 +61,7 @@

Documentation

Utiliser Flowlight

-

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.13.0, macOS 15 ou version ultérieure.

+

Tout, du premier lancement au réglage des règles des agents. Flowlight 0.13.1, macOS 15 ou version ultérieure.

diff --git a/docs/fr/index.html b/docs/fr/index.html index 339245e..3342185 100644 --- a/docs/fr/index.html +++ b/docs/fr/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

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

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -616,7 +616,7 @@

Sachez ce qui quitte votre Mac.

diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html index 611c048..483a232 100644 --- a/docs/fr/privacy/index.html +++ b/docs/fr/privacy/index.html @@ -173,7 +173,7 @@

Contact

diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html index 7276be5..83fef16 100644 --- a/docs/fr/threat-model/index.html +++ b/docs/fr/threat-model/index.html @@ -211,7 +211,7 @@

Signaler une vulnérabilité

diff --git a/docs/index.html b/docs/index.html index 53dd98b..d3b44ea 100644 --- a/docs/index.html +++ b/docs/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

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

-

v0.13.0macOS 15+UniversalGPL-3.0No telemetry

+

v0.13.1macOS 15+UniversalGPL-3.0No telemetry

@@ -625,7 +625,7 @@

Know what leaves your Mac.

diff --git a/docs/it/about/index.html b/docs/it/about/index.html index a4d95f2..b1b36ca 100644 --- a/docs/it/about/index.html +++ b/docs/it/about/index.html @@ -137,7 +137,7 @@

Ringraziamenti

diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html index bc81260..c4e52e8 100644 --- a/docs/it/docs/index.html +++ b/docs/it/docs/index.html @@ -61,7 +61,7 @@

Documentazione

Usare Flowlight

-

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.13.0, macOS 15 o successivo.

+

Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.13.1, macOS 15 o successivo.

diff --git a/docs/it/index.html b/docs/it/index.html index 81876b2..4faab09 100644 --- a/docs/it/index.html +++ b/docs/it/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

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

brew install --cask xinbetween/tap/flowlight

-

v0.13.0macOS 15+UniversaleGPL-3.0Nessuna telemetria

+

v0.13.1macOS 15+UniversaleGPL-3.0Nessuna telemetria

@@ -616,7 +616,7 @@

Sappi che cosa esce dal tuo Mac.

diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html index e87a59d..4058652 100644 --- a/docs/it/privacy/index.html +++ b/docs/it/privacy/index.html @@ -173,7 +173,7 @@

Contatti

diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html index 5a581a6..d46b031 100644 --- a/docs/it/threat-model/index.html +++ b/docs/it/threat-model/index.html @@ -209,7 +209,7 @@

Segnalare una vulnerabilità

diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html index bc4b631..1cb9627 100644 --- a/docs/ja/about/index.html +++ b/docs/ja/about/index.html @@ -138,7 +138,7 @@

謝辞

diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html index 7f2e472..babe886 100644 --- a/docs/ja/docs/index.html +++ b/docs/ja/docs/index.html @@ -61,7 +61,7 @@

ドキュメント

Flowlight の使い方

-

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.13.0、macOS 15 以降。

+

初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.13.1、macOS 15 以降。

diff --git a/docs/ja/index.html b/docs/ja/index.html index 39177ec..cb0c13e 100644 --- a/docs/ja/index.html +++ b/docs/ja/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

アプリの通信が見える。
brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -615,7 +615,7 @@

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

diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html index a5e9af2..43abdb8 100644 --- a/docs/ja/privacy/index.html +++ b/docs/ja/privacy/index.html @@ -171,7 +171,7 @@

連絡先

diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html index 23a38ab..2b22c12 100644 --- a/docs/ja/threat-model/index.html +++ b/docs/ja/threat-model/index.html @@ -210,7 +210,7 @@

脆弱性を報告する

diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html index 0ec3545..f53c215 100644 --- a/docs/ko/about/index.html +++ b/docs/ko/about/index.html @@ -138,7 +138,7 @@

감사

diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html index c0f4ccb..548104f 100644 --- a/docs/ko/docs/index.html +++ b/docs/ko/docs/index.html @@ -61,7 +61,7 @@

문서

Flowlight 사용하기

-

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.13.0, macOS 15 이상.

+

첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.13.1, macOS 15 이상.

diff --git a/docs/ko/index.html b/docs/ko/index.html index b62e2f1..ff69376 100644 --- a/docs/ko/index.html +++ b/docs/ko/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

앱이 무엇을 하는지 봅니다.

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -616,7 +616,7 @@

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

diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html index 3127e95..6b42b32 100644 --- a/docs/ko/privacy/index.html +++ b/docs/ko/privacy/index.html @@ -174,7 +174,7 @@

문의

diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html index 4eb0ce1..35f2968 100644 --- a/docs/ko/threat-model/index.html +++ b/docs/ko/threat-model/index.html @@ -208,7 +208,7 @@

취약점 신고하기

diff --git a/docs/llms-full.txt b/docs/llms-full.txt index c5a5d29..db7c9b6 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.13.0, macOS 15 or later. + Everything from the first launch to tuning the agent rules. Flowlight 0.13.1, macOS 15 or later. On this page @@ -839,7 +839,7 @@ Understand your AI agents. brew install --cask xinbetween/tap/flowlightCopy - v0.13.0macOS 15+UniversalGPL-3.0No telemetry + v0.13.1macOS 15+UniversalGPL-3.0No telemetry connectionslive @@ -1398,8 +1398,16 @@ Releases Downloads, checksums and full notes for each version are on GitHub Releases. + 0.13.1 +October 3, 2026Latest + + Rewrite form requests too. Request-modification rules now change fields in + application/x-www-form-urlencoded bodies, not only JSON. Set a field to a URL, text or + value and Flowlight encodes it as a browser would; remove a field and it is omitted. The request's + Content-Length is recalculated before it goes upstream. + 0.13.0 -October 1, 2026Latest +October 1, 2026 Change a request on its way out. A new kind of rule edits a matching outgoing request before it reaches the server — add or replace a header, pin a field in the JSON body, or strip diff --git a/docs/llms.txt b/docs/llms.txt index 106f436..b5b8780 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.13.0. +> 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.13.1. - [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg) - [Source code](https://github.com/xinbetween/flowlight) diff --git a/docs/privacy/index.html b/docs/privacy/index.html index d057e20..3b7b925 100644 --- a/docs/privacy/index.html +++ b/docs/privacy/index.html @@ -174,7 +174,7 @@

Contact

diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html index 53ae95f..b78d9c5 100644 --- a/docs/pt-PT/about/index.html +++ b/docs/pt-PT/about/index.html @@ -137,7 +137,7 @@

Agradecimentos

diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html index 9ab3862..bbf97b7 100644 --- a/docs/pt-PT/docs/index.html +++ b/docs/pt-PT/docs/index.html @@ -61,7 +61,7 @@

Documentação

Usar o Flowlight

-

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.13.0, macOS 15 ou posterior.

+

Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.13.1, macOS 15 ou posterior.

diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html index beb2c2b..f9c41f3 100644 --- a/docs/pt-PT/index.html +++ b/docs/pt-PT/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

Veja a atividade de rede das suas apps.

brew install --cask xinbetween/tap/flowlight

-

v0.13.0macOS 15+UniversalGPL-3.0Sem telemetria

+

v0.13.1macOS 15+UniversalGPL-3.0Sem telemetria

@@ -616,7 +616,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 8fd8b11..0873cef 100644 --- a/docs/pt-PT/privacy/index.html +++ b/docs/pt-PT/privacy/index.html @@ -173,7 +173,7 @@

Contacto

diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html index 8766507..a9ee0ed 100644 --- a/docs/pt-PT/threat-model/index.html +++ b/docs/pt-PT/threat-model/index.html @@ -210,7 +210,7 @@

Comunicar uma vulnerabilidade

diff --git a/docs/releases/index.html b/docs/releases/index.html index 7ccfb5b..4532b3e 100644 --- a/docs/releases/index.html +++ b/docs/releases/index.html @@ -55,7 +55,17 @@

What's new

-

0.13.0

Latest
+

0.13.1

Latest
+
    +
  • Rewrite form requests too. Request-modification rules now change fields in + application/x-www-form-urlencoded bodies, not only JSON. Set a field to a URL, text or + value and Flowlight encodes it as a browser would; remove a field and it is omitted. The request's + Content-Length is recalculated before it goes upstream.
  • +
+
+ +
+

0.13.0

  • Change a request on its way out. A new kind of rule edits a matching outgoing request before it reaches the server — add or replace a header, pin a field in the JSON body, or strip @@ -1026,7 +1036,7 @@

    What's new

diff --git a/docs/sitemap.xml b/docs/sitemap.xml index adafc65..f6555c0 100644 --- a/docs/sitemap.xml +++ b/docs/sitemap.xml @@ -4,7 +4,7 @@ https://flowlight.xinbetween.com/docs/2026-09-28 https://flowlight.xinbetween.com/2026-10-01 https://flowlight.xinbetween.com/privacy/2026-09-27 - https://flowlight.xinbetween.com/releases/2026-10-01 + https://flowlight.xinbetween.com/releases/2026-10-03 https://flowlight.xinbetween.com/threat-model/2026-09-27 https://flowlight.xinbetween.com/de/about/2026-09-26 https://flowlight.xinbetween.com/de/docs/2026-09-28 diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html index 937520b..146028f 100644 --- a/docs/threat-model/index.html +++ b/docs/threat-model/index.html @@ -209,7 +209,7 @@

Reporting a vulnerability

diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html index 0d1f191..719c9fd 100644 --- a/docs/zh-Hans/about/index.html +++ b/docs/zh-Hans/about/index.html @@ -137,7 +137,7 @@

致谢

diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html index ecfa416..70461bc 100644 --- a/docs/zh-Hans/docs/index.html +++ b/docs/zh-Hans/docs/index.html @@ -61,7 +61,7 @@

文档

使用 Flowlight

-

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.13.0,macOS 15 或更高版本。

+

从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.13.1,macOS 15 或更高版本。

diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html index 5cbeb77..2099dd2 100644 --- a/docs/zh-Hans/index.html +++ b/docs/zh-Hans/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

看清应用的网络活动。

brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -613,7 +613,7 @@

知道什么离开了你的 Mac。

diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html index 8b3ae14..781d049 100644 --- a/docs/zh-Hans/privacy/index.html +++ b/docs/zh-Hans/privacy/index.html @@ -171,7 +171,7 @@

联系方式

diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html index 7567a16..309e7e9 100644 --- a/docs/zh-Hans/threat-model/index.html +++ b/docs/zh-Hans/threat-model/index.html @@ -197,7 +197,7 @@

报告漏洞

diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html index ae95046..779c2ab 100644 --- a/docs/zh-Hant/about/index.html +++ b/docs/zh-Hant/about/index.html @@ -137,7 +137,7 @@

致謝

diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html index ff1f7dc..6e09406 100644 --- a/docs/zh-Hant/docs/index.html +++ b/docs/zh-Hant/docs/index.html @@ -61,7 +61,7 @@

說明文件

使用 Flowlight

-

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.13.0,macOS 15 或以上版本。

+

從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.13.1,macOS 15 或以上版本。

diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html index 47574b3..0c46601 100644 --- a/docs/zh-Hant/index.html +++ b/docs/zh-Hant/index.html @@ -34,7 +34,7 @@ - + @@ -75,7 +75,7 @@

看見每個 App 的網路活動。
brew install --cask xinbetween/tap/flowlight

-

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

+

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

@@ -614,7 +614,7 @@

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

diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html index ed46a27..37a00d0 100644 --- a/docs/zh-Hant/privacy/index.html +++ b/docs/zh-Hant/privacy/index.html @@ -171,7 +171,7 @@

聯絡

diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html index 7b1cfac..b2b7dda 100644 --- a/docs/zh-Hant/threat-model/index.html +++ b/docs/zh-Hant/threat-model/index.html @@ -198,7 +198,7 @@

回報漏洞

diff --git a/project.yml b/project.yml index a57e66a..3a00418 100644 --- a/project.yml +++ b/project.yml @@ -11,7 +11,7 @@ settings: DEVELOPMENT_TEAM: "" CODE_SIGN_STYLE: Automatic ENABLE_HARDENED_RUNTIME: YES - MARKETING_VERSION: "0.13.0" + MARKETING_VERSION: "0.13.1" CURRENT_PROJECT_VERSION: "22" targets: Flowlight: diff --git a/site/pages/releases.html b/site/pages/releases.html index 272b14c..0823e7c 100644 --- a/site/pages/releases.html +++ b/site/pages/releases.html @@ -13,7 +13,17 @@

What's new

-

0.13.0

Latest
+

0.13.1

Latest
+
    +
  • Rewrite form requests too. Request-modification rules now change fields in + application/x-www-form-urlencoded bodies, not only JSON. Set a field to a URL, text or + value and Flowlight encodes it as a browser would; remove a field and it is omitted. The request's + Content-Length is recalculated before it goes upstream.
  • +
+
+ +
+

0.13.0

  • Change a request on its way out. A new kind of rule edits a matching outgoing request before it reaches the server — add or replace a header, pin a field in the JSON body, or strip