Skip to content
Source
Sign inSign up
Open History to see the last commit

BindingHttp.ts

160 lines5.9 KB

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