feat(feedback): deliver shared diagnostic reports to Chatwoot support inbox (#88)
* feat(feedback): deliver shared diagnostic reports to Chatwoot support inbox
Wire shareReport() to the Chatwoot public client API
(/public/api/v1/inboxes/{inbox_identifier}) so user-shared diagnostic
reports also reach the OpenCode Mobile Feedback inbox.
- New src/lib/chatwoot.ts: dependency-injected, node-testable client —
anonymous contact -> conversation -> message. Ships only the inbox
identifier (EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER); never an
account api_access_token. Contact source_id persisted via
SecureStore for conversation continuity; stale id recreated on 404.
- Delivery is gated on the same telemetry consent flag as
Sentry/PostHog and is best-effort (share sheet never blocks on it).
- Reports are scrubbed before leaving the device: all URLs and every
occurrence of the target host redacted (new redactHostAndUrls in
scrub.ts).
- CI: pass EXPO_PUBLIC_CHATWOOT_INBOX_IDENTIFIER in build and
Play-publish workflows. Deliberately NOT added to the F-Droid
workflow to avoid widening reproducible-build divergence (#86).
- Consent modal copy discloses support-inbox delivery.
Closes #85
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(feedback): close host-leak gaps in support-report scrubbing
Security review findings on the Chatwoot delivery path:
- Log-buffer lines record server hosts without a scheme, which the
URL regex never matches, and crash reports carry no host of their
own — so bare hostnames could reach the support inbox. Track every
host probed this session and redact them all in the support copy.
- Redact bare IPv4 addresses as a catch-all for hosts never parsed.
- Resolve telemetry consent from SecureStore when a report is shared
before startup finished loading it, instead of silently dropping.
- Move redactHostAndUrls tests to scrub.test.ts alongside the module.
Refs #85
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { test } from "node:test"
|
||||
import assert from "node:assert/strict"
|
||||
import { scrubUrl, scrubString, scrubObject } from "./scrub.ts"
|
||||
import { scrubUrl, scrubString, scrubObject, redactHostAndUrls } from "./scrub.ts"
|
||||
|
||||
// scrubUrl ----------------------------------------------------------------
|
||||
|
||||
@@ -103,3 +103,36 @@ test("scrubObject: preserves non-string, non-object values as-is", () => {
|
||||
assert.deepEqual(result.items, [1, 2, 3])
|
||||
assert.equal(result.nothing, null)
|
||||
})
|
||||
|
||||
// redactHostAndUrls -------------------------------------------------------
|
||||
|
||||
test("redactHostAndUrls: strips URLs, bare host occurrences, credentials", () => {
|
||||
const host = "my-dev-box.tail1234.ts.net"
|
||||
const text = [
|
||||
`Target URL: https://user:pw@${host}:4096/api`,
|
||||
` scheme=https host=${host} port=4096 hostname=true`,
|
||||
`probe start http://${host}:4096/global/health`,
|
||||
"internet https://www.gstatic.com/generate_204 OK",
|
||||
].join("\n")
|
||||
const out = redactHostAndUrls(text, [host])
|
||||
assert.ok(!out.includes(host), "host must not survive")
|
||||
assert.ok(!out.includes("user:pw"), "credentials must not survive")
|
||||
assert.ok(out.includes("<redacted-url>"))
|
||||
assert.ok(out.includes("host=<redacted-host>"))
|
||||
})
|
||||
|
||||
test("redactHostAndUrls: redacts every session host, not just the report's own", () => {
|
||||
// Crash-report case: report.host is undefined but earlier failed-connect
|
||||
// log lines mention hosts without a scheme.
|
||||
const text = ['{"host":"box-a.tailnet.ts.net","port":"4096"}', "server unreachable box-b.local:8080"].join("\n")
|
||||
const out = redactHostAndUrls(text, [undefined, "box-a.tailnet.ts.net", "box-b.local"])
|
||||
assert.ok(!out.includes("box-a.tailnet.ts.net"))
|
||||
assert.ok(!out.includes("box-b.local"))
|
||||
})
|
||||
|
||||
test("redactHostAndUrls: blanks bare IPv4 addresses even when unknown", () => {
|
||||
const out = redactHostAndUrls("connect failed 192.168.1.50:4096 via 10.0.0.1", [])
|
||||
assert.ok(!out.includes("192.168.1.50"))
|
||||
assert.ok(!out.includes("10.0.0.1"))
|
||||
assert.ok(out.includes("<redacted-ip>"))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user