255 lines · 11.2 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// Cloud's rollout gate for MCP Enterprise-Managed Authorization.//// Implements the vendor-free `EnterpriseManagedRollout` port from// `@executor-js/sdk` against PostHog, which is the only flag/analytics service// this deployment operates. Two deliberate shapes://// 1. NO SDK. This is a hand-written `fetch` against two documented PostHog//    endpoints, not `posthog-node`. Cloud already ate an unexplained 3-5s//    page-load regression from adding one dependency to the worker bundle and//    the mechanism was never identified (see the notes on the MCP SDK v2//    revert). Until that is understood, a new runtime dependency in this bundle//    is a cost we are not willing to pay for a boolean, and the whole client we//    would be importing is two POSTs wide.//// 2. FAIL CLOSED, ALWAYS. Every way of not getting an answer — timeout,//    transport failure, non-2xx, a body we cannot parse, no PostHog key//    configured, no acting user to key the rollout on — withholds the//    enterprise-managed path and falls back to ordinary interactive OAuth. A//    PostHog outage therefore degrades the rollout and never fails a connect,//    and (because the SDK freezes the verdict onto the connection) never//    touches an enterprise-managed connection that already exists.//// The rollout UNIT is the user: `distinct_id` is the acting user's id, the same// id `posthog.identify(user.id, …)` uses in the browser, so a percentage// rollout here means the same population it means everywhere else in the// project. The organization rides along as group context so org-level targeting// stays available in the PostHog UI without changing that unit.// --------------------------------------------------------------------------- import { env } from "cloudflare:workers";import { Effect, Schema } from "effect"; import type {  EnterpriseManagedRollout,  EnterpriseManagedRolloutContext,  EnterpriseManagedRolloutDecision,  EnterpriseManagedRolloutEvent,} from "@executor-js/sdk"; import { POSTHOG_INGEST_HOST } from "../edge/passthrough"; /** The flag that gates enterprise-managed authorization. */export const ENTERPRISE_MANAGED_AUTH_FLAG_KEY = "mcp-enterprise-managed-auth"; /** PostHog group type for organizations — the same one *  `posthog.group("organization", …)` establishes in the browser. */const ORGANIZATION_GROUP_TYPE = "organization"; /** * How long a flag evaluation may take before the gate gives up and withholds. * Short on purpose: this call sits in front of an interactive connect, and the * safe answer is already known, so waiting is strictly worse than answering. */export const DEFAULT_FLAG_EVALUATION_TIMEOUT_MS = 1_000; /** Wire names of the rollout events, in the product's `object_verb` style. */const EVENT_NAMES = {  attempted: "ema_connect_attempted",  connected: "ema_connect_connected",  "blocked-by-admin": "ema_connect_blocked_by_admin",} as const satisfies Record<EnterpriseManagedRolloutEvent["kind"], string>; // ---------------------------------------------------------------------------// Boundary parsing// --------------------------------------------------------------------------- /** The slice of PostHog's `POST /flags?v=2` response this gate reads. Unknown *  keys are ignored, so PostHog adding fields cannot make the gate fail closed; *  a body that is not this shape at all can, which is the intent. */const FlagsResponse = Schema.Struct({  flags: Schema.Record(Schema.String, Schema.Struct({ enabled: Schema.Boolean })),}); const decodeFlagsResponse = Schema.decodeUnknownEffect(FlagsResponse); // ---------------------------------------------------------------------------// Detached work// --------------------------------------------------------------------------- /** * Hand a request to the platform and stop caring about it. * * `waitUntil` is the correct owner when the caller has an `ExecutionContext`; * the executor composition seam this gate is installed through does not receive * one, so the default owner attaches a terminal rejection handler and lets the * in-flight request ride out the rest of the response. Either way the promise * is owned by something that cannot report back, which is what makes an event * structurally unable to fail or delay a connect. */const detach = (  work: Promise<unknown>,  waitUntil: ((work: Promise<unknown>) => void) | undefined,): void => {  const settled = work.then(    () => undefined,    () => undefined,  );  if (waitUntil) waitUntil(settled);}; // ---------------------------------------------------------------------------// The gate// --------------------------------------------------------------------------- export interface PostHogRolloutConfig {  /** Public project key (`phc_…`). Public by design — it is already shipped to   *  every browser — so this is a var, not a secret. */  readonly projectKey: string;  /** PostHog API origin (no trailing slash), e.g. `https://us.i.posthog.com`. */  readonly host: string;  readonly fetch: typeof globalThis.fetch;  readonly timeoutMs?: number;  /** Platform post-response hook, when the caller has one. */  readonly waitUntil?: (work: Promise<unknown>) => void;} const withheld = (  reason: "disabled" | "evaluation-unavailable",): EnterpriseManagedRolloutDecision => ({ kind: "withheld", reason }); const ENABLED: EnterpriseManagedRolloutDecision = { kind: "enabled" }; /** Group context for a connect whose organization the host named. */const groupsFor = (context: EnterpriseManagedRolloutContext): Record<string, string> | undefined =>  context.organizationId === null    ? undefined    : { [ORGANIZATION_GROUP_TYPE]: context.organizationId }; /** * Build cloud's enterprise-managed rollout gate over an explicit PostHog * configuration. Everything environmental is a parameter so the composition * root parses it once and tests drive the real code path with an injected * `fetch`. */export const makePostHogEnterpriseManagedRollout = (  config: PostHogRolloutConfig,): EnterpriseManagedRollout => {  const timeoutMs = config.timeoutMs ?? DEFAULT_FLAG_EVALUATION_TIMEOUT_MS;   const evaluate = Effect.fn("Cloud.EnterpriseManagedRollout.decide")(function* (    context: EnterpriseManagedRolloutContext,  ) {    // The rollout unit is the user. With no acting user there is no    // `distinct_id` to key it on, so there is no answer to be had — withhold    // rather than invent a rollout bucket.    const distinctId = context.userId;    if (distinctId === null) return withheld("evaluation-unavailable");     const groups = groupsFor(context);    const response = yield* Effect.tryPromise(() =>      config.fetch(`${config.host}/flags?v=2`, {        method: "POST",        headers: {          "content-type": "application/json",          // Declares this a server-side evaluation, which is what selects          // server-runtime flags in PostHog's runtime detection.          "user-agent": "executor-cloud",        },        body: JSON.stringify({          api_key: config.projectKey,          distinct_id: distinctId,          ...(groups === undefined ? {} : { groups }),        }),        signal: AbortSignal.timeout(timeoutMs),      }),    );    if (!response.ok) return withheld("evaluation-unavailable");     const body = yield* Effect.tryPromise(() => response.json() as Promise<unknown>);    const decoded = yield* decodeFlagsResponse(body);    const flag = decoded.flags[ENTERPRISE_MANAGED_AUTH_FLAG_KEY];    // A flag PostHog did not return is a flag that is not on for this user    // (it may not exist yet, or the project may be quota-limited). That is a    // real "no", not a failure to answer.    if (flag === undefined || !flag.enabled) return withheld("disabled");    return ENABLED;  });   return {    decide: (context) =>      evaluate(context).pipe(        // Timeout, transport failure and an unparseable body all land here and        // all mean the same thing: no answer, so no enterprise-managed attempt.        // `catchCause` rather than `catchAll` because a defect in this adapter        // must not become a failed connect either.        Effect.catchCause(() => Effect.succeed(withheld("evaluation-unavailable"))),      ),    record: (event) =>      Effect.sync(() => {        const distinctId = event.context.userId;        // Same reason `decide` withholds: an event with no person to attach to        // would only pollute the project with anonymous rows.        if (distinctId === null) return;        const groups = groupsFor(event.context);        const decision = event.decision;        detach(          config.fetch(`${config.host}/i/v0/e/`, {            method: "POST",            headers: { "content-type": "application/json" },            body: JSON.stringify({              api_key: config.projectKey,              event: EVENT_NAMES[event.kind],              distinct_id: distinctId,              properties: {                // Product metadata only. There is no field on                // `EnterpriseManagedRolloutEvent` that can carry a token, an                // identity assertion, a client secret or a scope, and none is                // synthesized here.                integration_slug: String(event.context.integration),                ema_flag_enabled: decision.kind === "enabled",                ...(decision.kind === "withheld"                  ? { ema_flag_withheld_reason: decision.reason }                  : {}),                // PostHog's own convention, so these events can be filtered by                // flag value in the UI alongside every other flag.                [`$feature/${ENTERPRISE_MANAGED_AUTH_FLAG_KEY}`]: decision.kind === "enabled",                ...(event.kind === "blocked-by-admin" && event.oauthErrorCode !== undefined                  ? { oauth_error_code: event.oauthErrorCode }                  : {}),                ...(groups === undefined ? {} : { $groups: groups }),              },            }),          }),          config.waitUntil,        );      }),  };}; /** A gate that answers "no" to everything, for a deployment that has no PostHog *  configuration to evaluate against. Cloud is SUPPOSED to have one, so a *  missing key is a misconfiguration, and failing closed is the same choice the *  outage paths make. This is NOT the same as injecting no gate at all, which *  is how the hosts with no flag service (local, desktop, CLI, self-host) keep *  their existing behavior. */export const unavailableEnterpriseManagedRollout: EnterpriseManagedRollout = {  decide: () => Effect.succeed(withheld("evaluation-unavailable")),  record: () => Effect.void,}; /** * The gate as the cloud worker composes it: the public project key already * shipped in `wrangler.jsonc`, and the same PostHog ingest origin the * adblock-dodging passthrough proxies to. */export const cloudEnterpriseManagedRollout = (): EnterpriseManagedRollout => {  const projectKey = env.VITE_PUBLIC_POSTHOG_KEY;  if (!projectKey) return unavailableEnterpriseManagedRollout;  return makePostHogEnterpriseManagedRollout({    projectKey,    host: env.VITE_PUBLIC_POSTHOG_HOST ?? `https://${POSTHOG_INGEST_HOST}`,    fetch: (input, init) => globalThis.fetch(input, init),  });};