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
@@ -2,6 +2,7 @@ import { useState, useEffect, useCallback, useRef, useMemo } from "react";
import type { CanvasResolution } from "@hyperframes/parsers";
import { trackStudioRenderStart } from "../../telemetry/events";
import { getAnonymousId } from "../../telemetry/config";
import { browserTelemetryAllowed } from "../../telemetry/policy";
import { generateId } from "../../utils/generateId";
export interface RenderJob {
@@ -157,16 +158,28 @@ export function useRenderQueue(projectId: string | null) {
resolution?: string;
composition?: string;
variables?: Record<string, unknown>;
telemetryDistinctId: string;
telemetryDistinctId?: string;
telemetryOptOut?: boolean;
} = {
fps,
quality,
format,
};
// The id is MINTED by getAnonymousId(), so calling it unconditionally
// created a telemetry identity for a profile that had opted out — and
// then shipped it to the server. The server's own policy cannot see this
// browser's localStorage or DoNotTrack, so it has to be told: an
// explicit `telemetryOptOut` suppresses the render outcome, which
// omitting the id alone does NOT (an old client omits it too, and that
// falls back to the install id).
if (browserTelemetryAllowed()) {
// So the server-emitted render_complete/render_error is attributed to
// this browser user (same id studio_* events use), making the render
// funnel joinable. Matches studio_render_start fired just above.
telemetryDistinctId: getAnonymousId(),
};
body.telemetryDistinctId = getAnonymousId();
} else {
body.telemetryOptOut = true;
}
if (resolution && resolution !== "auto") body.resolution = resolution;
if (composition) body.composition = composition;
if (opts.variables && Object.keys(opts.variables).length > 0) {