AWS Lambda middleware — Yealink desk phone DSS key unlocks front door via LenelS2 Elements API
Find a file
Adam Moussa bbc113497a
Some checks failed
Deploy / deploy (push) Has been cancelled
feat(3cx): sync office department blfs via xapi (PLAT-116) (#80)
* feat(3cx): sync office department BLFs via XAPI

Keep unlock and lockdown keys in the templates and write colleague plus shared-parking BLFs per extension so phones skip their own line.

* fix(3cx): preserve parking BLF IDs and fail the job on PATCH errors
2026-08-27 14:58:55 -04:00
.github/workflows chore(deps): remove dependabot version updates (#79) 2026-08-25 11:51:15 -04:00
.security-review feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
bin Rename stack to kebab-case 2026-05-01 18:56:59 -04:00
lambda feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
lib feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
scripts feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
.gitignore Add dependency-review caller workflow (#25) 2026-06-05 12:34:08 -04:00
.mergify.yml chore(ci): switch auto-merge from seahaven-bot to Mergify (#73) 2026-08-24 13:53:15 -04:00
AGENTS.md ci: add org PR policy caller 2026-08-11 11:15:08 -04:00
cdk.context.json Add lockdown mode and CI/CD pipeline (#3) 2026-05-01 18:52:37 -04:00
cdk.json build(deps): migrate CDK app ts-node->tsx, adopt typescript 7 (INFRA-183) (#54) 2026-07-08 17:48:18 -04:00
door-unlock-plan.md Add door unlock API — Yealink DSS key triggers LenelS2 Elements TemporaryUnlock via API Gateway + Lambda 2026-04-22 14:36:55 -04:00
package-lock.json feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
package.json feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
README.md feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
RUNBOOK-token-rotation.md security: door-unlock token rotation runbook + executed cutover (INFRA-105) (#47) 2026-07-06 16:54:32 -04:00
tsconfig.json feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
yealinkT54W-door-unlock-with-sp.ph.xml feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
yealinkT54W-door-unlock.ph.xml feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00
yealinkT58W-door-unlock.ph.xml feat(3cx): sync office department blfs via xapi (PLAT-116) (#80) 2026-08-27 14:58:55 -04:00

Sea Haven Door Unlock API

TypeScript AWS CDK CI

AWS Lambda middleware that allows Yealink desk phones to unlock the front door and manage lockdown profiles via LenelS2 Elements.

Yealink T54W/T58W → HTTPS GET (?token=) → API Gateway (token authorizer) → Lambda → LenelS2 Elements API

Architecture

  • API Gateway (HTTP API) — GET /unlock, GET /lockdown, GET /lockdown/status with throttling (5 burst / 2 sustained req/sec)
  • Token authorizer Lambda — a REQUEST-type Lambda authorizer validates the ?token= query-string value (the same shared secret the phones already send) against the /seahaven/door-unlock/auth-token SSM parameter, so unauthenticated callers are rejected at the gateway (401/403) before any handler runs. Identity source is $request.querystring.token; results are cached 5 minutes. Fail-closed. The handlers also re-validate the token as defense-in-depth.
  • Unlock Lambda — validates a shared auth token, calls the Elements TemporaryUnlock command
  • Lockdown Lambda — toggles lockdown profiles (start/stop) and checks status, returns Yealink XML TextScreen responses
  • Lockdown Poller Lambda — VPC-connected, polls Elements API every 15 seconds for lockdown status (runs 4x per 1-minute EventBridge schedule)
  • SSM Parameter Store — stores the Elements API key, auth token, door ID, and phone IPs
  • Secrets Manager — stores the Yealink phone admin password
  • Custom Domain — doorunlock.seahaven.com via Route 53 + ACM wildcard cert
  • CloudWatch alarms — one error alarm per Lambda (unlock, lockdown, authorizer, poller); each fires on Errors > 0 and notifies the cross-stack site-alerts SNS topic (ALARM state only)

Infrastructure (CDK)

All infrastructure is defined as code with the AWS CDK v2 (TypeScript); aws-cdk-lib is pinned to 2.261.0. The whole system is a single CloudFormation stack.

Layout

bin/app.ts                    # CDK app entry point
lib/door-unlock-stack.ts      # DoorUnlockStack — all resource definitions
lambda/
├── unlock/unlock-handler.ts        # Unlock Lambda
├── lockdown/lockdown-handler.ts    # Lockdown Lambda
├── poller/lockdown-poller.ts       # Lockdown Poller Lambda
├── authorizer/authorizer-handler.ts # Token authorizer Lambda
└── blf-sync/blf-sync-handler.ts    # 3CX department BLF sync Lambda
cdk.json                      # CDK config (app command, watch, context flags)

bin/app.ts

Instantiates DoorUnlockStack with an explicit stackName of seahaven-door-unlock-api, pinned to account 328440206208 / us-east-1.

lib/door-unlock-stack.ts

Defines every resource the stack owns:

  • The five Lambda functions (Node 24.x, arm64, 60-day log retention), bundled from TypeScript with esbuild
  • The HTTP API (door-unlock-api), its GET /unlock, GET /lockdown, and GET /lockdown/status routes, throttling, and JSON access logging
  • The HttpLambdaAuthorizer token authorizer (identity source $request.querystring.token, 5-minute result cache)
  • The EventBridge rule that invokes the poller once a minute, plus the poller's VPC config and security group (imported VPC/subnets, egress to the Elements API and phone LAN)
  • The EventBridge rule that invokes department BLF sync daily at 09:00 UTC, plus imports of the afterhours 3CX XAPI secrets
  • The custom domain, ACM certificate import, and Route 53 A record for doorunlock.seahaven.com
  • Imports of the SSM parameters, the phone-password secret, the afterhours 3CX XAPI secrets, and the site-alerts SNS topic, with the corresponding grantRead IAM permissions
  • The five per-Lambda CloudWatch error alarms

cdk.json

CDK configuration committed to the repo. The app command runs npx tsx bin/app.ts, so the TypeScript entry point executes directly via tsx (no separate compile step). It also carries the watch include/exclude globs and the CDK feature-flag context.

Commands

npx cdk synth   # synthesize the CloudFormation template
npx cdk diff    # diff against the deployed stack
npx cdk deploy  # deploy (see Manual Deployment below)

The same commands are also exposed as npm scripts (npm run synth, npm run diff, npm run deploy).

Documentation

The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's seahaven-door-unlock-api stack is represented there as a Mermaid subgraph.

Lockdown Profiles

Two lockdown profiles are configured:

Profile Elements ID Line Key
Bohemia - Whole Building 4b4a3e6b-c903-4cce-8cd6-288612bf0542 3
Ronkonkoma - Whole Building ff9876bc-c54f-472e-aef9-d2bffd4b7cf7 4

Pressing the line key toggles the lockdown on/off and displays the current status on the phone screen.

Known limitation: Line key LED color does not currently change to reflect lockdown status. The T58W's XML Browser key type (17) does not support persistent LED color changes via Push XML or Execute commands — LED commands are transient and immediately overridden by the phone's key type management.

SSM Parameters

Parameter Type Description
/seahaven/door-unlock/elements-api-key SecureString LenelS2 Elements API key
/seahaven/door-unlock/auth-token SecureString Shared secret embedded in the Yealink DSS key URL
/seahaven/door-unlock/door-id String Elements device ID for the front door reader
/seahaven/door-unlock/phone-ips String Comma-separated phone IPs for lockdown poller

Secrets Manager

Secret Description
door-unlock-api/phone-password Yealink phone admin password for Push XML

CI/CD

GitHub Actions, using the Sea Haven reusable workflows:

  • .github/workflows/ci.yaml — on pull requests to main, runs the ci-typescript-cdk reusable workflow (build, lint, synth).
  • .github/workflows/deploy.yaml — on push to main, runs the cd-cdk reusable workflow which assumes the githubdeploy-seahaven-door-unlock-api OIDC role (AWS_DEPLOY_ROLE_ARN repo secret) and runs cdk deploy.

The legacy CodePipeline/CodeBuild deploy path has been fully decommissioned.

Manual Deployment

npm install
npx cdk deploy

Phone Configuration

Configure DSS keys on the Yealink T54W/T58W (via phone web UI or 3CX):

  • Key 2 — Unlock Door

    • Type: URL
    • Value: https://doorunlock.seahaven.com/unlock?token=<auth-token>
  • Keys 3-4 — Lockdown Toggle

    • Type: XML Browser (17)
    • Value: https://doorunlock.seahaven.com/lockdown?token=<auth-token>&profile=bohemia|ronkonkoma

3CX Provisioning Templates

Custom 3CX templates are included with door unlock and lockdown URLs hardcoded.

Template Model Key 2 Keys 3-4 Display
yealinkT54W-door-unlock.ph.xml T54W Unlock Door Managed by 3CX BLF Dim after 5 min, never sleep
yealinkT54W-door-unlock-with-sp.ph.xml T54W Unlock Door SP1-3 via BLF sync Dim after 5 min, never sleep
yealinkT58W-door-unlock.ph.xml T58W Unlock Door Lockdown Toggle (Bohemia/Ronkonkoma) Default T58W display settings

Department colleague BLFs are not encoded in these templates. A scheduled Lambda (door-unlock-api-blf-sync) writes each Yealink user's 3CX BLF list from that user's first non-DEFAULT 3CX department, excluding the phone's own extension. Extension 100 is always included, even when the XAPI Users list omits it. Unlock and lockdown URL keys stay hardcoded in the template. Shared parking on the T54W+SP template is written by the sync job as 3CX SharedParking BLFs.

Template Reserved (never write) Sync-owned parking Own line Managed department BLFs Personal
yealinkT54W-door-unlock.ph.xml blf2 none blf1 blf3–blf12 blf13+
yealinkT54W-door-unlock-with-sp.ph.xml blf2 blf3–blf5 (SP1–SP3) blf1 blf6–blf15 blf16+
yealinkT58W-door-unlock.ph.xml blf2–blf4 none blf1 blf5–blf14 blf15+

The job authenticates to 3CX XAPI with the existing afterhours-shift-manager/3cx-* Secrets Manager values. Invoke door-unlock-api-blf-sync with DRY_RUN=true for a proposed-XML log and no writes. Set SMOKE_EXTENSION to PATCH a single extension. The daily EventBridge rule runs at 09:00 UTC (05:00 ET during EDT).