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 { isInstance, type Instance } from "./Instance.ts";
6import type { SecurityGroup } from "./SecurityGroup.ts";
7import type { Volume } from "./Volume.ts";
8
9/**
10 * Shared scaffolding for EC2 HTTP bindings.
11 *
12 * NOT exported from `index.ts` — every single-operation `{Op}Http.ts` in this
13 * service is a thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of
14 * the three builders below (one per bound resource kind). Everything except
15 * the operation, the IAM action list, and the injected identifier is
16 * boilerplate. Genuinely-different bindings (custom response shaping like
17 * `DescribeInstance`) stay bespoke.
18 */
19
20/**
21 * Build the impl Effect for an operation scoped to a bound {@link Instance}.
22 * The runtime callable injects the instance id (as the scalar `InstanceId` or
23 * the single-element `InstanceIds` array, per `requestKey`) and the
24 * deploy-time half grants `actions` on the instance ARN — or on `*` for
25 * `Describe*` actions, which do not support resource-level permissions.
26 */
27export const makeInstanceHttpBinding = <I extends object, A, E, R>(options: {
28 /** Fully-qualified binding tag, e.g. `AWS.EC2.StartInstance`. */
29 tag: string;
30 /** The distilled operation; the instance id is injected from the instance. */
31 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
32 /** IAM actions granted by the binding. */
33 actions: readonly string[];
34 /** How the operation names the instance in its request. */
35 requestKey: "InstanceId" | "InstanceIds";
36 /**
37 * IAM resource scope. Mutating actions support resource-level permissions
38 * on the instance ARN (`"instance"`, the default); `Describe*` actions do
39 * not (`"*"`).
40 */
41 resource?: "instance" | "*";
42}) =>
43 Effect.gen(function* () {
44 const op = yield* options.operation;
45
46 return Effect.fn(function* (instance: Instance) {
47 const instanceId = yield* instance.instanceId;
48 if (!globalThis.__ALCHEMY_RUNTIME__) {
49 const host = yield* Binding.Host;
50 if (isBindingHost(host) || isInstance(host)) {
51 yield* host.bind`Allow(${host}, ${options.tag}(${instance}))`({
52 policyStatements: [
53 {
54 Effect: "Allow",
55 Action: [...options.actions],
56 Resource:
57 options.resource === "*"
58 ? ["*"]
59 : [Output.interpolate`${instance.instanceArn}`],
60 },
61 ],
62 });
63 }
64 }
65 return Effect.fn(`${options.tag}(${instance.LogicalId})`)(function* (
66 request?: Omit<I, "InstanceId" | "InstanceIds">,
67 ) {
68 const id = yield* instanceId;
69 return yield* op({
70 ...request,
71 [options.requestKey]:
72 options.requestKey === "InstanceIds" ? [id] : id,
73 } as I);
74 });
75 });
76 });
77
78/**
79 * Build the impl Effect for an operation scoped to a bound
80 * {@link SecurityGroup}. The runtime callable injects the group id as
81 * `GroupId` and the deploy-time half grants `actions` on the security group
82 * ARN.
83 */
84export const makeSecurityGroupHttpBinding = <
85 I extends object,
86 A,
87 E,
88 R,
89>(options: {
90 /** Fully-qualified binding tag, e.g. `AWS.EC2.AuthorizeSecurityGroupIngress`. */
91 tag: string;
92 /** The distilled operation; `GroupId` is injected from the group. */
93 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
94 /** IAM actions granted on the security group ARN. */
95 actions: readonly string[];
96}) =>
97 Effect.gen(function* () {
98 const op = yield* options.operation;
99
100 return Effect.fn(function* (group: SecurityGroup) {
101 const groupId = yield* group.groupId;
102 if (!globalThis.__ALCHEMY_RUNTIME__) {
103 const host = yield* Binding.Host;
104 if (isBindingHost(host) || isInstance(host)) {
105 yield* host.bind`Allow(${host}, ${options.tag}(${group}))`({
106 policyStatements: [
107 {
108 Effect: "Allow",
109 Action: [...options.actions],
110 Resource: [Output.interpolate`${group.groupArn}`],
111 },
112 ],
113 });
114 }
115 }
116 return Effect.fn(`${options.tag}(${group.LogicalId})`)(function* (
117 request: Omit<I, "GroupId" | "GroupName">,
118 ) {
119 return yield* op({
120 ...request,
121 GroupId: yield* groupId,
122 } as I);
123 });
124 });
125 });
126
127/**
128 * Build the impl Effect for an operation scoped to a bound {@link Volume}.
129 * The runtime callable injects the volume id as `VolumeId` and the
130 * deploy-time half grants `actions` on the volume ARN plus any
131 * `extraResources` (e.g. the region-wide snapshot wildcard `CreateSnapshot`
132 * also authorizes against).
133 */
134export const makeVolumeHttpBinding = <I extends object, A, E, R>(options: {
135 /** Fully-qualified binding tag, e.g. `AWS.EC2.CreateSnapshot`. */
136 tag: string;
137 /** The distilled operation; `VolumeId` is injected from the volume. */
138 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
139 /** IAM actions granted on the volume ARN (+ `extraResources`). */
140 actions: readonly string[];
141 /** Additional IAM resource ARNs (e.g. `arn:aws:ec2:*::snapshot/*`). */
142 extraResources?: readonly string[];
143}) =>
144 Effect.gen(function* () {
145 const op = yield* options.operation;
146
147 return Effect.fn(function* (volume: Volume) {
148 const volumeId = yield* volume.volumeId;
149 if (!globalThis.__ALCHEMY_RUNTIME__) {
150 const host = yield* Binding.Host;
151 if (isBindingHost(host) || isInstance(host)) {
152 yield* host.bind`Allow(${host}, ${options.tag}(${volume}))`({
153 policyStatements: [
154 {
155 Effect: "Allow",
156 Action: [...options.actions],
157 Resource: [
158 Output.interpolate`${volume.volumeArn}`,
159 ...(options.extraResources ?? []),
160 ],
161 },
162 ],
163 });
164 }
165 }
166 return Effect.fn(`${options.tag}(${volume.LogicalId})`)(function* (
167 request?: Omit<I, "VolumeId">,
168 ) {
169 return yield* op({
170 ...request,
171 VolumeId: yield* volumeId,
172 } as I);
173 });
174 });
175 });