seahaven-door-unlock-api/lambda/unlock/unlock-handler.ts
Adam Moussa b0976f94eb Add lockdown profile toggle endpoints with T58W linekey support
Add a new Lambda handler that toggles Elements lockdown profiles
(Bohemia and Ronkonkoma) via the Elements API, with status
verification before and after each toggle. Returns Yealink XML
to control linekey LEDs (green=inactive, red=locked down).

Also brings both Lambda handlers into compliance with system
standards: Node 22.x runtime, arm64 architecture, 60-day log
retention, and kebab-case function names.
2026-04-28 18:05:38 -04:00

90 lines
3.2 KiB
TypeScript

import {
SSMClient,
GetParameterCommand,
} from "@aws-sdk/client-ssm";
const ssm = new SSMClient({});
let cachedAuthToken: string | undefined;
let cachedApiKey: string | undefined;
let cachedDoorId: string | undefined;
let lastUnlockTime = 0;
const COOLDOWN_MS = 5_000;
async function getParameter(name: string, decrypt: boolean): Promise<string> {
const res = await ssm.send(
new GetParameterCommand({ Name: name, WithDecryption: decrypt })
);
return res.Parameter!.Value!;
}
async function loadSecrets() {
const [authToken, apiKey, doorId] = await Promise.all([
cachedAuthToken ?? getParameter(process.env.AUTH_TOKEN_PARAM!, true),
cachedApiKey ?? getParameter(process.env.ELEMENTS_API_KEY_PARAM!, true),
cachedDoorId ?? getParameter(process.env.DOOR_ID_PARAM!, false),
]);
cachedAuthToken = authToken;
cachedApiKey = apiKey;
cachedDoorId = doorId;
return { authToken, apiKey, doorId };
}
export async function handler(event: {
queryStringParameters?: Record<string, string>;
requestContext?: { http?: { sourceIp?: string } };
}) {
const sourceIp = event.requestContext?.http?.sourceIp ?? "unknown";
const token = event.queryStringParameters?.token;
if (!token) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "rejected", reason: "missing_token", sourceIp }));
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
}
let secrets;
try {
secrets = await loadSecrets();
} catch (err) {
console.error(JSON.stringify({ action: "unlock_attempt", status: "error", reason: "ssm_failure", sourceIp, error: String(err) }));
return { statusCode: 500, body: JSON.stringify({ error: "Internal error" }) };
}
if (token !== secrets.authToken) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "rejected", reason: "invalid_token", sourceIp }));
return { statusCode: 403, body: JSON.stringify({ error: "Forbidden" }) };
}
const now = Date.now();
if (now - lastUnlockTime < COOLDOWN_MS) {
console.log(JSON.stringify({ action: "unlock_attempt", status: "cooldown", sourceIp }));
return { statusCode: 429, body: JSON.stringify({ message: "Cooldown active, try again shortly" }) };
}
const url = `https://api.elementssecure.com/v1/devices/${secrets.doorId}/commands/TemporaryUnlock/execute`;
try {
const response = await fetch(url, {
method: "POST",
headers: {
"api-key": secrets.apiKey,
"Content-Type": "application/json",
},
body: JSON.stringify({}),
});
if (!response.ok) {
const body = await response.text();
console.error(JSON.stringify({ action: "unlock_attempt", status: "elements_error", statusCode: response.status, body, sourceIp }));
return { statusCode: 502, body: JSON.stringify({ error: "Door system error" }) };
}
lastUnlockTime = now;
console.log(JSON.stringify({ action: "unlock_attempt", status: "success", sourceIp }));
return { statusCode: 200, body: JSON.stringify({ message: "Door unlocked" }) };
} catch (err) {
console.error(JSON.stringify({ action: "unlock_attempt", status: "error", reason: "elements_unreachable", sourceIp, error: String(err) }));
return { statusCode: 502, body: JSON.stringify({ error: "Door system unreachable" }) };
}
}