patchwork-os 1.2.0-beta.3.canary.883 → 1.2.0-beta.3.canary.885

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Periodic + shutdown usage reporting.
3
+ *
4
+ * Telemetry used to be sent only at bridge shutdown. Long-running bridges
5
+ * (launchd services, containers) almost never shut down, so they never
6
+ * reported. The reporter sends on an interval and once more at shutdown.
7
+ *
8
+ * Each activity entry is counted exactly once: a send covers entries newer
9
+ * than the previous send's cutoff, and the cutoff advances even when the send
10
+ * fails (a failed send is dropped, never retried — retrying would double-count
11
+ * if the collector had in fact stored it). Activity while telemetry is not
12
+ * opted in advances the cutoff too, so opting in later never uploads history
13
+ * the user had not consented to.
14
+ */
15
+ import type { ActivityEntry } from "./activityTypes.js";
16
+ import { type AnalyticsSummary } from "./analyticsAggregator.js";
17
+ export declare const DEFAULT_REPORT_INTERVAL_MS: number;
18
+ export interface ReporterDeps {
19
+ /** Current in-memory activity entries (any order). */
20
+ entries: () => readonly ActivityEntry[];
21
+ /** True when the user has opted in. Checked at every flush. */
22
+ enabled: () => boolean;
23
+ bridgeVersion: string;
24
+ salt: () => string;
25
+ send: (summary: AnalyticsSummary) => Promise<void>;
26
+ now?: () => number;
27
+ }
28
+ export declare class AnalyticsReporter {
29
+ private readonly deps;
30
+ private cutoffMs;
31
+ private windowStartMs;
32
+ private timer;
33
+ private inFlight;
34
+ constructor(deps: ReporterDeps);
35
+ start(intervalMs?: number): void;
36
+ stop(): void;
37
+ /** Send everything since the last cutoff. Never rejects. Serialised. */
38
+ flush(): Promise<void>;
39
+ private doFlush;
40
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Periodic + shutdown usage reporting.
3
+ *
4
+ * Telemetry used to be sent only at bridge shutdown. Long-running bridges
5
+ * (launchd services, containers) almost never shut down, so they never
6
+ * reported. The reporter sends on an interval and once more at shutdown.
7
+ *
8
+ * Each activity entry is counted exactly once: a send covers entries newer
9
+ * than the previous send's cutoff, and the cutoff advances even when the send
10
+ * fails (a failed send is dropped, never retried — retrying would double-count
11
+ * if the collector had in fact stored it). Activity while telemetry is not
12
+ * opted in advances the cutoff too, so opting in later never uploads history
13
+ * the user had not consented to.
14
+ */
15
+ import { buildSummary } from "./analyticsAggregator.js";
16
+ export const DEFAULT_REPORT_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
+ export class AnalyticsReporter {
18
+ deps;
19
+ cutoffMs;
20
+ windowStartMs;
21
+ timer = null;
22
+ inFlight = null;
23
+ constructor(deps) {
24
+ this.deps = deps;
25
+ const now = (deps.now ?? Date.now)();
26
+ this.cutoffMs = now;
27
+ this.windowStartMs = now;
28
+ }
29
+ start(intervalMs = DEFAULT_REPORT_INTERVAL_MS) {
30
+ if (this.timer)
31
+ return;
32
+ this.timer = setInterval(() => {
33
+ void this.flush();
34
+ }, intervalMs);
35
+ this.timer.unref?.();
36
+ }
37
+ stop() {
38
+ if (this.timer)
39
+ clearInterval(this.timer);
40
+ this.timer = null;
41
+ }
42
+ /** Send everything since the last cutoff. Never rejects. Serialised. */
43
+ flush() {
44
+ const run = (this.inFlight ?? Promise.resolve()).then(() => this.doFlush());
45
+ this.inFlight = run.finally(() => {
46
+ if (this.inFlight === run)
47
+ this.inFlight = null;
48
+ });
49
+ return run;
50
+ }
51
+ async doFlush() {
52
+ const now = (this.deps.now ?? Date.now)();
53
+ const since = this.cutoffMs;
54
+ const batch = this.deps.entries().filter((e) => {
55
+ const t = Date.parse(e.timestamp);
56
+ return Number.isFinite(t) && t > since && t <= now;
57
+ });
58
+ const windowMs = now - this.windowStartMs;
59
+ // Advance BEFORE sending: success, failure and opted-out all consume the
60
+ // window, so nothing is ever counted twice or uploaded retroactively.
61
+ this.cutoffMs = now;
62
+ if (batch.length === 0)
63
+ return;
64
+ this.windowStartMs = now;
65
+ if (!this.deps.enabled())
66
+ return;
67
+ try {
68
+ const summary = buildSummary(batch, Math.max(0, windowMs), this.deps.bridgeVersion, this.deps.salt());
69
+ await this.deps.send(summary);
70
+ }
71
+ catch {
72
+ // Dropped by design — see the header.
73
+ }
74
+ }
75
+ }
76
+ //# sourceMappingURL=analyticsReporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyticsReporter.js","sourceRoot":"","sources":["../src/analyticsReporter.ts"],"names":[],"mappings":"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"}
@@ -1,18 +1,27 @@
1
1
  /**
2
- * Sends an anonymized analytics summary to the usage endpoint.
3
- * - Fire-and-forget is NOT used: callers must await this with a timeout race.
4
- * - All errors are caught and swallowed — telemetry must never affect bridge operation.
5
- * - Endpoint defaults to the upstream collector. Operators may override via:
6
- * 1. env: PATCHWORK_ANALYTICS_ENDPOINT (highest precedence; for CI/headless)
7
- * 2. config file: ~/.claude/ide/analytics-config.json (preferred — managed by
8
- * `patchwork analytics configure`, keeps secrets out of launchd plists)
9
- * 3. default upstream collector
10
- * Endpoint is resolved per-call (cheap fs read) so live config changes take
11
- * effect on the next send without restart. Invalid values fall back through.
12
- * Never read from the network or summary payload — preserves the
13
- * redirect-attack property the original hardcoded constant was protecting.
2
+ * Sends an anonymized analytics summary to the usage endpoint (opt-in only;
3
+ * the caller checks the preference).
4
+ *
5
+ * - Callers await this with a timeout race; it never rejects.
6
+ * - Endpoint resolution (per call, so config changes apply without restart):
7
+ * 1. env PATCHWORK_ANALYTICS_ENDPOINT (CI / headless / containers)
8
+ * 2. config file ~/.claude/ide/analytics-config.json
9
+ * (managed by `patchwork analytics configure`)
10
+ * 3. DEFAULT_ANALYTICS_ENDPOINT
11
+ * Never read from the network or the payload — a redirect attacker cannot
12
+ * choose where telemetry goes.
13
+ * - Stops, in order: DO_NOT_TRACK (Console Do Not Track convention),
14
+ * PATCHWORK_TELEMETRY_DEBUG (prints the payload instead of sending), an
15
+ * engaged write kill switch.
16
+ * - Transport is `safeFetch`, never raw fetch, and never follows redirects.
17
+ * The default endpoint must resolve to a PUBLIC address; an operator-
18
+ * configured endpoint may be private or plain http (e.g. a collector on
19
+ * a private network), because the operator chose it.
14
20
  */
15
21
  import type { AnalyticsSummary } from "./analyticsAggregator.js";
22
+ import { type SafeFetchOptions } from "./ssrfGuard.js";
23
+ /** The live public collector. The single definition; import it, never copy it. */
24
+ export declare const DEFAULT_ANALYTICS_ENDPOINT = "https://analytics.patchworkos.com/v1/usage";
16
25
  export declare function resolveAnalyticsTarget(): {
17
26
  endpoint: string;
18
27
  key: string | undefined;
@@ -21,8 +30,15 @@ export declare function resolveAnalyticsTarget(): {
21
30
  key: "env" | "config" | "none";
22
31
  };
23
32
  };
24
- /**
25
- * Sends the summary to the analytics endpoint.
26
- * Resolves (never rejects) — all errors are swallowed silently.
27
- */
28
- export declare function sendAnalytics(summary: AnalyticsSummary): Promise<void>;
33
+ /** Why telemetry is suppressed in this environment, or null. */
34
+ export declare function telemetrySuppressed(env?: NodeJS.ProcessEnv): "do_not_track" | "debug" | null;
35
+ /** Injection seam for tests; production uses the defaults. */
36
+ export interface SendDeps {
37
+ fetchImpl?: SafeFetchOptions["fetchImpl"];
38
+ resolveDns?: SafeFetchOptions["resolveDns"];
39
+ killSwitchEngaged?: () => boolean;
40
+ writeDebug?: (line: string) => void;
41
+ onSent?: () => void;
42
+ }
43
+ /** Sends the summary. Resolves (never rejects) — all errors are swallowed. */
44
+ export declare function sendAnalytics(summary: AnalyticsSummary, deps?: SendDeps): Promise<void>;
@@ -1,20 +1,30 @@
1
1
  /**
2
- * Sends an anonymized analytics summary to the usage endpoint.
3
- * - Fire-and-forget is NOT used: callers must await this with a timeout race.
4
- * - All errors are caught and swallowed — telemetry must never affect bridge operation.
5
- * - Endpoint defaults to the upstream collector. Operators may override via:
6
- * 1. env: PATCHWORK_ANALYTICS_ENDPOINT (highest precedence; for CI/headless)
7
- * 2. config file: ~/.claude/ide/analytics-config.json (preferred — managed by
8
- * `patchwork analytics configure`, keeps secrets out of launchd plists)
9
- * 3. default upstream collector
10
- * Endpoint is resolved per-call (cheap fs read) so live config changes take
11
- * effect on the next send without restart. Invalid values fall back through.
12
- * Never read from the network or summary payload — preserves the
13
- * redirect-attack property the original hardcoded constant was protecting.
2
+ * Sends an anonymized analytics summary to the usage endpoint (opt-in only;
3
+ * the caller checks the preference).
4
+ *
5
+ * - Callers await this with a timeout race; it never rejects.
6
+ * - Endpoint resolution (per call, so config changes apply without restart):
7
+ * 1. env PATCHWORK_ANALYTICS_ENDPOINT (CI / headless / containers)
8
+ * 2. config file ~/.claude/ide/analytics-config.json
9
+ * (managed by `patchwork analytics configure`)
10
+ * 3. DEFAULT_ANALYTICS_ENDPOINT
11
+ * Never read from the network or the payload — a redirect attacker cannot
12
+ * choose where telemetry goes.
13
+ * - Stops, in order: DO_NOT_TRACK (Console Do Not Track convention),
14
+ * PATCHWORK_TELEMETRY_DEBUG (prints the payload instead of sending), an
15
+ * engaged write kill switch.
16
+ * - Transport is `safeFetch`, never raw fetch, and never follows redirects.
17
+ * The default endpoint must resolve to a PUBLIC address; an operator-
18
+ * configured endpoint may be private or plain http (e.g. a collector on
19
+ * a private network), because the operator chose it.
14
20
  */
15
21
  import { getAnalyticsConfig } from "./analyticsConfig.js";
16
22
  import { recordAnalyticsSent } from "./analyticsPrefs.js";
17
- const DEFAULT_ENDPOINT = "https://analytics.claude-ide-bridge.dev/v1/usage";
23
+ import { readKillSwitch } from "./governance/killSwitchPolicy.js";
24
+ import { safeFetch } from "./ssrfGuard.js";
25
+ /** The live public collector. The single definition; import it, never copy it. */
26
+ export const DEFAULT_ANALYTICS_ENDPOINT = "https://analytics.patchworkos.com/v1/usage";
27
+ const SEND_TIMEOUT_MS = 3000;
18
28
  function validUrl(value) {
19
29
  if (!value)
20
30
  return undefined;
@@ -33,7 +43,7 @@ export function resolveAnalyticsTarget() {
33
43
  const envKey = process.env.PATCHWORK_ANALYTICS_KEY;
34
44
  const cfg = getAnalyticsConfig();
35
45
  const cfgEndpoint = validUrl(cfg.endpoint);
36
- let endpoint = DEFAULT_ENDPOINT;
46
+ let endpoint = DEFAULT_ANALYTICS_ENDPOINT;
37
47
  let endpointSource = "default";
38
48
  if (envEndpoint) {
39
49
  endpoint = envEndpoint;
@@ -59,14 +69,34 @@ export function resolveAnalyticsTarget() {
59
69
  source: { endpoint: endpointSource, key: keySource },
60
70
  };
61
71
  }
62
- const SEND_TIMEOUT_MS = 3000;
63
- /**
64
- * Sends the summary to the analytics endpoint.
65
- * Resolves (never rejects) — all errors are swallowed silently.
66
- */
67
- export async function sendAnalytics(summary) {
72
+ function truthy(v) {
73
+ if (v === undefined)
74
+ return false;
75
+ const s = v.trim().toLowerCase();
76
+ return s !== "" && s !== "0" && s !== "false" && s !== "no";
77
+ }
78
+ /** Why telemetry is suppressed in this environment, or null. */
79
+ export function telemetrySuppressed(env = process.env) {
80
+ if (truthy(env.DO_NOT_TRACK))
81
+ return "do_not_track";
82
+ if (truthy(env.PATCHWORK_TELEMETRY_DEBUG))
83
+ return "debug";
84
+ return null;
85
+ }
86
+ /** Sends the summary. Resolves (never rejects) — all errors are swallowed. */
87
+ export async function sendAnalytics(summary, deps = {}) {
68
88
  try {
69
- const { endpoint, key } = resolveAnalyticsTarget();
89
+ const suppressed = telemetrySuppressed();
90
+ if (suppressed === "do_not_track")
91
+ return;
92
+ if (suppressed === "debug") {
93
+ (deps.writeDebug ?? ((l) => process.stderr.write(l)))(`[telemetry debug — not sent] ${JSON.stringify(summary)}\n`);
94
+ return;
95
+ }
96
+ const engaged = deps.killSwitchEngaged ?? (() => readKillSwitch().engaged);
97
+ if (engaged())
98
+ return;
99
+ const { endpoint, key, source } = resolveAnalyticsTarget();
70
100
  const controller = new AbortController();
71
101
  const timer = setTimeout(() => controller.abort(), SEND_TIMEOUT_MS);
72
102
  try {
@@ -75,22 +105,26 @@ export async function sendAnalytics(summary) {
75
105
  };
76
106
  if (key)
77
107
  headers["X-Analytics-Key"] = key;
78
- const res = await fetch(endpoint, {
108
+ const { response } = await safeFetch(endpoint, {
79
109
  method: "POST",
80
110
  headers,
81
111
  body: JSON.stringify(summary),
82
112
  signal: controller.signal,
113
+ }, {
114
+ allowPrivate: source.endpoint !== "default",
115
+ followRedirects: false,
116
+ ...(deps.fetchImpl ? { fetchImpl: deps.fetchImpl } : {}),
117
+ ...(deps.resolveDns ? { resolveDns: deps.resolveDns } : {}),
83
118
  });
84
- if (res.ok) {
85
- recordAnalyticsSent();
86
- }
119
+ if (response.ok)
120
+ (deps.onSent ?? recordAnalyticsSent)();
87
121
  }
88
122
  finally {
89
123
  clearTimeout(timer);
90
124
  }
91
125
  }
92
126
  catch {
93
- // Silently swallow all errors — telemetry must never surface to the user
127
+ // Telemetry must never surface to the user or affect the bridge.
94
128
  }
95
129
  }
96
130
  //# sourceMappingURL=analyticsSend.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"analyticsSend.js","sourceRoot":"","sources":["../src/analyticsSend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"analyticsSend.js","sourceRoot":"","sources":["../src/analyticsSend.ts"],"names":[],"mappings":"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"}
package/dist/bridge.d.ts CHANGED
@@ -17,6 +17,7 @@ export declare class Bridge {
17
17
  private server;
18
18
  private extensionClient;
19
19
  private activityLog;
20
+ private analyticsReporter;
20
21
  private authToken;
21
22
  private sessions;
22
23
  /**
package/dist/bridge.js CHANGED
@@ -4,8 +4,8 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { WebSocket } from "ws";
6
6
  import { ActivityLog } from "./activityLog.js";
7
- import { buildSummary } from "./analyticsAggregator.js";
8
7
  import { getAnalyticsPref, getAnalyticsSalt } from "./analyticsPrefs.js";
8
+ import { AnalyticsReporter } from "./analyticsReporter.js";
9
9
  import { sendAnalytics } from "./analyticsSend.js";
10
10
  import { dispatchCancelPush, enqueueApprovalWithDispatch, } from "./approvalHttp.js";
11
11
  import { resolveApprovalLogDir } from "./approvalPersistence.js";
@@ -116,6 +116,7 @@ export class Bridge {
116
116
  server;
117
117
  extensionClient;
118
118
  activityLog;
119
+ analyticsReporter = null;
119
120
  authToken;
120
121
  sessions = new Map();
121
122
  /**
@@ -958,6 +959,18 @@ export class Bridge {
958
959
  async start() {
959
960
  // 0. Initialize OpenTelemetry (no-op when OTEL_EXPORTER_OTLP_ENDPOINT is unset)
960
961
  initTelemetry();
962
+ // 0. Opt-in usage telemetry: daily + at shutdown, so long-running bridges
963
+ // report too. The preference is re-read at every flush.
964
+ this.analyticsReporter = new AnalyticsReporter({
965
+ entries: () => this.activityLog.queryAll(),
966
+ enabled: () => (this.config.analyticsEnabled !== null
967
+ ? this.config.analyticsEnabled
968
+ : getAnalyticsPref()) === true,
969
+ bridgeVersion: PACKAGE_VERSION,
970
+ salt: getAnalyticsSalt,
971
+ send: (summary) => sendAnalytics(summary),
972
+ });
973
+ this.analyticsReporter.start();
961
974
  // 0a. Snapshot env-derived kill-switch flags. After this, plugins or
962
975
  // recipe steps mutating `process.env.PATCHWORK_FLAG_*` cannot disable an
963
976
  // active emergency stop. Closes MED-2 from the 2026-04-28 audit.
@@ -2244,22 +2257,18 @@ export class Bridge {
2244
2257
  ? `, ${totalErrors} error${totalErrors === 1 ? "" : "s"}`
2245
2258
  : "";
2246
2259
  this.logger.info(`Shutdown complete — ${totalSessions} session${totalSessions === 1 ? "" : "s"}, ${totalCalls} tool call${totalCalls === 1 ? "" : "s"}${shutdownErrorPart}`);
2247
- // Send analytics if opted in — awaited with 2s timeout so it completes before process.exit()
2248
- const analyticsOn = this.config.analyticsEnabled !== null
2249
- ? this.config.analyticsEnabled
2250
- : getAnalyticsPref();
2251
- if (analyticsOn === true && totalSessions > 0) {
2260
+ // Final telemetry flush (opt-in only; the reporter checks). Covers activity
2261
+ // since the last periodic send. Awaited with a 2s cap before exit.
2262
+ if (this.analyticsReporter) {
2263
+ this.analyticsReporter.stop();
2252
2264
  try {
2253
- const entries = this.activityLog.query({ last: 500 });
2254
- const salt = getAnalyticsSalt();
2255
- const summary = buildSummary(entries, maxDurationMs, PACKAGE_VERSION, salt);
2256
2265
  await Promise.race([
2257
- sendAnalytics(summary),
2266
+ this.analyticsReporter.flush(),
2258
2267
  new Promise((resolve) => setTimeout(resolve, 2000)),
2259
2268
  ]);
2260
2269
  }
2261
2270
  catch {
2262
- // Swallow all errors — analytics must never affect shutdown
2271
+ // Telemetry must never affect shutdown.
2263
2272
  }
2264
2273
  }
2265
2274
  // Send aggregate session-end notification to the extension before disconnecting