seahaven-door-unlock-api/lib/door-unlock-stack.ts

408 lines
15 KiB
TypeScript
Raw Normal View History

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";
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
import * as authorizers from "aws-cdk-lib/aws-apigatewayv2-authorizers";
import * as ssm from "aws-cdk-lib/aws-ssm";
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as events from "aws-cdk-lib/aws-events";
import * as targets from "aws-cdk-lib/aws-events-targets";
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 * as cloudwatch from "aws-cdk-lib/aws-cloudwatch";
import * as cwactions from "aws-cdk-lib/aws-cloudwatch-actions";
import * as sns from "aws-cdk-lib/aws-sns";
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" }
);
chore(security): resolve open dependabot and code scanning alerts (#59) * ci: add least-privilege permissions blocks to workflow callers Resolves code scanning alerts #3 and #4 (actions/missing-workflow-permissions). Both callable workflows only need contents: read; the dependency-review callable already declares it internally, this caps the caller token to match. Signed-off-by: Adam Moussa <adam@seahavenind.com> * build(deps): bump aws-cdk-lib to 2.262.0 to patch brace-expansion aws-cdk-lib 2.261.0 bundles brace-expansion 5.0.6, which is vulnerable to CVE-2026-13149 (GHSA-3jxr-9vmj-r5cp), an exponential-time DoS in expand(). This was the only path pulling the vulnerable package into the tree. 2.262.0 vendors the patched 5.0.7, resolving Dependabot alert 7. Because brace-expansion arrives bundled inside the aws-cdk-lib tarball rather than resolved by npm, the aws-cdk-lib bump is the only way to move it. Signed-off-by: Adam Moussa <adam@seahavenind.com> * refactor(cdk): drop unreferenced ssm value parameters fromStringParameterName injects an AWS::SSM::Parameter::Value CloudFormation parameter to carry the parameter's value, but DoorId and PhoneIps are only used for grantRead, which builds the ARN from the name string — so DoorIdParameter and PhoneIpsParameter sat unreferenced in the template (flagged W2001 by the CloudFormation validator newly bundled in aws-cdk-lib 2.262.0). Switching to fromStringParameterAttributes with forceDynamicReference resolves the value lazily via an SSM dynamic reference, emitting nothing when unused. Verified the synthesized template is identical apart from the removed Parameters entries and the CDKMetadata analytics hash — no IAM or resource changes. Also re-points the four line-keyed semgrep detect-child-process suppressions (adjudicated FPs, INFRA-105) to the shifted line numbers; the findings themselves are unchanged. Signed-off-by: Adam Moussa <adam@seahavenind.com> --------- Signed-off-by: Adam Moussa <adam@seahavenind.com>
2026-07-23 16:24:23 -04:00
// forceDynamicReference: the stack only needs the parameter for grantRead
// (ARN, built from the name); without it, fromStringParameterName injects
// an unreferenced AWS::SSM::Parameter::Value CloudFormation parameter.
const doorIdParam = ssm.StringParameter.fromStringParameterAttributes(
this,
"DoorId",
chore(security): resolve open dependabot and code scanning alerts (#59) * ci: add least-privilege permissions blocks to workflow callers Resolves code scanning alerts #3 and #4 (actions/missing-workflow-permissions). Both callable workflows only need contents: read; the dependency-review callable already declares it internally, this caps the caller token to match. Signed-off-by: Adam Moussa <adam@seahavenind.com> * build(deps): bump aws-cdk-lib to 2.262.0 to patch brace-expansion aws-cdk-lib 2.261.0 bundles brace-expansion 5.0.6, which is vulnerable to CVE-2026-13149 (GHSA-3jxr-9vmj-r5cp), an exponential-time DoS in expand(). This was the only path pulling the vulnerable package into the tree. 2.262.0 vendors the patched 5.0.7, resolving Dependabot alert 7. Because brace-expansion arrives bundled inside the aws-cdk-lib tarball rather than resolved by npm, the aws-cdk-lib bump is the only way to move it. Signed-off-by: Adam Moussa <adam@seahavenind.com> * refactor(cdk): drop unreferenced ssm value parameters fromStringParameterName injects an AWS::SSM::Parameter::Value CloudFormation parameter to carry the parameter's value, but DoorId and PhoneIps are only used for grantRead, which builds the ARN from the name string — so DoorIdParameter and PhoneIpsParameter sat unreferenced in the template (flagged W2001 by the CloudFormation validator newly bundled in aws-cdk-lib 2.262.0). Switching to fromStringParameterAttributes with forceDynamicReference resolves the value lazily via an SSM dynamic reference, emitting nothing when unused. Verified the synthesized template is identical apart from the removed Parameters entries and the CDKMetadata analytics hash — no IAM or resource changes. Also re-points the four line-keyed semgrep detect-child-process suppressions (adjudicated FPs, INFRA-105) to the shifted line numbers; the findings themselves are unchanged. Signed-off-by: Adam Moussa <adam@seahavenind.com> --------- Signed-off-by: Adam Moussa <adam@seahavenind.com>
2026-07-23 16:24:23 -04:00
{
parameterName: "/seahaven/door-unlock/door-id",
forceDynamicReference: true,
}
);
const unlockHandler = new lambda.Function(this, "UnlockHandler", {
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
functionName: "door-unlock-api-unlock",
runtime: lambda.Runtime.NODEJS_24_X,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
architecture: lambda.Architecture.ARM_64,
handler: "unlock-handler.handler",
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/unlock"), {
bundling: {
image: lambda.Runtime.NODEJS_24_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=node24 --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(20),
memorySize: 128,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
logRetention: logs.RetentionDays.TWO_MONTHS,
});
const lockdownHandler = new lambda.Function(this, "LockdownHandler", {
functionName: "door-unlock-api-lockdown",
runtime: lambda.Runtime.NODEJS_24_X,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
architecture: lambda.Architecture.ARM_64,
handler: "lockdown-handler.handler",
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/lockdown"), {
bundling: {
image: lambda.Runtime.NODEJS_24_X.bundlingImage,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
local: {
tryBundle(outputDir: string) {
const { execSync } = require("child_process");
execSync(
`esbuild ${path.join(__dirname, "../lambda/lockdown/lockdown-handler.ts")} --bundle --platform=node --target=node24 --outfile=${path.join(outputDir, "lockdown-handler.js")} --external:@aws-sdk/*`
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
);
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);
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
elementsApiKeyParam.grantRead(lockdownHandler);
authTokenParam.grantRead(lockdownHandler);
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
// Gateway authorizer (INFRA-99): validates the same `?token=` query-string
// value the Yealink XML Browser keys already send, so it is transparent to
// the phones while rejecting unauthenticated callers at the gateway.
const authorizerHandler = new lambda.Function(this, "AuthorizerHandler", {
functionName: "door-unlock-api-authorizer",
runtime: lambda.Runtime.NODEJS_24_X,
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
architecture: lambda.Architecture.ARM_64,
handler: "authorizer-handler.handler",
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/authorizer"), {
bundling: {
image: lambda.Runtime.NODEJS_24_X.bundlingImage,
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
local: {
tryBundle(outputDir: string) {
const { execSync } = require("child_process");
execSync(
`esbuild ${path.join(__dirname, "../lambda/authorizer/authorizer-handler.ts")} --bundle --platform=node --target=node24 --outfile=${path.join(outputDir, "authorizer-handler.js")} --external:@aws-sdk/*`
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
);
return true;
},
},
},
}),
environment: {
AUTH_TOKEN_PARAM: authTokenParam.parameterName,
},
// APIGW HTTP API authorizers have a hard 10s limit; keep margin so the
// gateway returns its own 500 rather than racing the Lambda timeout. The
// token is cached in module scope, so warm invocations never hit SSM.
timeout: cdk.Duration.seconds(8),
memorySize: 128,
logRetention: logs.RetentionDays.TWO_MONTHS,
});
authTokenParam.grantRead(authorizerHandler);
const tokenAuthorizer = new authorizers.HttpLambdaAuthorizer(
"DoorUnlockTokenAuthorizer",
authorizerHandler,
{
authorizerName: "door-unlock-api-token-authorizer",
// The Yealink phones send the token in the query string; scope the
// identity source there. A request with no `token` query param is
// rejected by the gateway before the authorizer Lambda is invoked.
identitySource: ["$request.querystring.token"],
responseTypes: [authorizers.HttpLambdaResponseType.SIMPLE],
// Authorizer result caching keyed on the identity source (the token).
// 5 min keeps repeated phone presses fast without a long stale window.
resultsCacheTtl: cdk.Duration.minutes(5),
}
);
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
const vpc = ec2.Vpc.fromLookup(this, "SeaHavenVpc", {
vpcId: "vpc-0d3d4b67bd0cf8a68",
});
const privateSubnet1 = ec2.Subnet.fromSubnetId(
this, "PrivateSubnet1", "subnet-04e38c507e96f1926"
);
const privateSubnet2 = ec2.Subnet.fromSubnetId(
this, "PrivateSubnet2", "subnet-0a0b4fc6f296dfba5"
);
const pollerSg = new ec2.SecurityGroup(this, "PollerSecurityGroup", {
vpc,
securityGroupName: "door-unlock-api-poller",
description: "Lockdown poller - outbound to Elements API and phone LAN",
allowAllOutbound: false,
});
pollerSg.addEgressRule(
ec2.Peer.anyIpv4(), ec2.Port.tcp(443), "HTTPS to Elements API and SSM via NAT"
);
pollerSg.addEgressRule(
ec2.Peer.ipv4("10.10.0.0/16"), ec2.Port.tcp(443), "HTTPS to phone LAN via VPN"
);
chore(security): resolve open dependabot and code scanning alerts (#59) * ci: add least-privilege permissions blocks to workflow callers Resolves code scanning alerts #3 and #4 (actions/missing-workflow-permissions). Both callable workflows only need contents: read; the dependency-review callable already declares it internally, this caps the caller token to match. Signed-off-by: Adam Moussa <adam@seahavenind.com> * build(deps): bump aws-cdk-lib to 2.262.0 to patch brace-expansion aws-cdk-lib 2.261.0 bundles brace-expansion 5.0.6, which is vulnerable to CVE-2026-13149 (GHSA-3jxr-9vmj-r5cp), an exponential-time DoS in expand(). This was the only path pulling the vulnerable package into the tree. 2.262.0 vendors the patched 5.0.7, resolving Dependabot alert 7. Because brace-expansion arrives bundled inside the aws-cdk-lib tarball rather than resolved by npm, the aws-cdk-lib bump is the only way to move it. Signed-off-by: Adam Moussa <adam@seahavenind.com> * refactor(cdk): drop unreferenced ssm value parameters fromStringParameterName injects an AWS::SSM::Parameter::Value CloudFormation parameter to carry the parameter's value, but DoorId and PhoneIps are only used for grantRead, which builds the ARN from the name string — so DoorIdParameter and PhoneIpsParameter sat unreferenced in the template (flagged W2001 by the CloudFormation validator newly bundled in aws-cdk-lib 2.262.0). Switching to fromStringParameterAttributes with forceDynamicReference resolves the value lazily via an SSM dynamic reference, emitting nothing when unused. Verified the synthesized template is identical apart from the removed Parameters entries and the CDKMetadata analytics hash — no IAM or resource changes. Also re-points the four line-keyed semgrep detect-child-process suppressions (adjudicated FPs, INFRA-105) to the shifted line numbers; the findings themselves are unchanged. Signed-off-by: Adam Moussa <adam@seahavenind.com> --------- Signed-off-by: Adam Moussa <adam@seahavenind.com>
2026-07-23 16:24:23 -04:00
// forceDynamicReference for the same reason as DoorId above.
const phoneIpsParam = ssm.StringParameter.fromStringParameterAttributes(
this, "PhoneIps",
{ parameterName: "/seahaven/door-unlock/phone-ips", forceDynamicReference: true }
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
);
const phonePasswordSecret = secretsmanager.Secret.fromSecretNameV2(
this, "PhonePassword", "door-unlock-api/phone-password"
);
const pollerHandler = new lambda.Function(this, "LockdownPoller", {
functionName: "door-unlock-api-lockdown-poller",
runtime: lambda.Runtime.NODEJS_24_X,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
architecture: lambda.Architecture.ARM_64,
handler: "lockdown-poller.handler",
code: lambda.Code.fromAsset(path.join(__dirname, "../lambda/poller"), {
bundling: {
image: lambda.Runtime.NODEJS_24_X.bundlingImage,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
local: {
tryBundle(outputDir: string) {
const { execSync } = require("child_process");
execSync(
`esbuild ${path.join(__dirname, "../lambda/poller/lockdown-poller.ts")} --bundle --platform=node --target=node24 --outfile=${path.join(outputDir, "lockdown-poller.js")} --external:@aws-sdk/*`
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
);
return true;
},
},
},
}),
environment: {
ELEMENTS_API_KEY_PARAM: "/seahaven/door-unlock/elements-api-key",
PHONE_IPS_PARAM: "/seahaven/door-unlock/phone-ips",
PHONE_PASSWORD_SECRET: "door-unlock-api/phone-password",
},
vpc,
vpcSubnets: { subnets: [privateSubnet1, privateSubnet2] },
securityGroups: [pollerSg],
timeout: cdk.Duration.seconds(75),
memorySize: 128,
logRetention: logs.RetentionDays.TWO_MONTHS,
});
elementsApiKeyParam.grantRead(pollerHandler);
phoneIpsParam.grantRead(pollerHandler);
phonePasswordSecret.grantRead(pollerHandler);
new events.Rule(this, "LockdownPollerSchedule", {
ruleName: "door-unlock-api-lockdown-poller-schedule",
schedule: events.Schedule.rate(cdk.Duration.minutes(1)),
targets: [new targets.LambdaFunction(pollerHandler)],
});
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,
});
// Access logging (audit M-18). Throttling above was already present.
const apiAccessLogGroup = new logs.LogGroup(this, "ApiAccessLogGroup", {
logGroupName: "/aws/apigateway/door-unlock-api",
retention: logs.RetentionDays.THREE_MONTHS,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
defaultStage.addPropertyOverride("AccessLogSettings", {
DestinationArn: apiAccessLogGroup.logGroupArn,
Format: JSON.stringify({
requestId: "$context.requestId",
ip: "$context.identity.sourceIp",
requestTime: "$context.requestTime",
method: "$context.httpMethod",
routeKey: "$context.routeKey",
status: "$context.status",
protocol: "$context.protocol",
responseLength: "$context.responseLength",
integrationError: "$context.integrationErrorMessage",
}),
});
httpApi.addRoutes({
path: "/unlock",
methods: [apigwv2.HttpMethod.GET],
integration: new integrations.HttpLambdaIntegration(
"UnlockIntegration",
unlockHandler
),
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
authorizer: tokenAuthorizer,
});
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
const lockdownIntegration = new integrations.HttpLambdaIntegration(
"LockdownIntegration",
lockdownHandler
);
httpApi.addRoutes({
path: "/lockdown",
methods: [apigwv2.HttpMethod.GET],
integration: lockdownIntegration,
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
authorizer: tokenAuthorizer,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
});
httpApi.addRoutes({
path: "/lockdown/status",
methods: [apigwv2.HttpMethod.GET],
integration: lockdownIntegration,
Gateway token authorizer + finish CI/CD migration (INFRA-99, INFRA-2) (#32) * feat: add gateway token authorizer to door-unlock API (INFRA-99) All three routes (GET /unlock, /lockdown, /lockdown/status) were AuthorizationType NONE — auth relied solely on each handler checking the ?token= query param. Add a REQUEST-type HTTP API Lambda authorizer (door-unlock-api-authorizer) that validates the SAME ?token= value the Yealink XML Browser keys already send, against the existing /seahaven/door-unlock/auth-token SSM SecureString, and attach it to all three routes. Transparent to the phones: identity source is $request.querystring.token (exactly what the type-17 XML Browser keys send via GET), simple response {isAuthorized}, fail-closed, 5-min results cache. Token is cached in module scope so warm invocations skip SSM. GET is kept (not switched to POST): the Yealink type-17 XML Browser keys are GET-only and render the returned Yealink XML — they cannot issue a POST body or custom headers. POST is therefore deferred to avoid bricking the door keys. Handlers retain their own token check as defense-in-depth. Purely additive change set; no existing Lambda or integration is modified. * chore: complete CI/CD migration to GitHub Actions (INFRA-2) GitHub Actions (ci.yaml + deploy.yaml via the Sea Haven reusable workflows) is the proven deploy path. Remove the now-orphaned buildspec.yml and update the README CI/CD and architecture sections. The legacy CodePipeline was already deleted (2026-06-05); the leftover CodeBuild project seahaven-door-unlock-api-build and its IAM role seahaven-door-unlock-api-codebuild have now also been decommissioned.
2026-06-08 18:03:30 -04:00
authorizer: tokenAuthorizer,
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
});
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`,
});
Add lockdown mode and CI/CD pipeline (#3) * 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. * Add lockdown poller Lambda and fix lockdown handler responses - Add VPC-connected poller Lambda that monitors lockdown status via Elements API every 15 seconds (4 polls per 1-min EventBridge schedule) - Handle Elements API rate limits (429) with retry-after support - Fix lockdown handler to use TextScreen XML instead of Execute XML (Execute shows globe icon on T58W, TextScreen renders properly) - Fix Elements API status parsing to be case-insensitive - Trust toggle action instead of re-checking status (eventual consistency) - Configure push_xml.server = any in T58W template for Push XML support - Clear action_url.setup_completed (poller replaces boot-time check) - Update README with lockdown architecture and known LED limitation Note: T58W line key LED color does not change to reflect lockdown status. Execute LED commands are transient on the T58W - the phone's XML Browser key type immediately overrides them. * Add buildspec for CodePipeline CI/CD * Update README with CI/CD pipeline details
2026-05-01 18:52:37 -04:00
new cdk.CfnOutput(this, "LockdownApiUrl", {
value: `https://doorunlock.seahaven.com/lockdown`,
});
// ── CloudWatch error alarms (INFRA-101) ──────────────────────────────────
// Import the cross-stack site-alerts SNS topic. This topic is managed by
// seahaven-account-baseline and encrypted with alias/seahaven-alarm-topics
// (CMK). Never use alias/aws/sns here — CloudWatch cannot publish to topics
// using the AWS-managed SNS key (silent publish failure).
// ALARM state only; no OK/recovery actions per Sea Haven preference.
const siteAlerts = sns.Topic.fromTopicArn(
this,
"SiteAlerts",
"arn:aws:sns:us-east-1:328440206208:site-alerts"
);
interface AlarmSpec {
readonly id: string;
readonly fn: lambda.Function;
readonly alarmName: string;
readonly description: string;
}
const alarmSpecs: AlarmSpec[] = [
{
id: "UnlockErrors",
fn: unlockHandler,
alarmName: "door-unlock-api-unlock-errors",
description: "door-unlock-api-unlock Lambda is erroring — physical door unlock calls may be failing",
},
{
id: "LockdownErrors",
fn: lockdownHandler,
alarmName: "door-unlock-api-lockdown-errors",
description: "door-unlock-api-lockdown Lambda is erroring — lockdown commands may not be executing",
},
{
id: "AuthorizerErrors",
fn: authorizerHandler,
alarmName: "door-unlock-api-authorizer-errors",
description: "door-unlock-api-authorizer Lambda is erroring — all API requests will be rejected",
},
{
id: "PollerErrors",
fn: pollerHandler,
alarmName: "door-unlock-api-lockdown-poller-errors",
description: "door-unlock-api-lockdown-poller Lambda is erroring — lockdown state polling may be broken",
},
];
for (const spec of alarmSpecs) {
const alarm = new cloudwatch.Alarm(this, spec.id, {
alarmName: spec.alarmName,
alarmDescription: spec.description,
metric: spec.fn.metricErrors({
period: cdk.Duration.minutes(5),
statistic: "Sum",
}),
threshold: 0,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
evaluationPeriods: 1,
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
});
// ALARM-only: addAlarmAction only (no addOkAction / addInsufficientDataAction)
alarm.addAlarmAction(new cwactions.SnsAction(siteAlerts));
}
}
}