1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { LogGroup } from "./LogGroup.ts";
6
7/**
8 * Shared scaffolding for CloudWatch Logs HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every single-operation `{Op}Http.ts` in this
11 * service is a thin `Layer.effect(Cap, makeLogGroupHttpBinding({ … }))` over
12 * this builder. Everything except the operation, the IAM action(s), and the
13 * IAM resource shape is boilerplate:
14 *
15 * - the deploy-time half registers `Allow(host, tag(logGroup))` with the
16 * requested actions on the bound group's ARN (and/or its `:*` log-stream
17 * wildcard, per `iamResources`);
18 * - the runtime callable injects the resolved `logGroupName` into the request
19 * (unless `injectLogGroupName: false` — for query-id / record-pointer
20 * scoped operations like `GetQueryResults`, `StopQuery`, `GetLogRecord`).
21 *
22 * Genuinely-different bindings stay bespoke: `LogEventSink` (a batching sink
23 * over the `PutLogEvents` capability).
24 */
25export const makeLogGroupHttpBinding = <I, A, E, R>(options: {
26 /** Fully-qualified binding tag, e.g. `AWS.Logs.FilterLogEvents`. */
27 tag: string;
28 /**
29 * The distilled operation the runtime callable invokes.
30 *
31 * Typed as distilled's `OperationMethod` intersection (yieldable Effect +
32 * direct call signature) so `I` is inferred from the call signature — the
33 * Effect half alone defeats inference for requests without `logGroupName`
34 * (e.g. `GetLogRecord`, `GetQueryResults`, `StopQuery`).
35 */
36 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R> &
37 ((input: I) => Effect.Effect<A, E, R>);
38 /** IAM actions granted on the bound log group. */
39 actions: readonly string[];
40 /**
41 * Which ARNs the actions are granted on:
42 * - `"group-and-streams"` (default): the log-group ARN plus `${arn}:*`;
43 * - `"streams"`: only `${arn}:*` — for actions that authorize against the
44 * log-stream resource (`PutLogEvents`, `DeleteLogStream`);
45 * - `"all"`: `*` — for actions that do not support resource-level
46 * permissions (`StopQuery`).
47 */
48 iamResources?: "group-and-streams" | "streams" | "all";
49 /**
50 * Inject the bound group's name under `logGroupName` (default `true`).
51 * Disable for operations scoped by a query id or record pointer.
52 */
53 injectLogGroupName?: boolean;
54}) =>
55 Effect.gen(function* () {
56 const op = yield* options.operation;
57 const inject = options.injectLogGroupName ?? true;
58
59 return Effect.fn(function* <G extends LogGroup>(logGroup: G) {
60 const LogGroupName = yield* logGroup.logGroupName;
61 if (!globalThis.__ALCHEMY_RUNTIME__) {
62 const host = yield* Binding.Host;
63 if (isBindingHost(host)) {
64 yield* host.bind`Allow(${host}, ${options.tag}(${logGroup}))`({
65 policyStatements: [
66 {
67 Effect: "Allow",
68 Action: [...options.actions],
69 Resource:
70 options.iamResources === "all"
71 ? ["*"]
72 : options.iamResources === "streams"
73 ? [Output.interpolate`${logGroup.logGroupArn}:*`]
74 : [
75 logGroup.logGroupArn,
76 Output.interpolate`${logGroup.logGroupArn}:*`,
77 ],
78 },
79 ],
80 });
81 }
82 }
83 return Effect.fn(`${options.tag}(${logGroup.LogicalId})`)(function* (
84 request?: Omit<I, "logGroupName">,
85 ) {
86 const input: Record<string, unknown> = { ...request };
87 if (inject) {
88 input.logGroupName = yield* LogGroupName;
89 }
90 return yield* op(input as unknown as I);
91 });
92 });
93 });