Files
floci-ui/packages/api/src/aws.ts
T
Zakaria bb5ca85a88
CI / checks (pull_request) Successful in 26m49s
feat(api): add HTTP and WebSocket APIs (API Gateway v2) to the API Gateway page
API Gateway v2 is a separate AWS API from REST APIs, so HTTP and WebSocket APIs
made with the SDK, SAM, CDK or Terraform never showed up. The API Gateway page
now lists all three kinds with a Type column, creates each from the same form,
and opens HTTP and WebSocket APIs in their own panel:

- Quick create: an HTTP API with a Lambda gets a catch-all route on an
  auto-deployed $default stage, plus the Lambda permission.
- Routes to Lambda (payload 2.0) or HTTP backends; deleting a route removes
  the integration and Lambda permission only it used.
- Stages with or without automatic deployment, and deploy on demand.
- JWT authorizers filled in from a Cognito pool's OpenID Connect discovery
  document and an app client, or from any issuer; protect routes with them.
- CORS configuration.
- A tester that sends a real request to a stage, and for WebSocket APIs one
  that connects, sends a message and collects the replies.

Capabilities are advertised under a new httpApiActions group, covered by the
cloudProxy guard test. Verified end to end against TheCloud's runtime.
2026-10-25 09:18:50 -04:00

191 lines
7.6 KiB
TypeScript

