seahaven-door-unlock-api/lib/door-unlock-stack.ts
Adam Moussa b0976f94eb Add lockdown profile toggle endpoints with T58W linekey support
Add a new Lambda handler that toggles Elements lockdown profiles
(Bohemia and Ronkonkoma) via the Elements API, with status
verification before and after each toggle. Returns Yealink XML
to control linekey LEDs (green=inactive, red=locked down).

Also brings both Lambda handlers into compliance with system
standards: Node 22.x runtime, arm64 architecture, 60-day log
retention, and kebab-case function names.
2026-04-28 18:05:38 -04:00

179 lines
5.8 KiB
TypeScript

import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as apigwv2 from "aws-cdk-lib/aws-apigatewayv2";
import * as integrations from "aws-cdk-lib/aws-apigatewayv2-integrations";
import * as ssm from "aws-cdk-lib/aws-ssm";
import * as route53 from "aws-cdk-lib/aws-route53";
import * as route53Targets from "aws-cdk-lib/aws-route53-targets";
import * as acm from "aws-cdk-lib/aws-certificatemanager";
import * as logs from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
import * as path from "path";
export class DoorUnlockStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const elementsApiKeyParam = ssm.StringParameter.fromSecureStringParameterAttributes(
this,
"ElementsApiKey",
{ parameterName: "/seahaven/door-unlock/elements-api-key" }
);
const authTokenParam = ssm.StringParameter.fromSecureStringParameterAttributes(
this,
"AuthToken",
{ parameterName: "/seahaven/door-unlock/auth-token" }
);
const doorIdParam = ssm.StringParameter.fromStringParameterName(
this,
"DoorId",
"/seahaven/door-unlock/door-id"
);
const unlockHandler = new lambda.Function(this, "UnlockHandler", {
functionName: "door-unlock-api-unlock",
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
handler: "unlock-handler.handler",
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/unlock"), {
bundling: {
image: lambda.Runtime.NODEJS_22_X.bundlingImage,
local: {
tryBundle(outputDir: string) {
const { execSync } = require("child_process");
execSync(
`esbuild ${path.join(__dirname, "../lambda/unlock/unlock-handler.ts")} --bundle --platform=node --target=node22 --outfile=${path.join(outputDir, "unlock-handler.js")} --external:@aws-sdk/*`
);
return true;
},
},
},
}),
environment: {
ELEMENTS_API_KEY_PARAM: "/seahaven/door-unlock/elements-api-key",
AUTH_TOKEN_PARAM: "/seahaven/door-unlock/auth-token",
DOOR_ID_PARAM: "/seahaven/door-unlock/door-id",
},
timeout: cdk.Duration.seconds(10),
memorySize: 128,
logRetention: logs.RetentionDays.TWO_MONTHS,
});
const lockdownHandler = new lambda.Function(this, "LockdownHandler", {
functionName: "door-unlock-api-lockdown",
runtime: lambda.Runtime.NODEJS_22_X,
architecture: lambda.Architecture.ARM_64,
handler: "lockdown-handler.handler",
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/lockdown"), {
bundling: {
image: lambda.Runtime.NODEJS_22_X.bundlingImage,
local: {
tryBundle(outputDir: string) {
const { execSync } = require("child_process");
execSync(
`esbuild ${path.join(__dirname, "../lambda/lockdown/lockdown-handler.ts")} --bundle --platform=node --target=node22 --outfile=${path.join(outputDir, "lockdown-handler.js")} --external:@aws-sdk/*`
);
return true;
},
},
},
}),
environment: {
ELEMENTS_API_KEY_PARAM: "/seahaven/door-unlock/elements-api-key",
AUTH_TOKEN_PARAM: "/seahaven/door-unlock/auth-token",
},
timeout: cdk.Duration.seconds(15),
memorySize: 128,
logRetention: logs.RetentionDays.TWO_MONTHS,
});
elementsApiKeyParam.grantRead(unlockHandler);
authTokenParam.grantRead(unlockHandler);
doorIdParam.grantRead(unlockHandler);
elementsApiKeyParam.grantRead(lockdownHandler);
authTokenParam.grantRead(lockdownHandler);
const httpApi = new apigwv2.HttpApi(this, "DoorUnlockApi", {
apiName: "door-unlock-api",
});
const defaultStage = httpApi.defaultStage!.node.defaultChild as apigwv2.CfnStage;
defaultStage.addPropertyOverride("DefaultRouteSettings", {
ThrottlingBurstLimit: 5,
ThrottlingRateLimit: 2,
});
httpApi.addRoutes({
path: "/unlock",
methods: [apigwv2.HttpMethod.GET],
integration: new integrations.HttpLambdaIntegration(
"UnlockIntegration",
unlockHandler
),
});
const lockdownIntegration = new integrations.HttpLambdaIntegration(
"LockdownIntegration",
lockdownHandler
);
httpApi.addRoutes({
path: "/lockdown",
methods: [apigwv2.HttpMethod.GET],
integration: lockdownIntegration,
});
httpApi.addRoutes({
path: "/lockdown/status",
methods: [apigwv2.HttpMethod.GET],
integration: lockdownIntegration,
});
const hostedZone = route53.HostedZone.fromHostedZoneAttributes(
this,
"SeaHavenZone",
{
hostedZoneId: "Z06652411XKH89KTZD3XA",
zoneName: "seahaven.com",
}
);
const certificate = acm.Certificate.fromCertificateArn(
this,
"WildcardCert",
"arn:aws:acm:us-east-1:328440206208:certificate/a66c0994-90d4-410a-a1d9-5595c2a3fae3"
);
const domainName = new apigwv2.DomainName(this, "DoorUnlockDomain", {
domainName: "doorunlock.seahaven.com",
certificate,
});
new apigwv2.ApiMapping(this, "DoorUnlockMapping", {
api: httpApi,
domainName,
});
new route53.ARecord(this, "DoorUnlockARecord", {
zone: hostedZone,
recordName: "doorunlock",
target: route53.RecordTarget.fromAlias(
new route53Targets.ApiGatewayv2DomainProperties(
domainName.regionalDomainName,
domainName.regionalHostedZoneId
)
),
});
new cdk.CfnOutput(this, "ApiUrl", {
value: `https://doorunlock.seahaven.com/unlock`,
});
new cdk.CfnOutput(this, "LockdownApiUrl", {
value: `https://doorunlock.seahaven.com/lockdown`,
});
}
}