seahaven-door-unlock-api/door-unlock-plan.md

237 lines
12 KiB
Markdown

# 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.com` using 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.
1. Log into the Yealink web interface at `http://<phone-ip>`
2. Navigate to **DSSKey → Line Key** (or **Programmable Key**)
3. Configure an available key:
- **Type:** URL
- **Label:** Unlock Door
- **Value:** `https://doorunlock.seahaven.com/unlock?token=<shared-secret>`
4. 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:
1. **Enable the Elements API** — Go to the Elements Marketplace and request/enable API access if not already done
2. **Generate an API key** — From the Marketplace, create an API key. This key will be passed as an `api-key` header in requests to `api.elementssecure.com`
3. **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.
4. **Review the API docs** — Go to `https://api.elementssecure.com/docs` and look for:
- A door unlock or device command endpoint (likely under a `/doors` or `/devices` or `/commands` path)
- The exact HTTP method, URL path, and request body format for a momentary unlock
- Note the exact endpoint path and required parameters
5. **Test the API manually** — Use Postman or curl to confirm you can unlock the door via the API before building the Lambda. Example:
```
curl -X POST https://api.elementssecure.com/v1/doors/<door-id>/unlock \
-H "api-key: <your-api-key>" \
-H "Content-Type: application/json"
```
(The exact path/method will come from the API docs — this is just an example)
### From AWS (should mostly be in place already):
6. **Confirm the seahaven.com Route 53 hosted zone ID** — for custom domain setup
7. **Confirm the wildcard ACM certificate ARN** — `*.seahaven.com` cert in us-east-1
8. **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
1. **Store the secrets in SSM:**
```bash
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
```
2. **Test the endpoint:**
```bash
# 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>
```
3. **Configure the Yealink T58A** (see section above)
4. **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.requestId` not `$context.path` in 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` |