import { S3Client } from "@aws-sdk/client-s3";
import { LambdaClient } from "@aws-sdk/client-lambda";
import { EKSClient } from "@aws-sdk/client-eks";
import { EC2Client } from "@aws-sdk/client-ec2";
import { RDSClient } from "@aws-sdk/client-rds";
import { RDSDataClient } from "@aws-sdk/client-rds-data";
import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager";
import { CloudWatchLogsClient } from "@aws-sdk/client-cloudwatch-logs";
import { SSMClient } from "@aws-sdk/client-ssm";
import { KMSClient } from "@aws-sdk/client-kms";
import { SFNClient } from "@aws-sdk/client-sfn";
import { ElasticLoadBalancingV2Client } from "@aws-sdk/client-elastic-load-balancing-v2";
import { EventBridgeClient } from "@aws-sdk/client-eventbridge";
import { SQSClient } from "@aws-sdk/client-sqs";
import { IAMClient } from "@aws-sdk/client-iam";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { APIGatewayClient } from "@aws-sdk/client-api-gateway";
import { ApiGatewayV2Client } from "@aws-sdk/client-apigatewayv2";
import { CloudFormationClient } from "@aws-sdk/client-cloudformation";
import { AppConfigClient } from "@aws-sdk/client-appconfig";
import { SNSClient } from "@aws-sdk/client-sns";
import { ECRClient } from "@aws-sdk/client-ecr";
import { CodeBuildClient } from "@aws-sdk/client-codebuild";
import { CodePipelineClient } from "@aws-sdk/client-codepipeline";
import { CodeDeployClient } from "@aws-sdk/client-codedeploy";
import { ECSClient } from "@aws-sdk/client-ecs";
import { KafkaClient } from "@aws-sdk/client-kafka";
import { KafkaConnectClient } from "@aws-sdk/client-kafkaconnect";
import { KinesisClient } from "@aws-sdk/client-kinesis";
import { SESv2Client } from "@aws-sdk/client-sesv2";
import { CloudWatchClient } from "@aws-sdk/client-cloudwatch";
import { ElastiCacheClient } from "@aws-sdk/client-elasticache";
import { LightsailClient } from "@aws-sdk/client-lightsail";
import { CognitoIdentityProviderClient } from "@aws-sdk/client-cognito-identity-provider";
import { Route53Client } from "@aws-sdk/client-route-53";
import { CloudFrontClient } from "@aws-sdk/client-cloudfront";
const endpoint = process.env.FLOCI_ENDPOINT;
const region = process.env.AWS_REGION || "us-east-1";
// Floci derives the AWS account from AWS_ACCESS_KEY_ID: a value that is exactly
// 12 digits is used verbatim as the account id, and resources are isolated per
// account. Any other key (e.g. "test", "AKIA...") falls back to
// FLOCI_DEFAULT_ACCOUNT_ID (000000000000). We lean on that contract to scope the
// console to an account: the access key *is* the account id and the secret can
// be any non-empty string (Floci does not validate it).
const DEFAULT_ACCOUNT_ID = process.env.FLOCI_DEFAULT_ACCOUNT_ID || "000000000000";
const SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY || "floci";
export function isAccountId(value: string | null | undefined): value is string {
return typeof value === "string" && /^\d{12}$/.test(value);
}
/** The account used when a request does not (validly) specify one. */
export function defaultAccountId(): string {
const envKey = process.env.AWS_ACCESS_KEY_ID;
return isAccountId(envKey) ? envKey : DEFAULT_ACCOUNT_ID;
}
/** Normalize a caller-supplied account id, falling back to the default. */
export function resolveAccountId(raw?: string | null): string {
return isAccountId(raw) ? raw : defaultAccountId();
}
export type AwsClients = {
s3: S3Client;
lambda: LambdaClient;
eks: EKSClient;
ec2: EC2Client;
rds: RDSClient;
rdsData: RDSDataClient;
secretsManager: SecretsManagerClient;
logs: CloudWatchLogsClient;
ssm: SSMClient;
kms: KMSClient;
sfn: SFNClient;
elbv2: ElasticLoadBalancingV2Client;
eventbridge: EventBridgeClient;
sqs: SQSClient;
iam: IAMClient;
dynamodb: DynamoDBClient;
apiGateway: APIGatewayClient;
apiGatewayV2: ApiGatewayV2Client;
cloudformation: CloudFormationClient;
appConfig: AppConfigClient;
sns: SNSClient;
ecr: ECRClient;
codebuild: CodeBuildClient;
codepipeline: CodePipelineClient;
codedeploy: CodeDeployClient;
ecs: ECSClient;
kafka: KafkaClient;
kafkaConnect: KafkaConnectClient;
kinesis: KinesisClient;
sesv2: SESv2Client;
cloudwatch: CloudWatchClient;
elasticache: ElastiCacheClient;
lightsail: LightsailClient;
cognito: CognitoIdentityProviderClient;
route53: Route53Client;
cloudfront: CloudFrontClient;
};
export type AwsClientName = keyof AwsClients;
const clientCache = new Map<string, AwsClients>();
function buildClients(accountId: string): AwsClients {
const base = {
region,
credentials: { accessKeyId: accountId, secretAccessKey: SECRET_ACCESS_KEY },
...(endpoint ? { endpoint, forcePathStyle: true } : {}),
};
return {
s3: new S3Client({ ...base, forcePathStyle: true }),
lambda: new LambdaClient(base),
eks: new EKSClient(base),
ec2: new EC2Client(base),
rds: new RDSClient(base),
rdsData: new RDSDataClient(base),
secretsManager: new SecretsManagerClient(base),
logs: new CloudWatchLogsClient(base),
ssm: new SSMClient(base),
kms: new KMSClient(base),
sfn: new SFNClient(base),
elbv2: new ElasticLoadBalancingV2Client(base),
eventbridge: new EventBridgeClient(base),
sqs: new SQSClient(base),
iam: new IAMClient(base),
dynamodb: new DynamoDBClient(base),
apiGateway: new APIGatewayClient(base),
apiGatewayV2: new ApiGatewayV2Client(base),
cloudformation: new CloudFormationClient(base),
appConfig: new AppConfigClient(base),
sns: new SNSClient(base),
ecr: new ECRClient(base),
codebuild: new CodeBuildClient(base),
codepipeline: new CodePipelineClient(base),
codedeploy: new CodeDeployClient(base),
ecs: new ECSClient(base),
kafka: new KafkaClient(base),
kafkaConnect: new KafkaConnectClient(base),
kinesis: new KinesisClient(base),
sesv2: new SESv2Client(base),
cloudwatch: new CloudWatchClient(base),
elasticache: new ElastiCacheClient(base),
lightsail: new LightsailClient(base),
cognito: new CognitoIdentityProviderClient(base),
route53: new Route53Client(base),
cloudfront: new CloudFrontClient(base),
};
}
/**
* Return the cached AWS SDK client set bound to a given account. Clients are
* cheap to build but each holds connection state, so we memoize one set per
* resolved account id.
*/
export function awsClientsForAccount(accountId?: string | null): AwsClients {
const id = resolveAccountId(accountId);
let clients = clientCache.get(id);
if (!clients) {
clients = buildClients(id);
clientCache.set(id, clients);
}
return clients;
}
// Default-account singletons, kept for surfaces that are not yet account-aware:
// the dedicated EC2, EKS, RDS and Secrets Manager routes. These ignore the
// x-floci-account-id header and always serve the default account, so they do not
// follow the console's active account. The Cloud Explorer proxy resolves clients
// per request via awsClientsForAccount() instead.
export const awsClients = awsClientsForAccount(defaultAccountId());
export const s3 = awsClients.s3;
export const lambda = awsClients.lambda;
export const eks = awsClients.eks;
export const ec2 = awsClients.ec2;
export const rds = awsClients.rds;
export const secretsManager = awsClients.secretsManager;
export const eventbridge = awsClients.eventbridge;
export const sqs = awsClients.sqs;
export const iam = awsClients.iam;
export const dynamodb = awsClients.dynamodb;
export const apiGateway = awsClients.apiGateway;
export const apiGatewayV2 = awsClients.apiGatewayV2;
export const cloudformation = awsClients.cloudformation;
export const ecr = awsClients.ecr;
export const kinesis = awsClients.kinesis;