AWS Lambda middleware — Yealink desk phone DSS key unlocks front door via LenelS2 Elements API
Find a file
2026-09-28 05:43:22 +00:00
.github/workflows chore(deps): update sea-haven-industries/.github action to v1.0.19 2026-09-28 05:43:22 +00: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
firmware feat(phones): add T57W door-unlock-with-sp template (#87) 2026-09-03 22:56:04 +00:00
lambda feat(phones): add T57W door-unlock-with-sp template (#87) 2026-09-03 22:56:04 +00: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
terraform chore(deps): update terraform aws to v6.65.0 (#93) 2026-09-21 15:47:28 +00:00
.gitignore feat(terraform): add hcp terraform for prod door-unlock-api (PLAT-76) (#81) 2026-08-27 22:03:12 +00:00
.npmrc chore(ci): add npmrc to allow remote package installs (#95) 2026-09-21 15:58:41 +00:00
AGENTS.md docs(agents): drop security review gates (#97) 2026-09-26 17:18:00 -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 fix(deps): update npm minor and patch (#94) 2026-09-21 16:02:44 +00:00
package.json fix(deps): update npm minor and patch (#94) 2026-09-21 16:02:44 +00:00
README.md docs(readme): record HCP VCS trigger patterns (PLAT-183) (#90) 2026-09-10 21:00:45 +00:00
RUNBOOK-token-rotation.md feat(phones): add T57W door-unlock-with-sp template (#87) 2026-09-03 22:56:04 +00: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
yealinkT57W-door-unlock-with-sp.ph.xml feat(phones): add T57W door-unlock-with-sp template (#87) 2026-09-03 22:56:04 +00: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 Terraform CI

AWS Lambda middleware that allows Yealink desk phones to unlock the front door and manage lockdown profiles via LenelS2 Elements. Target account is seahaven-prod (011934824531) under HCP Terraform workspace seahaven-door-unlock-api-prod. The workspace working directory is terraform/. VCS file triggers use trigger-patterns = [terraform/**/*, lambda/**/*] because terraform/build_packages.sh bundles handlers from lambda/. A lambda/-only merge must still queue a run.

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

Phones keep https://doorunlock.seahaven.com. Cutover is a Route 53 A-record flip in the mgmt zone (Z06652411XKH89KTZD3XA). DNS is not managed in this Terraform.

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. API Gateway invokes the authorizer through a Lambda resource policy, not AuthorizerCredentialsArn.
  • 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 — polls the public Elements API every minute. No VPC.
  • BLF sync Lambda — daily 09:00 UTC EventBridge job that writes 3CX department BLFs
  • SSM Parameter Store — Elements API key, auth token, and door ID (created out of band; Terraform never reads SecureString values)
  • Secrets Manager — 3CX XAPI credentials (afterhours-shift-manager/3cx-*), referenced by ARN only
  • Custom Domain — doorunlock.seahaven.com via an out-of-band ACM certificate in prod. The API Gateway domain mapping is attached at DNS cutover (attach_custom_domain). Route 53 stays in mgmt. Until then, proof uses the execute-api URL.
  • CloudWatch alarms — one error alarm per Lambda (unlock, lockdown, authorizer, poller, blf-sync); each fires on Errors > 0 and notifies the prod site-alerts SNS topic (ALARM state only)

Infrastructure (HCP Terraform)

All live infrastructure is defined in terraform/ and applied from HCP Terraform workspace seahaven-door-unlock-api-prod (manual apply). The AWS provider is pinned at 6.58.0. Functions run Node 24 arm64 under path /tf-managed/ with permissions boundary seahaven-lambda-execution-boundary-seahaven-door-unlock-api.

CDK sources (bin/, lib/) remain in the repo as the mgmt rollback target until that stack is deleted. GitHub Actions CDK deploy is frozen (.github/workflows/deploy.yaml.frozen).

Layout

terraform/                    # HCP Terraform (working directory)
lambda/
├── unlock/unlock-handler.ts
├── lockdown/lockdown-handler.ts
├── poller/lockdown-poller.ts
├── authorizer/authorizer-handler.ts
└── blf-sync/blf-sync-handler.ts

HCP plan workers may not have Node. terraform/build_packages_external.sh bootstraps Node 24 if needed, then esbuild-bundles the five handlers during plan. Packages upload through aws_s3_object.content_base64 because plan and apply run on different workers.

EventBridge schedules are created disabled (enable_schedules = false) until live-path proof and DNS cutover.

Do not put secret values in Terraform, *.tfvars, chat, or PRs. Create SSM and Secrets Manager objects out of band; Terraform uses names and ARNs only.

Documentation

The canonical map of Sea Haven's AWS infrastructure lives in Confluence.

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

Phone LED/Push XML is not in the live path. /seahaven/door-unlock/phone-ips and door-unlock-api/phone-password are not used by this Terraform.

Secrets Manager

Secret Description
afterhours-shift-manager/3cx-domain 3CX XAPI hostname
afterhours-shift-manager/3cx-client-id 3CX XAPI client id
afterhours-shift-manager/3cx-client-secret 3CX XAPI client secret

CI/CD

  • .github/workflows/ci.yaml — on pull requests to main, runs TypeScript tests. CDK synth is off.
  • .github/workflows/ci-terraform.yaml — on pull requests that touch terraform/, runs terraform fmt, init -backend=false, and validate.
  • HCP Terraform — workspace seahaven-door-unlock-api-prod in project seahaven-prod. Working directory terraform/. trigger-patterns = [terraform/**/*, lambda/**/*]. Manual apply. Auto-apply stays off until the stack is sealed.

Do not run cdk deploy against prod. The GitHub CDK deploy workflow is frozen.

Phone Configuration

Configure DSS keys on the Yealink T54W/T57W/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
yealinkT57W-door-unlock-with-sp.ph.xml T57W 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 and T57W+SP templates 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+
yealinkT57W-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).