12 KiB
Sea Haven — Desk Phone Door Unlock via LenelS2 Elements
Project Plan for Claude Code
Overview
Build a lightweight AWS middleware that allows a Yealink T58A desk phone to unlock the front door controlled by LenelS2 Elements. The user presses a dedicated button (DSS key) on the Yealink, which fires an HTTPS GET request to an API Gateway endpoint. A Lambda function receives the request, authenticates to the LenelS2 Elements API, and sends a door unlock command.
Flow: Yealink T58A (DSS key press) → HTTPS GET → API Gateway → Lambda → Elements API → Door Unlocks
Architecture
┌──────────────┐ HTTPS GET ┌──────────────────┐ HTTPS POST ┌─────────────────────┐
│ Yealink T58A │ ───────────────► │ API Gateway │ ──────────────► │ LenelS2 Elements │
│ DSS Key: │ ?token=xxxxx │ + Lambda │ api-key header │ api.elementssecure │
│ "Unlock Door"│ │ (us-east-1) │ │ .com │
└──────────────┘ └──────────────────┘ └─────────────────────┘
│
▼
┌──────────────┐
│ Secrets Mgr │
│ or SSM Param │
│ (API keys) │
└──────────────┘
Components to Build
1. AWS CDK Stack (TypeScript — matches existing LedgerFlow stack)
Stack name: SeaHavenDoorUnlockStack
Region: us-east-1 (matches existing infra)
Resources:
- API Gateway (HTTP API) — a single GET endpoint at
/unlock - Lambda function (Node.js 20.x) — handles the request
- SSM Parameter Store or Secrets Manager — stores the Elements API key and the shared bearer token for Yealink auth
- Optional: Route 53 custom domain — e.g.,
doorunlock.seahaven.comusing the existing wildcard ACM cert and hosted zone
API Gateway Configuration:
- HTTP API (not REST API — simpler, cheaper)
- Single route:
GET /unlock - No CORS needed (Yealink sends a plain GET, not a browser request)
- Throttling: 5 requests/second burst, 2 requests/second sustained (matches Elements rate limit of 2 req/sec)
2. Lambda Function Logic
// Pseudocode for the Lambda handler
1. Extract query parameter `token` from the request
2. Validate `token` against stored shared secret (from SSM/Secrets Manager)
- If invalid → return 403
3. Retrieve Elements API key from SSM/Secrets Manager
4. Identify which door/reader to unlock (hardcoded or passed as param)
5. Call the Elements API to execute the door unlock command
- Endpoint: https://api.elementssecure.com/...
- Header: api-key: <elements-api-key>
- Method: POST (or whatever the Elements API docs specify for door commands)
6. Return 200 with a simple success message (or error details)
Key Design Decisions:
- Auth from the Yealink: Use a simple shared secret as a query parameter (
?token=<long-random-string>). The Yealink DSS URL key supports specifying a full URL including query params. HTTPS ensures the token is encrypted in transit. - Idempotency: The door unlock is inherently idempotent (unlocking an already-unlocking door is harmless), so no special handling needed.
- Logging: Log every unlock attempt to CloudWatch with timestamp, source IP, and success/failure for audit trail.
- Rate limiting: API Gateway throttling prevents abuse. Optionally add a cooldown (e.g., ignore requests within 5 seconds of the last successful unlock).
3. Yealink T58A Configuration
This is manual config done in the Yealink web UI — not code. Documenting here for completeness.
- Log into the Yealink web interface at
http://<phone-ip> - Navigate to DSSKey → Line Key (or Programmable Key)
- Configure an available key:
- Type: URL
- Label: Unlock Door
- Value:
https://doorunlock.seahaven.com/unlock?token=<shared-secret>
- Click Confirm to save
Security note: Under Features → General Information, the Yealink has an "Action URI" trusted IP list. This is for incoming action URIs (remote control of the phone). It's not relevant here since the phone is the one initiating the HTTP GET. No additional Yealink security config needed for outbound requests.
Prerequisites — Adam Needs to Do Before Claude Code
From LenelS2 Elements:
- Enable the Elements API — Go to the Elements Marketplace and request/enable API access if not already done
- Generate an API key — From the Marketplace, create an API key. This key will be passed as an
api-keyheader in requests toapi.elementssecure.com - Identify the door/reader ID — In the Elements portal, find the device ID for the front door reader that needs to be unlocked. You'll need this for the API call.
- Review the API docs — Go to
https://api.elementssecure.com/docsand look for:- A door unlock or device command endpoint (likely under a
/doorsor/devicesor/commandspath) - The exact HTTP method, URL path, and request body format for a momentary unlock
- Note the exact endpoint path and required parameters
- A door unlock or device command endpoint (likely under a
- Test the API manually — Use Postman or curl to confirm you can unlock the door via the API before building the Lambda. Example:
(The exact path/method will come from the API docs — this is just an example)curl -X POST https://api.elementssecure.com/v1/doors/<door-id>/unlock \ -H "api-key: <your-api-key>" \ -H "Content-Type: application/json"
From AWS (should mostly be in place already):
- Confirm the seahaven.com Route 53 hosted zone ID — for custom domain setup
- Confirm the wildcard ACM certificate ARN —
*.seahaven.comcert in us-east-1 - Generate a shared secret token — a long random string (32+ characters) that will be embedded in the Yealink URL and validated by the Lambda. Generate with:
openssl rand -hex 32
Information to Provide to Claude Code
When starting the Claude Code session, provide the following:
Project: Sea Haven Door Unlock Middleware
Stack: AWS CDK (TypeScript), Lambda (Node.js 20.x)
Region: us-east-1
Build an AWS CDK stack called SeaHavenDoorUnlockStack that creates:
1. An HTTP API Gateway with a single GET /unlock route
2. A Lambda function (Node.js 20.x) that:
- Validates a `token` query parameter against a secret stored in SSM Parameter Store
- On valid token, calls the LenelS2 Elements API to unlock the front door
- Elements API base URL: https://api.elementssecure.com
- Elements API auth: pass `api-key` header with the stored API key
- Elements API endpoint for door unlock: [FILL IN from API docs]
- Door/reader ID: [FILL IN from Elements portal]
- Logs all attempts to CloudWatch
- Returns 200 on success, 403 on bad token, 502 on Elements API failure
3. SSM Parameter Store parameters:
- /seahaven/door-unlock/elements-api-key (SecureString)
- /seahaven/door-unlock/auth-token (SecureString)
- /seahaven/door-unlock/door-id (String)
4. Custom domain: doorunlock.seahaven.com
- Route 53 hosted zone: [FILL IN hosted zone ID]
- ACM certificate: [FILL IN wildcard cert ARN]
5. API Gateway throttling: 5 burst, 2 sustained requests/sec
After deployment, the Yealink phone will call:
GET https://doorunlock.seahaven.com/unlock?token=<auth-token-value>
Additional requirements:
- Add a 5-second cooldown between successful unlocks (store last unlock time in Lambda global scope or a simple DynamoDB TTL item)
- Include proper error handling and structured JSON logging
- No external npm dependencies beyond AWS SDK v3 (bundled with Lambda runtime)
Post-Deployment Steps
-
Store the secrets in SSM:
aws ssm put-parameter --name "/seahaven/door-unlock/elements-api-key" \ --value "<your-elements-api-key>" --type SecureString --region us-east-1 aws ssm put-parameter --name "/seahaven/door-unlock/auth-token" \ --value "<your-generated-shared-secret>" --type SecureString --region us-east-1 aws ssm put-parameter --name "/seahaven/door-unlock/door-id" \ --value "<your-elements-door-id>" --type String --region us-east-1 -
Test the endpoint:
# Should return 403 curl https://doorunlock.seahaven.com/unlock?token=wrong-token # Should unlock the door curl https://doorunlock.seahaven.com/unlock?token=<correct-token> -
Configure the Yealink T58A (see section above)
-
Test end-to-end: Press the DSS key on the Yealink and verify the door unlocks
Security Considerations
- HTTPS everywhere — Yealink → API Gateway is TLS, Lambda → Elements API is TLS
- Shared secret in URL — acceptable because HTTPS encrypts the full URL including query parameters. The token is never logged by API Gateway (use
$context.requestIdnot$context.pathin access logs). Only the Lambda sees the token for validation. - Rate limiting — API Gateway throttle + Lambda cooldown prevent brute force and accidental double-taps
- IP restriction (optional enhancement) — if the Yealink phones have static IPs on the LAN and the office has a static public IP, you can add an API Gateway resource policy to restrict access to that IP only. This makes the shared token a second factor rather than the only factor.
- Audit trail — every unlock attempt (success or failure) is logged to CloudWatch with timestamp and source IP
- API key rotation — the Elements API key and shared token are in SSM Parameter Store, so rotation requires updating the SSM value and (for the shared token) the Yealink DSS key URL
Estimated AWS Cost
- API Gateway HTTP API: $1.00 per million requests (negligible for door unlocks)
- Lambda: free tier covers 1M requests/month (you'll use maybe 100)
- SSM Parameter Store: free for standard parameters, $0.05/parameter/month for SecureString via advanced
- Route 53: already have the hosted zone ($0.50/month existing)
- Total incremental cost: effectively $0/month
Files Claude Code Should Create
seahaven-door-unlock/
├── bin/
│ └── app.ts # CDK app entry point
├── lib/
│ └── door-unlock-stack.ts # CDK stack definition
├── lambda/
│ └── unlock-handler.ts # Lambda function code
├── cdk.json
├── tsconfig.json
├── package.json
└── README.md
Open Items (Requires Your Input)
| Item | What's Needed | Where to Find It |
|---|---|---|
| Elements API door unlock endpoint | Exact path, method, and body for unlocking a door | https://api.elementssecure.com/docs |
| Elements door/reader ID | The device ID for your front door | Elements portal → Devices |
| Elements API key | Your API key for auth | Elements Marketplace |
| Route 53 hosted zone ID | Your seahaven.com zone | AWS Console → Route 53 |
| ACM wildcard cert ARN | Your *.seahaven.com cert | AWS Console → ACM (us-east-1) |
| Shared secret token | Generate a random token | Run: openssl rand -hex 32 |