mirror of
https://github.com/Sea-Haven-Industries/seahaven-door-unlock-api.git
synced 2026-09-30 03:43:11 +00:00
237 lines
12 KiB
Markdown
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` |
|