fix(cli,studio,core): close five R5 telemetry and canary findings

- A long-lived preview cached its telemetry posture in two places
  (readConfig and shouldTrack). Running `telemetry disable` in another
  terminal left it resolving canaries and injecting the CLI id for hours.
  Both caches are now dropped together at a request boundary.
- Studio minted and shipped a telemetry id for every render regardless of
  the browser profile's opt-out, and the server emitted the outcome under
  CLI policy, which cannot see localStorage or DNT. The browser now sends
  an explicit telemetryOptOut, distinct from an old client's omission.
- Any non-empty HYPERFRAMES_PREVIEW_HOST disabled the DNS-rebinding guard,
  so even a loopback bind accepted a hostile Host. The guard now holds for
  loopback binds and, on a LAN bind, admits only names this machine
  answers on.
- sunsetAfter had no reader of the current date. A scheduled workflow runs
  scripts/check-canary-sunset.ts weekly, so a failure lands on the
  rollout's owner rather than on an unrelated PR author.
- The install-state seed memo outlived `rm -rf ~/.hyperframes`,
  resurrecting a cleared cohort. Removed; it only saved a read on a
  readConfig cache miss.

Docs updated for the Host rule and the 100% exclusion carve-out.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-08-01 17:21:41 -07:00
co-authored by Claude Opus 5
parent 3f69a2c635
commit 6f0df2640b
16 changed files with 504 additions and 47 deletions
@@ -602,6 +602,55 @@ describe("POST /projects/:id/render — telemetryDistinctId forwarding", () => {
}
});
// Explicit suppression, forwarded so the CLI can honour a browser opt-out
// it has no other way to observe.
it("forwards an explicit telemetryOptOut", async () => {
const spy = vi.fn();
const { app, cleanup } = buildApp(spy);
try {
const res = await app.request("http://localhost/projects/demo/render", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
fps: 30,
quality: "standard",
format: "mp4",
telemetryOptOut: true,
}),
});
expect(res.status).toBe(200);
expect(spy.mock.calls[0][0].telemetryOptOut).toBe(true);
} finally {
cleanup();
}
});
// An old client omits the flag, and a non-boolean is not a signal either.
// Defaulting those to "opted out" would silently drop every pre-upgrade
// render outcome.
it.each([undefined, false, "true"])(
"treats telemetryOptOut %s as not opted out",
async (flag) => {
const spy = vi.fn();
const { app, cleanup } = buildApp(spy);
try {
await app.request("http://localhost/projects/demo/render", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
fps: 30,
quality: "standard",
format: "mp4",
telemetryOptOut: flag,
}),
});
expect(spy.mock.calls[0][0].telemetryOptOut).toBe(false);
} finally {
cleanup();
}
},
);
it("ignores a non-string telemetryDistinctId", async () => {
const spy = vi.fn();
const { app, cleanup } = buildApp(spy);
@@ -66,6 +66,10 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
// Browser telemetry id, so the server-emitted render outcome is
// attributed to the user who triggered the render (joinable funnel).
telemetryDistinctId?: string;
// Explicit "this browser profile opted out" flag. Distinct from simply
// omitting the id: an OLD client omits it too, and that case falls back
// to the install anonymousId. Only an explicit `true` suppresses.
telemetryOptOut?: boolean;
// Composition-variable overrides ({variableId: value}), injected as
// window.__hfVariables — same channel as `hyperframes render --variables`.
variables?: Record<string, unknown>;
@@ -126,6 +130,7 @@ export function registerRenderRoutes(api: Hono, adapter: StudioApiAdapter): void
variables,
distinctId:
typeof body.telemetryDistinctId === "string" ? body.telemetryDistinctId : undefined,
telemetryOptOut: body.telemetryOptOut === true,
});
(jobState as RenderJobState & { createdAt: number }).createdAt = Date.now();
renderJobs.set(jobId, jobState as RenderJobState & { createdAt: number });
+7
View File
@@ -144,6 +144,13 @@ export interface StudioApiAdapter {
fps: import("@hyperframes/core").Fps;
quality: string;
jobId: string;
/**
* The triggering browser profile has telemetry disabled (localStorage
* opt-out, DNT, dev build...). The CLI cannot observe any of that, so the
* browser has to say so — without it the server emitted render outcomes
* for a user who had opted out, under the CLI's own policy.
*/
telemetryOptOut?: boolean;
/**
* Optional output resolution preset. See `resolveDeviceScaleFactor` in
* the producer for the integer-scale + aspect + HDR constraints.