| 1 | /** |
| 2 | * Runtime helpers consumed by generated bundle entrypoints (Cloudflare |
| 3 | * Workers, Cloudflare Containers, AWS Lambda, …). |
| 4 | * |
| 5 | * Anything exported here runs *inside* the deployed function — keep the |
| 6 | * surface tiny and dependency-light. |
| 7 | */ |
| 8 | |
| 9 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 10 | import * as Effect from "effect/Effect"; |
| 11 | import * as Layer from "effect/Layer"; |
| 12 | import { isRedactedMarker, sanitizeKey } from "./RuntimeContext.ts"; |
| 13 | import { asEffect } from "./Util/types.ts"; |
| 14 | |
| 15 | /** |
| 16 | * Resolve the user's default-export entrypoint into a `Layer` for the |
| 17 | * bundled runtime. |
| 18 | * |
| 19 | * `entrypoint` may be any of: |
| 20 | * - a `Layer` factory (`{ build: (...) => ... }`) — used as-is |
| 21 | * - an Alchemy `Platform`/`Worker` construct (now a real `Effect`) |
| 22 | * - a plain `Effect` |
| 23 | * |
| 24 | * Centralized so the inline ternary doesn't have to be re-emitted into |
| 25 | * every bundle template (and accidentally rewritten to `x : x` by a bulk |
| 26 | * replace, which silently swaps the class in for the Effect and bricks |
| 27 | * every deployed worker/lambda). |
| 28 | */ |
| 29 | export const makeEntrypointLayer = ( |
| 30 | tag: any, |
| 31 | entrypoint: any, |
| 32 | ): Layer.Layer<any> => { |
| 33 | if (typeof entrypoint?.build === "function") { |
| 34 | return entrypoint; |
| 35 | } |
| 36 | return Layer.effect(tag, asEffect(entrypoint)); |
| 37 | }; |
| 38 | |
| 39 | /** |
| 40 | * Unwrap the `{"_tag":"Redacted","value":...}` marker that the deploy-time |
| 41 | * `Config` interceptor (see `Platform.ts`) and `RuntimeContext.set` use to |
| 42 | * preserve `Redacted`-ness across the env-var boundary. Returns the inner |
| 43 | * source value as a string, or `undefined` when `raw` is not a marker. |
| 44 | */ |
| 45 | const parseRedactedMarker = (raw: string): string | undefined => { |
| 46 | if (!raw.startsWith("{")) { |
| 47 | return undefined; |
| 48 | } |
| 49 | try { |
| 50 | const parsed: unknown = JSON.parse(raw); |
| 51 | if (isRedactedMarker(parsed)) { |
| 52 | return typeof parsed.value === "string" |
| 53 | ? parsed.value |
| 54 | : JSON.stringify(parsed.value); |
| 55 | } |
| 56 | } catch { |
| 57 | // not JSON — plain env value, fall through |
| 58 | } |
| 59 | return undefined; |
| 60 | }; |
| 61 | |
| 62 | /** |
| 63 | * Reify an env-var string the way `RuntimeContext.get` does: unwrap the |
| 64 | * `Redacted` marker, unquote a JSON-stringified string, and pass anything |
| 65 | * else through verbatim. |
| 66 | */ |
| 67 | const reifyEnvString = (raw: string): string => { |
| 68 | const marker = parseRedactedMarker(raw); |
| 69 | if (marker !== undefined) { |
| 70 | return marker; |
| 71 | } |
| 72 | if (raw.startsWith('"')) { |
| 73 | try { |
| 74 | const parsed: unknown = JSON.parse(raw); |
| 75 | if (typeof parsed === "string") { |
| 76 | return parsed; |
| 77 | } |
| 78 | } catch { |
| 79 | // not JSON — plain env value, fall through |
| 80 | } |
| 81 | } |
| 82 | return raw; |
| 83 | }; |
| 84 | |
| 85 | /** |
| 86 | * Wrap a runtime's env-backed `ConfigProvider` so values that were |
| 87 | * auto-bound by the deploy-time `Config` interceptor decode transparently. |
| 88 | * |
| 89 | * The engine can't know which config values are sensitive, so the |
| 90 | * interceptor binds every `Config` read during Init onto the deploy target |
| 91 | * as a secret, serialized as a `{"_tag":"Redacted","value":<source>}` |
| 92 | * marker. The interceptor's runtime branch reifies those markers for reads |
| 93 | * during Init, but effects that run later (request handlers, nested |
| 94 | * layers) resolve `Config` against the raw env-backed provider — without |
| 95 | * this wrapper, `Config.Number("PORT")` inside a handler sees the marker |
| 96 | * JSON instead of the source value and fails with a schema error. |
| 97 | * |
| 98 | * Two behaviors: |
| 99 | * - Leaf values that carry the marker are unwrapped to the raw source |
| 100 | * string before `Config` schemas decode them; everything else passes |
| 101 | * through untouched. |
| 102 | * - On a miss, falls back to the flat `sanitizeKey`-canonicalized key |
| 103 | * (`my.key` → `my_key`) that the interceptor bound the value under, so |
| 104 | * config names with non-alphanumeric characters resolve at runtime too. |
| 105 | */ |
| 106 | export const reifyBoundConfigProvider = ( |
| 107 | base: ConfigProvider.ConfigProvider, |
| 108 | env: Record<string, unknown>, |
| 109 | ): ConfigProvider.ConfigProvider => |
| 110 | ConfigProvider.make((path) => |
| 111 | base.load(path).pipe( |
| 112 | Effect.map((node) => { |
| 113 | if (node?._tag === "Value") { |
| 114 | const value = parseRedactedMarker(node.value); |
| 115 | return value === undefined ? node : ConfigProvider.makeValue(value); |
| 116 | } |
| 117 | if (node === undefined) { |
| 118 | const raw = env[sanitizeKey(path.map((p) => p.toString()).join("_"))]; |
| 119 | if (typeof raw === "string") { |
| 120 | return ConfigProvider.makeValue(reifyEnvString(raw)); |
| 121 | } |
| 122 | } |
| 123 | return node; |
| 124 | }), |
| 125 | ), |
| 126 | ); |