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 { Pipeline } from "./Pipeline.ts";
6
7/**
8 * Shared scaffolding for AWS CodePipeline HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
11 * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders
12 * below. Everything except the operation, the IAM action, and the injected
13 * pipeline-name key is boilerplate.
14 *
15 * CodePipeline addresses the pipeline as `name` on some operations
16 * (StartPipelineExecution, GetPipelineState) and `pipelineName` on the rest.
17 * Stage- and action-addressed operations authorize against sub-resource ARNs
18 * (`{pipelineArn}/{stage}` / `{pipelineArn}/{stage}/{action}`) — set
19 * `subScoped` to additionally grant on `{pipelineArn}/*`.
20 */
21
22const bindPipelinePolicy = Effect.fn(function* (
23 tag: string,
24 pipeline: Pipeline,
25 actions: readonly string[],
26 subScoped: boolean | undefined,
27) {
28 if (!globalThis.__ALCHEMY_RUNTIME__) {
29 const host = yield* Binding.Host;
30 if (isBindingHost(host)) {
31 yield* host.bind`Allow(${host}, ${tag}(${pipeline}))`({
32 policyStatements: [
33 {
34 Effect: "Allow",
35 Action: [...actions],
36 Resource: [
37 Output.interpolate`${pipeline.pipelineArn}`,
38 ...(subScoped
39 ? [Output.interpolate`${pipeline.pipelineArn}/*`]
40 : []),
41 ],
42 },
43 ],
44 });
45 }
46 }
47});
48
49/**
50 * Build the impl Effect for an operation that addresses the pipeline via a
51 * `name` field (StartPipelineExecution, GetPipelineState): the runtime
52 * callable injects the bound {@link Pipeline}'s name and the deploy-time
53 * half grants `actions` on the pipeline ARN.
54 */
55export const makeCodePipelineNameHttpBinding = <
56 I extends { name?: string },
57 A,
58 E,
59 R,
60>(options: {
61 /** Fully-qualified binding tag, e.g. `AWS.CodePipeline.GetPipelineState`. */
62 tag: string;
63 /** The distilled operation; `name` is injected from the pipeline. */
64 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
65 /** IAM actions granted on the pipeline ARN. */
66 actions: readonly string[];
67}) =>
68 Effect.gen(function* () {
69 const op = yield* options.operation;
70
71 return Effect.fn(function* <P extends Pipeline>(pipeline: P) {
72 // Outputs yield a DEFERRED effect — resolve again per invocation below.
73 const PipelineName = yield* pipeline.pipelineName;
74 yield* bindPipelinePolicy(options.tag, pipeline, options.actions, false);
75 return Effect.fn(`${options.tag}(${pipeline.LogicalId})`)(function* (
76 request?: Omit<I, "name">,
77 ) {
78 return yield* op({
79 ...request,
80 name: yield* PipelineName,
81 } as I);
82 });
83 });
84 });
85
86/**
87 * Build the impl Effect for an operation that addresses the pipeline via a
88 * `pipelineName` field (the majority): the runtime callable injects the
89 * bound {@link Pipeline}'s name and the deploy-time half grants `actions`
90 * on the pipeline ARN (plus `{pipelineArn}/*` when `subScoped` — stage- and
91 * action-addressed operations authorize against sub-resource ARNs).
92 */
93export const makeCodePipelinePipelineNameHttpBinding = <
94 I extends { pipelineName?: string },
95 A,
96 E,
97 R,
98>(options: {
99 /** Fully-qualified binding tag, e.g. `AWS.CodePipeline.RollbackStage`. */
100 tag: string;
101 /** The distilled operation; `pipelineName` is injected from the pipeline. */
102 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
103 /** IAM actions granted on the pipeline ARN. */
104 actions: readonly string[];
105 /** Also grant on `{pipelineArn}/*` (stage-/action-addressed actions). */
106 subScoped?: boolean;
107}) =>
108 Effect.gen(function* () {
109 const op = yield* options.operation;
110
111 return Effect.fn(function* <P extends Pipeline>(pipeline: P) {
112 const PipelineName = yield* pipeline.pipelineName;
113 yield* bindPipelinePolicy(
114 options.tag,
115 pipeline,
116 options.actions,
117 options.subScoped,
118 );
119 return Effect.fn(`${options.tag}(${pipeline.LogicalId})`)(function* (
120 request?: Omit<I, "pipelineName">,
121 ) {
122 return yield* op({
123 ...request,
124 pipelineName: yield* PipelineName,
125 } as I);
126 });
127 });
128 });
129
130/**
131 * Build the impl Effect for a job-worker operation (GetJobDetails,
132 * PutJobSuccessResult, PutJobFailureResult). CodePipeline job actions do
133 * not support resource-level permissions, so the deploy-time half grants
134 * `actions` on `*`; the runtime callable passes the request through as-is.
135 */
136export const makeCodePipelineJobHttpBinding = <I, A, E, R>(options: {
137 /** Fully-qualified binding tag, e.g. `AWS.CodePipeline.PutJobSuccessResult`. */
138 tag: string;
139 /** The distilled operation, invoked with the caller's request as-is. */
140 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
141 /** IAM actions granted on `*` (no resource-level permission support). */
142 actions: readonly string[];
143}) =>
144 Effect.gen(function* () {
145 const op = yield* options.operation;
146
147 return Effect.fn(function* () {
148 if (!globalThis.__ALCHEMY_RUNTIME__) {
149 const host = yield* Binding.Host;
150 if (isBindingHost(host)) {
151 yield* host.bind`Allow(${host}, ${options.tag}())`({
152 policyStatements: [
153 {
154 Effect: "Allow",
155 Action: [...options.actions],
156 Resource: ["*"],
157 },
158 ],
159 });
160 }
161 }
162 return Effect.fn(options.tag)(function* (request: I) {
163 return yield* op(request);
164 });
165 });
166 });