{
 "AWSTemplateFormatVersion": "2010-09-09",
 "Description": "Cloud Window: your private dashboard for one Omarchy computer in this AWS account. Creating the stack starts no computer; you create it from the dashboard.",
 "Parameters": {
  "OwnerEmail": {
   "Type": "String"
  },
  "ArtifactBucket": {
   "Type": "String"
  },
  "BackendKey": {
   "Type": "String"
  },
  "CognitoDomainPrefix": {
   "Type": "String",
   "Default": "",
   "MaxLength": 63,
   "AllowedPattern": "^$|^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$",
   "Description": "Unique login prefix for each new workspace. Empty preserves the original account-region login domain."
  },
  "DesktopAmi": {
   "Type": "String",
   "Default": ""
  },
  "AvailabilityZone": {
   "Type": "AWS::EC2::AvailabilityZone::Name"
  },
  "SiteSourceBucket": {
   "Type": "String",
   "Default": "",
   "Description": "Public bucket holding the published website files. Empty means the deployer uploads the site itself."
  },
  "SiteSourcePrefix": {
   "Type": "String",
   "Default": "",
   "Description": "Prefix of the published website files inside SiteSourceBucket, ending with a slash."
  },
  "ControllerReservedConcurrency": {
   "Type": "String",
   "Default": "1",
   "AllowedValues": [
    "0",
    "1"
   ],
   "Description": "1 reserves one execution for the controller (needs a Lambda concurrency quota above the AWS default); 0 leaves the reservation off."
  },
  "DashboardOrigin": {
   "Type": "String",
   "Default": "https://getcloudwindow.com",
   "AllowedPattern": "^https://[a-z0-9.-]+$",
   "Description": "The Cloud Window website that may open this dashboard from its computers page. Your stack still hosts its own copy."
  },
  "CustomDomain": {
   "Type": "String",
   "Default": "",
   "AllowedPattern": "^$|^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?\\.[a-z]{2,}$"
  },
  "CustomCertificateArn": {
   "Type": "String",
   "Default": "",
   "AllowedPattern": "^$|^arn:aws:acm:us-east-1:[0-9]{12}:certificate/[a-zA-Z0-9-]+$"
  }
 },
 "Resources": {
  "Site": {
   "Type": "AWS::S3::Bucket",
   "Properties": {
    "PublicAccessBlockConfiguration": {
     "BlockPublicAcls": true,
     "BlockPublicPolicy": true,
     "IgnorePublicAcls": true,
     "RestrictPublicBuckets": true
    },
    "BucketEncryption": {
     "ServerSideEncryptionConfiguration": [
      {
       "ServerSideEncryptionByDefault": {
        "SSEAlgorithm": "AES256"
       }
      }
     ]
    }
   }
  },
  "OriginAccess": {
   "Type": "AWS::CloudFront::OriginAccessControl",
   "Properties": {
    "OriginAccessControlConfig": {
     "Name": {
      "Fn::Sub": "${AWS::StackName}-site"
     },
     "OriginAccessControlOriginType": "s3",
     "SigningBehavior": "always",
     "SigningProtocol": "sigv4"
    }
   }
  },
  "SecurityHeaders": {
   "Type": "AWS::CloudFront::ResponseHeadersPolicy",
   "Properties": {
    "ResponseHeadersPolicyConfig": {
     "Name": {
      "Fn::Sub": "${AWS::StackName}-security"
     },
     "SecurityHeadersConfig": {
      "ContentTypeOptions": {
       "Override": true
      },
      "ReferrerPolicy": {
       "ReferrerPolicy": "no-referrer",
       "Override": true
      },
      "FrameOptions": {
       "FrameOption": "DENY",
       "Override": true
      },
      "StrictTransportSecurity": {
       "AccessControlMaxAgeSec": 31536000,
       "IncludeSubdomains": true,
       "Override": true
      },
      "ContentSecurityPolicy": {
       "ContentSecurityPolicy": "default-src 'self'; script-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://*.amazonaws.com https://*.amazoncognito.com; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self' https://*.amazoncognito.com",
       "Override": true
      }
     }
    }
   }
  },
  "Distribution": {
   "Type": "AWS::CloudFront::Distribution",
   "Properties": {
    "DistributionConfig": {
     "Enabled": true,
     "DefaultRootObject": "index.html",
     "PriceClass": "PriceClass_100",
     "HttpVersion": "http2",
     "Origins": [
      {
       "Id": "site",
       "DomainName": {
        "Fn::GetAtt": [
         "Site",
         "RegionalDomainName"
        ]
       },
       "OriginAccessControlId": {
        "Ref": "OriginAccess"
       },
       "S3OriginConfig": {
        "OriginAccessIdentity": ""
       }
      }
     ],
     "DefaultCacheBehavior": {
      "TargetOriginId": "site",
      "ViewerProtocolPolicy": "redirect-to-https",
      "AllowedMethods": [
       "GET",
       "HEAD"
      ],
      "CachedMethods": [
       "GET",
       "HEAD"
      ],
      "Compress": true,
      "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
      "ResponseHeadersPolicyId": {
       "Ref": "SecurityHeaders"
      }
     },
     "Aliases": {
      "Fn::If": [
       "HasCustomDomain",
       [
        {
         "Ref": "CustomDomain"
        }
       ],
       {
        "Ref": "AWS::NoValue"
       }
      ]
     },
     "ViewerCertificate": {
      "Fn::If": [
       "HasCustomDomain",
       {
        "AcmCertificateArn": {
         "Ref": "CustomCertificateArn"
        },
        "SslSupportMethod": "sni-only",
        "MinimumProtocolVersion": "TLSv1.2_2021"
       },
       {
        "CloudFrontDefaultCertificate": true
       }
      ]
     }
    }
   }
  },
  "SitePolicy": {
   "Type": "AWS::S3::BucketPolicy",
   "Properties": {
    "Bucket": {
     "Ref": "Site"
    },
    "PolicyDocument": {
     "Version": "2012-10-17",
     "Statement": [
      {
       "Effect": "Allow",
       "Principal": {
        "Service": "cloudfront.amazonaws.com"
       },
       "Action": "s3:GetObject",
       "Resource": {
        "Fn::Sub": "${Site.Arn}/*"
       },
       "Condition": {
        "StringEquals": {
         "AWS:SourceArn": {
          "Fn::Sub": "arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}"
         }
        }
       }
      }
     ]
    }
   }
  },
  "State": {
   "Type": "AWS::DynamoDB::Table",
   "Properties": {
    "BillingMode": "PAY_PER_REQUEST",
    "AttributeDefinitions": [
     {
      "AttributeName": "id",
      "AttributeType": "S"
     }
    ],
    "KeySchema": [
     {
      "AttributeName": "id",
      "KeyType": "HASH"
     }
    ],
    "SSESpecification": {
     "SSEEnabled": true
    }
   }
  },
  "Vpc": {
   "Type": "AWS::EC2::VPC",
   "Properties": {
    "CidrBlock": "10.73.0.0/16",
    "EnableDnsSupport": true,
    "EnableDnsHostnames": true
   }
  },
  "Gateway": {
   "Type": "AWS::EC2::InternetGateway",
   "Properties": {}
  },
  "GatewayAttachment": {
   "Type": "AWS::EC2::VPCGatewayAttachment",
   "Properties": {
    "VpcId": {
     "Ref": "Vpc"
    },
    "InternetGatewayId": {
     "Ref": "Gateway"
    }
   }
  },
  "Subnet": {
   "Type": "AWS::EC2::Subnet",
   "Properties": {
    "VpcId": {
     "Ref": "Vpc"
    },
    "CidrBlock": "10.73.1.0/24",
    "AvailabilityZone": {
     "Ref": "AvailabilityZone"
    },
    "MapPublicIpOnLaunch": true
   }
  },
  "RouteTable": {
   "Type": "AWS::EC2::RouteTable",
   "Properties": {
    "VpcId": {
     "Ref": "Vpc"
    }
   }
  },
  "Route": {
   "Type": "AWS::EC2::Route",
   "Properties": {
    "RouteTableId": {
     "Ref": "RouteTable"
    },
    "DestinationCidrBlock": "0.0.0.0/0",
    "GatewayId": {
     "Ref": "Gateway"
    }
   },
   "DependsOn": "GatewayAttachment"
  },
  "RouteAssociation": {
   "Type": "AWS::EC2::SubnetRouteTableAssociation",
   "Properties": {
    "SubnetId": {
     "Ref": "Subnet"
    },
    "RouteTableId": {
     "Ref": "RouteTable"
    }
   }
  },
  "DesktopSecurity": {
   "Type": "AWS::EC2::SecurityGroup",
   "Properties": {
    "VpcId": {
     "Ref": "Vpc"
    },
    "GroupDescription": "Encrypted Tailscale UDP only. No public SSH, desktop, or Sunshine admin.",
    "SecurityGroupIngress": [
     {
      "IpProtocol": "udp",
      "FromPort": 41641,
      "ToPort": 41641,
      "CidrIp": "0.0.0.0/0"
     }
    ],
    "SecurityGroupEgress": [
     {
      "IpProtocol": "-1",
      "CidrIp": "0.0.0.0/0"
     }
    ]
   }
  },
  "GuestRole": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Version": "2012-10-17",
     "Statement": [
      {
       "Effect": "Allow",
       "Principal": {
        "Service": "ec2.amazonaws.com"
       },
       "Action": "sts:AssumeRole"
      }
     ]
    },
    "Policies": [
     {
      "PolicyName": "DesktopPassword",
      "PolicyDocument": {
       "Version": "2012-10-17",
       "Statement": [
        {
         "Effect": "Allow",
         "Action": [
          "ssm:DescribeAssociation",
          "ssm:GetDeployablePatchSnapshotForInstance",
          "ssm:GetDocument",
          "ssm:DescribeDocument",
          "ssm:GetManifest",
          "ssm:ListAssociations",
          "ssm:ListInstanceAssociations",
          "ssm:PutInventory",
          "ssm:PutComplianceItems",
          "ssm:PutConfigurePackageResult",
          "ssm:UpdateAssociationStatus",
          "ssm:UpdateInstanceAssociationStatus",
          "ssm:UpdateInstanceInformation",
          "ssmmessages:CreateControlChannel",
          "ssmmessages:CreateDataChannel",
          "ssmmessages:OpenControlChannel",
          "ssmmessages:OpenDataChannel",
          "ec2messages:AcknowledgeMessage",
          "ec2messages:DeleteMessage",
          "ec2messages:FailMessage",
          "ec2messages:GetEndpoint",
          "ec2messages:GetMessages",
          "ec2messages:SendReply"
         ],
         "Resource": "*"
        },
        {
         "Effect": "Allow",
         "Action": [
          "ssm:GetParameter",
          "ssm:DeleteParameter"
         ],
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/cloud-window/${AWS::StackName}/password/*"
         }
        },
        {
         "Effect": "Allow",
         "Action": "s3:GetObject",
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:s3:::ec2-linux-nvidia-drivers/*"
         }
        }
       ]
      }
     }
    ]
   }
  },
  "GuestProfile": {
   "Type": "AWS::IAM::InstanceProfile",
   "Properties": {
    "Roles": [
     {
      "Ref": "GuestRole"
     }
    ]
   }
  },
  "ControllerRole": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Version": "2012-10-17",
     "Statement": [
      {
       "Effect": "Allow",
       "Principal": {
        "Service": "lambda.amazonaws.com"
       },
       "Action": "sts:AssumeRole"
      }
     ]
    },
    "ManagedPolicyArns": [
     {
      "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
     }
    ],
    "Policies": [
     {
      "PolicyName": "OneDesktop",
      "PolicyDocument": {
       "Version": "2012-10-17",
       "Statement": [
        {
         "Effect": "Allow",
         "Action": [
          "ssm:PutParameter",
          "ssm:DeleteParameter"
         ],
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}:${AWS::AccountId}:parameter/cloud-window/${AWS::StackName}/password/*"
         }
        },
        {
         "Effect": "Allow",
         "Action": [
          "ec2:DescribeInstances",
          "ec2:DescribeImages"
         ],
         "Resource": "*"
        },
        {
         "Effect": "Allow",
         "Action": [
          "ec2:StartInstances",
          "ec2:StopInstances"
         ],
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
         },
         "Condition": {
          "StringEquals": {
           "ec2:ResourceTag/CloudWindow": {
            "Ref": "AWS::StackName"
           }
          }
         }
        },
        {
         "Effect": "Allow",
         "Action": "ec2:RunInstances",
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
         },
         "Condition": {
          "StringEquals": {
           "ec2:InstanceType": "g6f.xlarge",
           "aws:RequestTag/CloudWindow": {
            "Ref": "AWS::StackName"
           }
          }
         }
        },
        {
         "Effect": "Allow",
         "Action": "ec2:RunInstances",
         "Resource": [
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}::image/*"
          },
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
          },
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:network-interface/*"
          },
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:subnet/*"
          },
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:security-group/*"
          }
         ]
        },
        {
         "Effect": "Allow",
         "Action": "ec2:CreateTags",
         "Resource": [
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
          },
          {
           "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
          }
         ],
         "Condition": {
          "StringEquals": {
           "ec2:CreateAction": "RunInstances"
          }
         }
        },
        {
         "Effect": "Allow",
         "Action": "iam:PassRole",
         "Resource": {
          "Fn::GetAtt": [
           "GuestRole",
           "Arn"
          ]
         },
         "Condition": {
          "StringEquals": {
           "iam:PassedToService": "ec2.amazonaws.com"
          }
         }
        },
        {
         "Effect": "Allow",
         "Action": "ssm:SendCommand",
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:ssm:${AWS::Region}::document/AWS-RunShellScript"
         }
        },
        {
         "Effect": "Allow",
         "Action": "ssm:SendCommand",
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
         },
         "Condition": {
          "StringEquals": {
           "ssm:resourceTag/CloudWindow": {
            "Ref": "AWS::StackName"
           }
          }
         }
        },
        {
         "Effect": "Allow",
         "Action": "ssm:GetCommandInvocation",
         "Resource": "*"
        },
        {
         "Effect": "Allow",
         "Action": "cloudwatch:GetMetricStatistics",
         "Resource": "*"
        },
        {
         "Effect": "Allow",
         "Action": [
          "dynamodb:GetItem",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem"
         ],
         "Resource": {
          "Fn::GetAtt": [
           "State",
           "Arn"
          ]
         }
        }
       ]
      }
     }
    ]
   }
  },
  "Controller": {
   "Type": "AWS::Lambda::Function",
   "Properties": {
    "Runtime": "python3.13",
    "Handler": "app.handler",
    "Role": {
     "Fn::GetAtt": [
      "ControllerRole",
      "Arn"
     ]
    },
    "MemorySize": 256,
    "Timeout": 28,
    "ReservedConcurrentExecutions": {
     "Fn::If": [
      "ReserveController",
      1,
      {
       "Ref": "AWS::NoValue"
      }
     ]
    },
    "Code": {
     "S3Bucket": {
      "Ref": "ArtifactBucket"
     },
     "S3Key": {
      "Ref": "BackendKey"
     }
    },
    "Environment": {
     "Variables": {
      "OWNER_EMAIL": {
       "Ref": "OwnerEmail"
      },
      "STATE_TABLE": {
       "Ref": "State"
      },
      "STACK_NAME": {
       "Ref": "AWS::StackName"
      },
      "DESKTOP_AMI": {
       "Ref": "DesktopAmi"
      },
      "INSTANCE_PROFILE": {
       "Ref": "GuestProfile"
      },
      "SUBNET_ID": {
       "Ref": "Subnet"
      },
      "SECURITY_GROUP": {
       "Ref": "DesktopSecurity"
      }
     }
    }
   }
  },
  "ControllerLogs": {
   "Type": "AWS::Logs::LogGroup",
   "Properties": {
    "LogGroupName": {
     "Fn::Sub": "/aws/lambda/${Controller}"
    },
    "RetentionInDays": 7
   }
  },
  "GuardSchedule": {
   "Type": "AWS::Events::Rule",
   "Properties": {
    "ScheduleExpression": "rate(5 minutes)",
    "State": "ENABLED",
    "Targets": [
     {
      "Id": "guard",
      "Arn": {
       "Fn::GetAtt": [
        "Controller",
        "Arn"
       ]
      }
     }
    ]
   }
  },
  "GuardPermission": {
   "Type": "AWS::Lambda::Permission",
   "Properties": {
    "Action": "lambda:InvokeFunction",
    "FunctionName": {
     "Ref": "Controller"
    },
    "Principal": "events.amazonaws.com",
    "SourceArn": {
     "Fn::GetAtt": [
      "GuardSchedule",
      "Arn"
     ]
    }
   }
  },
  "UserPool": {
   "Type": "AWS::Cognito::UserPool",
   "Properties": {
    "UserPoolName": {
     "Fn::Sub": "${AWS::StackName}-owner"
    },
    "UsernameAttributes": [
     "email"
    ],
    "AutoVerifiedAttributes": [
     "email"
    ],
    "Policies": {
     "PasswordPolicy": {
      "MinimumLength": 12,
      "RequireLowercase": true,
      "RequireUppercase": true,
      "RequireNumbers": true,
      "RequireSymbols": false
     }
    },
    "AccountRecoverySetting": {
     "RecoveryMechanisms": [
      {
       "Name": "verified_email",
       "Priority": 1
      }
     ]
    },
    "LambdaConfig": {
     "PreSignUp": {
      "Fn::GetAtt": [
       "Controller",
       "Arn"
      ]
     }
    }
   }
  },
  "SignupPermission": {
   "Type": "AWS::Lambda::Permission",
   "Properties": {
    "Action": "lambda:InvokeFunction",
    "FunctionName": {
     "Ref": "Controller"
    },
    "Principal": "cognito-idp.amazonaws.com",
    "SourceArn": {
     "Fn::GetAtt": [
      "UserPool",
      "Arn"
     ]
    }
   }
  },
  "LoginDomain": {
   "Type": "AWS::Cognito::UserPoolDomain",
   "Properties": {
    "Domain": {
     "Fn::If": [
      "HasCognitoDomainPrefix",
      {
       "Ref": "CognitoDomainPrefix"
      },
      {
       "Fn::Sub": "cw-${AWS::AccountId}-${AWS::Region}"
      }
     ]
    },
    "UserPoolId": {
     "Ref": "UserPool"
    }
   }
  },
  "WebClient": {
   "Type": "AWS::Cognito::UserPoolClient",
   "Properties": {
    "UserPoolId": {
     "Ref": "UserPool"
    },
    "GenerateSecret": false,
    "SupportedIdentityProviders": [
     "COGNITO"
    ],
    "AllowedOAuthFlowsUserPoolClient": true,
    "AllowedOAuthFlows": [
     "code"
    ],
    "AllowedOAuthScopes": [
     "openid",
     "email"
    ],
    "CallbackURLs": {
     "Fn::If": [
      "HasCustomDomain",
      [
       {
        "Fn::Sub": "https://${Distribution.DomainName}/"
       },
       {
        "Fn::Sub": "https://${CustomDomain}/"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard.html"
       }
      ],
      [
       {
        "Fn::Sub": "https://${Distribution.DomainName}/"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard.html"
       }
      ]
     ]
    },
    "LogoutURLs": {
     "Fn::If": [
      "HasCustomDomain",
      [
       {
        "Fn::Sub": "https://${Distribution.DomainName}/"
       },
       {
        "Fn::Sub": "https://${CustomDomain}/"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard.html"
       }
      ],
      [
       {
        "Fn::Sub": "https://${Distribution.DomainName}/"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard"
       },
       {
        "Fn::Sub": "${DashboardOrigin}/dashboard.html"
       }
      ]
     ]
    },
    "PreventUserExistenceErrors": "ENABLED",
    "IdTokenValidity": 1,
    "AccessTokenValidity": 1,
    "RefreshTokenValidity": 1
   }
  },
  "Api": {
   "Type": "AWS::ApiGatewayV2::Api",
   "Properties": {
    "Name": {
     "Fn::Sub": "${AWS::StackName}-api"
    },
    "ProtocolType": "HTTP",
    "CorsConfiguration": {
     "AllowOrigins": {
      "Fn::If": [
       "HasCustomDomain",
       [
        {
         "Fn::Sub": "https://${Distribution.DomainName}"
        },
        {
         "Fn::Sub": "https://${CustomDomain}"
        },
        {
         "Ref": "DashboardOrigin"
        }
       ],
       [
        {
         "Fn::Sub": "https://${Distribution.DomainName}"
        },
        {
         "Ref": "DashboardOrigin"
        }
       ]
      ]
     },
     "AllowMethods": [
      "GET",
      "POST",
      "OPTIONS"
     ],
     "AllowHeaders": [
      "authorization",
      "content-type"
     ],
     "MaxAge": 300
    }
   }
  },
  "Authorizer": {
   "Type": "AWS::ApiGatewayV2::Authorizer",
   "Properties": {
    "ApiId": {
     "Ref": "Api"
    },
    "AuthorizerType": "JWT",
    "IdentitySource": [
     "$request.header.Authorization"
    ],
    "Name": "OwnerLogin",
    "JwtConfiguration": {
     "Audience": [
      {
       "Ref": "WebClient"
      }
     ],
     "Issuer": {
      "Fn::Sub": "https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}"
     }
    }
   }
  },
  "Integration": {
   "Type": "AWS::ApiGatewayV2::Integration",
   "Properties": {
    "ApiId": {
     "Ref": "Api"
    },
    "IntegrationType": "AWS_PROXY",
    "IntegrationUri": {
     "Fn::GetAtt": [
      "Controller",
      "Arn"
     ]
    },
    "PayloadFormatVersion": "2.0",
    "TimeoutInMillis": 28000
   }
  },
  "ApiRoute": {
   "Type": "AWS::ApiGatewayV2::Route",
   "Properties": {
    "ApiId": {
     "Ref": "Api"
    },
    "RouteKey": "GET /api/{proxy+}",
    "Target": {
     "Fn::Sub": "integrations/${Integration}"
    },
    "AuthorizationType": "JWT",
    "AuthorizerId": {
     "Ref": "Authorizer"
    }
   }
  },
  "ApiPostRoute": {
   "Type": "AWS::ApiGatewayV2::Route",
   "Properties": {
    "ApiId": {
     "Ref": "Api"
    },
    "RouteKey": "POST /api/{proxy+}",
    "Target": {
     "Fn::Sub": "integrations/${Integration}"
    },
    "AuthorizationType": "JWT",
    "AuthorizerId": {
     "Ref": "Authorizer"
    }
   }
  },
  "ApiStage": {
   "Type": "AWS::ApiGatewayV2::Stage",
   "Properties": {
    "ApiId": {
     "Ref": "Api"
    },
    "StageName": "$default",
    "AutoDeploy": true,
    "DefaultRouteSettings": {
     "ThrottlingBurstLimit": 5,
     "ThrottlingRateLimit": 2
    }
   }
  },
  "ApiPermission": {
   "Type": "AWS::Lambda::Permission",
   "Properties": {
    "Action": "lambda:InvokeFunction",
    "FunctionName": {
     "Ref": "Controller"
    },
    "Principal": "apigateway.amazonaws.com",
    "SourceArn": {
     "Fn::Sub": "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${Api}/*"
    }
   }
  },
  "Budget": {
   "Type": "AWS::Budgets::Budget",
   "Properties": {
    "Budget": {
     "BudgetName": {
      "Fn::Sub": "${AWS::StackName}-trial"
     },
     "BudgetLimit": {
      "Amount": 20,
      "Unit": "USD"
     },
     "TimeUnit": "MONTHLY",
     "BudgetType": "COST"
    },
    "NotificationsWithSubscribers": [
     {
      "Notification": {
       "NotificationType": "ACTUAL",
       "ComparisonOperator": "GREATER_THAN",
       "Threshold": 50,
       "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
       {
        "SubscriptionType": "EMAIL",
        "Address": {
         "Ref": "OwnerEmail"
        }
       }
      ]
     },
     {
      "Notification": {
       "NotificationType": "ACTUAL",
       "ComparisonOperator": "GREATER_THAN",
       "Threshold": 80,
       "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
       {
        "SubscriptionType": "EMAIL",
        "Address": {
         "Ref": "OwnerEmail"
        }
       }
      ]
     },
     {
      "Notification": {
       "NotificationType": "ACTUAL",
       "ComparisonOperator": "GREATER_THAN",
       "Threshold": 100,
       "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
       {
        "SubscriptionType": "EMAIL",
        "Address": {
         "Ref": "OwnerEmail"
        }
       }
      ]
     }
    ]
   }
  },
  "ReadyTopic": {
   "Type": "AWS::SNS::Topic",
   "Properties": {
    "DisplayName": "Cloud Window"
   },
   "Condition": "HasSiteSource"
  },
  "ReadySubscription": {
   "Type": "AWS::SNS::Subscription",
   "Properties": {
    "TopicArn": {
     "Ref": "ReadyTopic"
    },
    "Protocol": "email",
    "Endpoint": {
     "Ref": "OwnerEmail"
    }
   },
   "Condition": "HasSiteSource"
  },
  "SiteDeployRole": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Version": "2012-10-17",
     "Statement": [
      {
       "Effect": "Allow",
       "Principal": {
        "Service": "lambda.amazonaws.com"
       },
       "Action": "sts:AssumeRole"
      }
     ]
    },
    "ManagedPolicyArns": [
     {
      "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
     }
    ],
    "Policies": [
     {
      "PolicyName": "CopySite",
      "PolicyDocument": {
       "Version": "2012-10-17",
       "Statement": [
        {
         "Effect": "Allow",
         "Action": [
          "s3:GetObject"
         ],
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:s3:::${SiteSourceBucket}/${SiteSourcePrefix}*"
         }
        },
        {
         "Effect": "Allow",
         "Action": [
          "s3:ListBucket"
         ],
         "Resource": {
          "Fn::Sub": "arn:${AWS::Partition}:s3:::${SiteSourceBucket}"
         }
        },
        {
         "Effect": "Allow",
         "Action": [
          "s3:PutObject",
          "s3:DeleteObject"
         ],
         "Resource": {
          "Fn::Sub": "${Site.Arn}/*"
         }
        },
        {
         "Effect": "Allow",
         "Action": [
          "s3:ListBucket"
         ],
         "Resource": {
          "Fn::GetAtt": [
           "Site",
           "Arn"
          ]
         }
        },
        {
         "Effect": "Allow",
         "Action": [
          "sns:Publish"
         ],
         "Resource": {
          "Ref": "ReadyTopic"
         }
        }
       ]
      }
     }
    ]
   },
   "Condition": "HasSiteSource"
  },
  "SiteDeployFunction": {
   "Type": "AWS::Lambda::Function",
   "Properties": {
    "Runtime": "python3.13",
    "Handler": "index.handler",
    "Role": {
     "Fn::GetAtt": [
      "SiteDeployRole",
      "Arn"
     ]
    },
    "MemorySize": 256,
    "Timeout": 300,
    "Code": {
     "ZipFile": "# CloudFormation custom resource (inline, under 4 KB): copy the published site into\n# this stack's bucket, write config.json from stack outputs, email the owner the\n# link, and empty the bucket on delete. Always answers CloudFormation.\nimport json, mimetypes, urllib.request, boto3\ns3 = boto3.client('s3')\nT = {'.html': 'text/html; charset=utf-8', '.css': 'text/css; charset=utf-8', '.js': 'text/javascript; charset=utf-8',\n     '.svg': 'image/svg+xml', '.webp': 'image/webp', '.json': 'application/json', '.png': 'image/png', '.mp4': 'video/mp4'}\n\ndef respond(e, c, status, reason=''):\n    b = json.dumps({'Status': status, 'Reason': (reason or c.log_stream_name)[:1000], 'PhysicalResourceId': e.get('PhysicalResourceId') or e['LogicalResourceId'],\n                    'StackId': e['StackId'], 'RequestId': e['RequestId'], 'LogicalResourceId': e['LogicalResourceId'], 'Data': {}}).encode()\n    urllib.request.urlopen(urllib.request.Request(e['ResponseURL'], data=b, method='PUT', headers={'Content-Type': '', 'Content-Length': str(len(b))}), timeout=30).read()\n\ndef keys(bucket, prefix):\n    t = None\n    while True:\n        p = s3.list_objects_v2(**{'Bucket': bucket, 'Prefix': prefix, **({'ContinuationToken': t} if t else {})})\n        for i in p.get('Contents', []): yield i['Key']\n        t = p.get('NextContinuationToken')\n        if not t: return\n\ndef fix(v):\n    return {k: fix(x) for k, x in v.items()} if isinstance(v, dict) else (v.lower() == 'true' if isinstance(v, str) and v.lower() in ('true', 'false') else v)\n\ndef attach(p, c):\n    import base64\n    d = {k: c[k] for k in ('apiBase', 'cognitoDomain', 'clientId')}; d['name'] = p.get('StackName', 'Cloud Window')\n    return p['DashboardOrigin'] + '/computers#attach=' + base64.urlsafe_b64encode(json.dumps(d).encode()).decode().rstrip('=')\n\ndef notify(p, c):\n    try:\n        boto3.client('sns').publish(TopicArn=p['ReadyTopic'], Subject='Your Cloud Window dashboard is ready', Message=(\n            'Your Cloud Window computer is ready to create. Open it here (one click adds it to your Cloud Window account, or remembers it on this device):\\n\\n' + attach(p, c) +\n            '\\n\\nSign in there, then Create computer; the first boot takes five to eight minutes while it fetches and builds the GPU driver inside your account.\\n\\n'\n            'Your stack also hosts its own copy of the dashboard: ' + p['Website'] + '\\n\\nGuide: https://getcloudwindow.com/setup.html\\n'))\n    except Exception as x:\n        print('notify failed: %s: %s' % (type(x).__name__, x))\n\ndef handler(e, c):\n    p = e.get('ResourceProperties', {})\n    try:\n        site = p['SiteBucket']\n        if e['RequestType'] == 'Delete':\n            for k in list(keys(site, '')): s3.delete_object(Bucket=site, Key=k)\n            return respond(e, c, 'SUCCESS', 'Site bucket emptied.')\n        src, pre, n = p['SourceBucket'], p['SourcePrefix'], 0\n        for k in keys(src, pre):\n            r = k[len(pre):]\n            if not r or r.endswith('/') or r == 'config.json': continue\n            ext = '.' + r.rsplit('.', 1)[-1] if '.' in r else ''\n            s3.copy_object(Bucket=site, Key=r, CopySource={'Bucket': src, 'Key': k}, MetadataDirective='REPLACE',\n                           ContentType=T.get(ext) or mimetypes.guess_type(r)[0] or 'application/octet-stream',\n                           CacheControl='no-store' if r.endswith('.html') else 'public, max-age=300')\n            n += 1\n        if not n: raise RuntimeError('No website files under s3://%s/%s' % (src, pre))\n        c = fix(p['Config'])\n        s3.put_object(Bucket=site, Key='config.json', Body=json.dumps(c).encode(), ContentType='application/json', CacheControl='no-store')\n        if p.get('ReadyTopic') and p.get('Website') and p.get('DashboardOrigin'): notify(p, c)\n        respond(e, c, 'SUCCESS', 'Copied %d files.' % n)\n    except Exception as x:\n        respond(e, c, 'FAILED', '%s: %s' % (type(x).__name__, x))\n"
    }
   },
   "Condition": "HasSiteSource"
  },
  "SiteDeployLogs": {
   "Type": "AWS::Logs::LogGroup",
   "Properties": {
    "LogGroupName": {
     "Fn::Sub": "/aws/lambda/${SiteDeployFunction}"
    },
    "RetentionInDays": 7
   },
   "Condition": "HasSiteSource"
  },
  "SiteFiles": {
   "Type": "Custom::SiteFiles",
   "Properties": {
    "ServiceToken": {
     "Fn::GetAtt": [
      "SiteDeployFunction",
      "Arn"
     ]
    },
    "SourceBucket": {
     "Ref": "SiteSourceBucket"
    },
    "SourcePrefix": {
     "Ref": "SiteSourcePrefix"
    },
    "SiteBucket": {
     "Ref": "Site"
    },
    "ReadyTopic": {
     "Ref": "ReadyTopic"
    },
    "Website": {
     "Fn::Sub": "https://${Distribution.DomainName}"
    },
    "OwnerEmail": {
     "Ref": "OwnerEmail"
    },
    "DashboardOrigin": {
     "Ref": "DashboardOrigin"
    },
    "StackName": {
     "Ref": "AWS::StackName"
    },
    "Config": {
     "preview": false,
     "apiBase": {
      "Fn::GetAtt": [
       "Api",
       "ApiEndpoint"
      ]
     },
     "cognitoDomain": {
      "Fn::Sub": "https://${LoginDomain}.auth.${AWS::Region}.amazoncognito.com"
     },
     "clientId": {
      "Ref": "WebClient"
     },
     "downloadAvailable": false
    }
   },
   "Condition": "HasSiteSource",
   "DependsOn": [
    "SitePolicy",
    "ApiStage",
    "ReadySubscription"
   ]
  }
 },
 "Outputs": {
  "Website": {
   "Value": {
    "Fn::If": [
     "HasCustomDomain",
     {
      "Fn::Sub": "https://${CustomDomain}"
     },
     {
      "Fn::Sub": "https://${Distribution.DomainName}"
     }
    ]
   }
  },
  "SiteBucket": {
   "Value": {
    "Ref": "Site"
   }
  },
  "ApiBase": {
   "Value": {
    "Fn::GetAtt": [
     "Api",
     "ApiEndpoint"
    ]
   }
  },
  "ClientId": {
   "Value": {
    "Ref": "WebClient"
   }
  },
  "CognitoDomain": {
   "Value": {
    "Fn::Sub": "https://${LoginDomain}.auth.${AWS::Region}.amazoncognito.com"
   }
  },
  "UserPoolId": {
   "Value": {
    "Ref": "UserPool"
   }
  },
  "Controller": {
   "Value": {
    "Ref": "Controller"
   }
  },
  "GuestProfile": {
   "Value": {
    "Ref": "GuestProfile"
   }
  },
  "Subnet": {
   "Value": {
    "Ref": "Subnet"
   }
  },
  "SecurityGroup": {
   "Value": {
    "Ref": "DesktopSecurity"
   }
  },
  "StateTable": {
   "Value": {
    "Ref": "State"
   }
  }
 },
 "Conditions": {
  "HasCognitoDomainPrefix": {
   "Fn::Not": [
    {
     "Fn::Equals": [
      {
       "Ref": "CognitoDomainPrefix"
      },
      ""
     ]
    }
   ]
  },
  "HasSiteSource": {
   "Fn::Not": [
    {
     "Fn::Equals": [
      {
       "Ref": "SiteSourceBucket"
      },
      ""
     ]
    }
   ]
  },
  "ReserveController": {
   "Fn::Equals": [
    {
     "Ref": "ControllerReservedConcurrency"
    },
    "1"
   ]
  },
  "HasCustomDomain": {
   "Fn::Not": [
    {
     "Fn::Equals": [
      {
       "Ref": "CustomDomain"
      },
      ""
     ]
    }
   ]
  }
 },
 "Rules": {
  "DomainRequiresCertificate": {
   "Assertions": [
    {
     "Assert": {
      "Fn::Or": [
       {
        "Fn::And": [
         {
          "Fn::Not": [
           {
            "Fn::Equals": [
             {
              "Ref": "CustomDomain"
             },
             ""
            ]
           }
          ]
         },
         {
          "Fn::Not": [
           {
            "Fn::Equals": [
             {
              "Ref": "CustomCertificateArn"
             },
             ""
            ]
           }
          ]
         }
        ]
       },
       {
        "Fn::And": [
         {
          "Fn::Equals": [
           {
            "Ref": "CustomDomain"
           },
           ""
          ]
         },
         {
          "Fn::Equals": [
           {
            "Ref": "CustomCertificateArn"
           },
           ""
          ]
         }
        ]
       }
      ]
     },
     "AssertDescription": "Provide both CustomDomain and its us-east-1 ACM certificate, or neither."
    }
   ]
  }
 }
}