1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import type * as HttpClient from "effect/http/HttpClient";
4import { CloudflareEnvironment } from "../CloudflareEnvironment.ts";
5import type { Credentials } from "../Credentials.ts";
6import type { App } from "./App.ts";
7import { ReadFlags } from "./ReadFlags.ts";
8import {
9 type FlagshipAuth,
10 makeHttpFlagshipClient,
11} from "./ReadFlagsHttpClient.ts";
12
13/**
14 * Local implementation of the {@link ReadFlags} binding — evaluates Flagship
15 * feature flags over the Cloudflare HTTP API (`GET .../flagship/apps/{appId}/evaluate`)
16 * using the **current credentials** instead of a native Worker binding
17 * ({@link ReadFlagsBinding}).
18 *
19 * Provide it on an {@link Action} (or any deploy-time Effect) so you can read
20 * flag values with the same `getBooleanValue`/`getStringValue`/… client you'd
21 * use inside a Worker:
22 *
23 * @example Reading a flag from an Action
24 * ```typescript
25 * const CheckFlag = Alchemy.Action(
26 * "CheckFlag",
27 * Effect.gen(function* () {
28 * const flags = yield* Cloudflare.Flagship.ReadFlags(app);
29 * return Effect.fn(function* () {
30 * return yield* flags.getBooleanValue("new-checkout", false);
31 * });
32 * }).pipe(Effect.provide(Cloudflare.Flagship.ReadFlagsLocal)),
33 * );
34 * ```
35 *
36 * The app id is resolved at apply time through the ambient {@link RuntimeContext}
37 * (in an Action, that's the resolve context the engine provides around the
38 * body), so `ReadFlags(app)` works even though the app is created in the same
39 * deploy.
40 *
41 * Limitations vs. the Worker binding: the HTTP evaluate endpoint only accepts a
42 * single `targetingKey` (read from `context.targetingKey`), so attribute-based
43 * targeting rules that key off other context fields cannot be exercised locally.
44 * The `raw` runtime binding has no HTTP equivalent and dies if used — see
45 * {@link makeHttpFlagshipClient}.
46 */
47export const ReadFlagsLocal = Layer.effect(
48 ReadFlags,
49 Effect.gen(function* () {
50 // Account + credentials are ambient during stack-eval (the stack's
51 // providers layer). Capture the full context so the evaluate op can run
52 // with the current credentials — no `host.bind`, no minted token.
53 const { accountId } = yield* yield* CloudflareEnvironment;
54 const context = yield* Effect.context<
55 Credentials | HttpClient.HttpClient
56 >();
57 const auth: FlagshipAuth = {
58 authorize: (eff) => eff.pipe(Effect.provideContext(context)),
59 accountId,
60 };
61
62 return Effect.fn(function* (app: App) {
63 // Deferred accessor — resolves the appId against the tracker at apply
64 // time (in an Action, that's the engine's resolve context).
65 const appId = yield* app.appId;
66 return makeHttpFlagshipClient(auth, appId);
67 });
68 }),
69);