Compare commits

...

7 Commits

Author SHA1 Message Date
2f45005a8a cleanup old code
All checks were successful
Build and push image for doorman-homeassistant / docker (push) Successful in 29s
Build and push Doorman UI / API / docker (push) Successful in 2m2s
Build and push image for doorman-homeassistant / deploy-gitainer (push) Successful in 5s
2025-05-31 21:50:31 -07:00
aeefd6f5d3 change homeassistant container to use new ddb 2025-05-31 21:47:52 -07:00
ca2cd5286f migrate status route 2025-05-31 15:17:44 -07:00
154ca3aec0 migrate edit route 2025-05-31 15:09:39 -07:00
50c4e9ae2c migrate auth route 2025-05-31 14:47:43 -07:00
ff2b5f9b9e migrate rest of info route to dynabridge 2025-05-31 14:36:19 -07:00
9f1a05acf8 migrate door alias 2025-05-31 14:26:47 -07:00
13 changed files with 203 additions and 337 deletions

BIN
bun.lockb

Binary file not shown.

View File

@ -5,14 +5,14 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { UserAgentHeader } from "../../../utils/blockUserAgent";
import { clearLockStatusCommand, createDDBClient, ddbItemToJSON, getDoorConfigCommand, getLockStatusCommand, isLockOpen, setLockStatusCommand } from "../../../utils/ddb";
import { DoorConfig } from "../../../types/DoorConfig";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { AuthMethod } from "../../../types/AuthMethod";
import { Lock } from "../../../types/Lock";
import { DoorStatus } from "../../../types/DoorStatus";
import { getMetricFromRegistry, withMetrics } from "../../../common/DoormanHandler";
import { AuthMetrics, registerMetrics } from "../../../metrics/AuthMetrics";
import { Counter } from "prom-client";
import { DoorConfig, getDoorConfigID } from "../../../schema/DoorConfig";
import { createLockStatusWithTimeout, getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
export interface AuthRequest extends ServerlessEventObject<{}, UserAgentHeader> {
door?: string;
@ -34,10 +34,9 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
return callback(null, response);
}
const client = createDDBClient(context);
const db = createDynaBridgeClient(context);
const ddbConfig = await client.send(getDoorConfigCommand(door));
const config: DoorConfig = ddbItemToJSON<DoorConfig>(ddbConfig);
const config: DoorConfig | undefined = await db.entities.doorConfig.findById(getDoorConfigID(door));
if (!config) {
getMetricFromRegistry<Counter>(metricsRegistry, AuthMetrics.DOOR_CONFIG_NOT_FOUND).inc({ door }, 1);
@ -76,41 +75,32 @@ export const handler: ServerlessFunctionSignature<TwilioContext, AuthRequest> =
const timeout = event.timeout ? parseInt(event.timeout) : config.timeout;
// check lock status if locked, then unlock. If unlocked then lock
await client.send(getLockStatusCommand(door))
.then(async (lockDdb) => {
const isOpen = isLockOpen(lockDdb);
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const isOpen = isLockOpen(lock);
if (isOpen) {
const lock: Lock = ddbItemToJSON<Lock>(lockDdb);
const fingerprint = JSON.parse(lock.fingerprint);
if (isOpen && lock) {
const fingerprint = JSON.parse(lock.fingerprint);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
fingerprint,
});
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
fingerprint,
});
await client.send(clearLockStatusCommand(lockDdb));
return;
}
await db.entities.lockStatus.deleteById(getLockStatusID(door));
} else {
await db.entities.lockStatus.save(createLockStatusWithTimeout(door, timeout, fingerprint));
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
}
await client.send(setLockStatusCommand(door, timeout, fingerprint))
.then(async (item) => {
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: `Opened the door "${door}" for ${timeout}s` });
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
});
});
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
await client.destroy();
return callback(null, response);
});

View File

@ -6,9 +6,9 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest, UserAgentHeader } from "../../../utils/blockUserAgent";
import { createDDBClient, ddbItemToJSON, getDoorConfigCommand, getDoorConfigUpdateCommand, putDoorUpdateConfigCommand, replaceDoorConfigWithUpdateItem } from "../../../utils/ddb";
import { DoorConfig, EditDoorConfig } from "../../../types/DoorConfig";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { sendMessageToUser } from "../../../utils/discord";
import { createEditDoorConfig, EditDoorConfigReq, editDoorToDoorConfig, getDoorConfigID, getEditDoorConfigID } from "../../../schema/DoorConfig";
export interface EditRequest extends ServerlessEventObject<{}, UserAgentHeader> {
door?: string;
@ -28,19 +28,18 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
let approvalId = event.approvalId;
let newConfigString = event.newConfig;
const client = createDDBClient(context);
const db = createDynaBridgeClient(context);
// approve path
if (door && approvalId) {
const newConfigDdb = await client.send(getDoorConfigUpdateCommand(door));
const newConfig = ddbItemToJSON<EditDoorConfig>(newConfigDdb);
const newConfig = await db.entities.editDoorConfig.findById(getEditDoorConfigID(door));
if (!newConfig || newConfig.approvalId !== approvalId) {
response.setStatusCode(400);
return callback(null, response);
}
await client.send(replaceDoorConfigWithUpdateItem(newConfigDdb as any));
db.entities.doorConfig.save(editDoorToDoorConfig(newConfig));
// send update to discord users
const updateMessage = `Configuration change \`${approvalId}\` was approved @ Door "${door}"`;
@ -64,10 +63,8 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
return callback(null, response);
}
const newConfig: EditDoorConfig = JSON.parse(newConfigString);
const configDdb = await client.send(getDoorConfigCommand(door));
const config = ddbItemToJSON<DoorConfig>(configDdb);
const newConfig: EditDoorConfigReq = JSON.parse(newConfigString);
const config = await db.entities.doorConfig.findById(getDoorConfigID(door));
if (!config) {
response.setStatusCode(404);
@ -79,15 +76,14 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
newConfig.pin = config.pin;
}
const input = putDoorUpdateConfigCommand(door, newConfig);
const update = await client.send(input);
const editDoorConfig = createEditDoorConfig(door, newConfig);
await db.entities.editDoorConfig.save(editDoorConfig);
// newConfig.discordUsers = undefined;
// newConfig.fallbackNumbers = undefined;
// newConfig.status = undefined;
const approvalUrl = `https://doorman.chromart.cc/api/door/edit?door=${door}&approvalId=${input?.input?.Item?.approvalId.S as string}`;
const approvalUrl = `https://doorman.chromart.cc/api/door/edit?door=${door}&approvalId=${editDoorConfig.approvalId as string}`;
console.log(approvalUrl);
// send update to discord users
@ -106,8 +102,11 @@ export const handler: ServerlessFunctionSignature<TwilioContext, EditRequest> =
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({ msg: update });
.setBody({ msg: "Created Configuration change" });
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
await client.destroy();
return callback(null, response);
};

View File

@ -4,10 +4,11 @@
import { ServerlessEventObject, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { createDDBClient, createDynaBridgeClient, ddbItemToJSON, getDoorAliasCommand, getDoorConfigCommand, getLockStatusCommand, isLockOpen } from "../../../utils/ddb";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { DoorStatus } from "../../../types/DoorStatus";
import { DoorConfig } from "../../../types/DoorConfig";
import { DOOR_CONFIG_SK, getDoorConfigID } from "../../../schema/DoorConfig";
import { getDoorConfigID } from "../../../schema/DoorConfig";
import { getDoorAliasID } from "../../../schema/DoorAlias";
import { getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
export interface InfoRequest extends ServerlessEventObject {
door?: string;
@ -38,20 +39,19 @@ export const handler: ServerlessFunctionSignature<TwilioContext, InfoRequest> =
return callback(null, response);
}
const client = createDDBClient(context);
const db = createDynaBridgeClient(context);
if (buzzer) {
door = await client.send(getDoorAliasCommand(buzzer))
door = await db.entities.doorAlias.findById(getDoorAliasID(buzzer))
.then(async (alias) => {
if (!alias.Item) {
if (!alias) {
response
.setStatusCode(404)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: "This buzzer is not registered" });
return undefined;
}
return alias.Item.name.S;
return alias.name;
});
}
@ -74,39 +74,34 @@ export const handler: ServerlessFunctionSignature<TwilioContext, InfoRequest> =
door,
fallbackNumbers: config.fallbackNumbers,
pressKey: config.pressKey,
discordUsers: config.discordUsers || [],
discordUsers: config.discordUsers,
});
} else {
await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const status = isLockOpen(lock) ? DoorStatus.OPEN: DoorStatus.CLOSED;
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const status = isLockOpen(lock) ? DoorStatus.OPEN: DoorStatus.CLOSED;
// respond to UI
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
id: door,
timeout: config.timeout,
buzzer: config.buzzer,
status,
buzzerCode: config.buzzerCode,
fallbackNumbers: config.fallbackNumbers,
pressKey: config.pressKey,
discordUsers: config.discordUsers || [],
greeting: config.greeting || "",
});
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
// respond to UI
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
id: door,
timeout: config.timeout,
buzzer: config.buzzer,
status,
buzzerCode: config.buzzerCode,
fallbackNumbers: config.fallbackNumbers,
pressKey: config.pressKey,
discordUsers: config.discordUsers,
greeting: config.greeting || "",
});
}
}
}
await client.destroy();
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
return callback(null, response);
};

View File

@ -5,8 +5,9 @@
import { ServerlessFunctionSignature, ServerlessEventObject } from "@twilio-labs/serverless-runtime-types/types";
import { TwilioContext } from "../../../types/TwilioContext";
import { shouldBlockRequest } from "../../../utils/blockUserAgent";
import { clearLockStatusCommand, createDDBClient, getLockStatusCommand, isLockOpen } from "../../../utils/ddb";
import { createDynaBridgeClient } from "../../../utils/ddb";
import { DoorStatus } from "../../../types/DoorStatus";
import { getLockStatusID, isLockOpen } from "../../../schema/LockStatus";
export interface StatusRequest extends ServerlessEventObject {
door: string;
@ -32,42 +33,34 @@ export const handler: ServerlessFunctionSignature<TwilioContext, StatusRequest>
return callback(null, response);
}
const client = createDDBClient(context);
const db = createDynaBridgeClient(context);
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const isOpen = isLockOpen(lock);
const isOpen = isLockOpen(lock);
if (isOpen) {
const fingerprint = JSON.parse(lock?.Item?.fingerprint.S as string);
if (isOpen && lock) {
const fingerprint = JSON.parse(lock.fingerprint);
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.OPEN,
fingerprint,
});
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.OPEN,
fingerprint,
});
await client.send(clearLockStatusCommand(lock));
return;
}
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
});
}).catch((e) => {
console.log(e);
response
.setStatusCode(500)
.appendHeader('Content-Type', 'application/json')
.setBody({ err: e });
await db.entities.lockStatus.deleteById(getLockStatusID(door));
} else {
response
.setStatusCode(200)
.appendHeader('Content-Type', 'application/json')
.setBody({
status: DoorStatus.CLOSED,
});
}
await client.destroy();
// destroy the internal client after
// @ts-ignore
db.ddbClient.destroy();
return callback(null, response);
};

View File

@ -0,0 +1,22 @@
import { z } from "zod";
import { DynaBridgeEntity } from 'dynabridge';
export const DOOR_ALIAS_SK = "alias";
export const DoorAliasSchema = z.object({
// keys
PK: z.string(), // phone number (buzzer number)
SK: z.literal(DOOR_ALIAS_SK).default(DOOR_ALIAS_SK),
name: z.string(),
});
export const getDoorAliasID = (buzzer: string): string[] => {
return [buzzer, DOOR_ALIAS_SK];
}
export type DoorAlias = z.infer<typeof DoorAliasSchema>;
export const DoorAliasEntity: DynaBridgeEntity<DoorAlias> = {
tableName: "doorman",
id: ["PK", "SK"],
};

View File

@ -1,5 +1,6 @@
import { z } from "zod";
import { DynaBridge, DynaBridgeEntity } from 'dynabridge';
import { DynaBridgeEntity } from 'dynabridge';
import { randomUUID } from "crypto";
export const DOOR_CONFIG_SK = "config";
export const EDIT_DOOR_CONFIG_SK = "config-update";
@ -15,7 +16,7 @@ export const DoorConfigSchema = z.object({
fallbackNumbers: z.array(z.string()),
pin: z.string(),
pressKey: z.string(),
greeting: z.string(),
greeting: z.string().optional(),
timeout: z.number(),
});
@ -36,6 +37,8 @@ export const getEditDoorConfigID = (doorName: string): string[] => {
export type DoorConfig = z.infer<typeof DoorConfigSchema>;
export type EditDoorConfig = z.infer<typeof EditDoorConfigSchema>;
export type EditDoorConfigReq = Omit<EditDoorConfig, "PK" | "SK" | "approvalId">;
export const DoorConfigEntity: DynaBridgeEntity<DoorConfig> = {
tableName: "doorman",
id: ["PK", "SK"],
@ -45,3 +48,23 @@ export const EditDoorConfigEntity: DynaBridgeEntity<EditDoorConfig> = {
tableName: "doorman",
id: ["PK", "SK"],
};
export const editDoorToDoorConfig = (updateDoor: EditDoorConfig): DoorConfig => {
const newItem: any = {
...updateDoor,
SK: DOOR_CONFIG_SK,
};
delete newItem.approvalId;
return newItem;
}
export const createEditDoorConfig = (door: string, newConfig: EditDoorConfigReq): EditDoorConfig => {
return {
...newConfig,
PK: "door-" + door,
SK: EDIT_DOOR_CONFIG_SK,
approvalId: randomUUID().toString(),
};
}

View File

@ -0,0 +1,40 @@
import { z } from "zod";
import { DynaBridgeEntity } from 'dynabridge';
export const LOCK_STATUS_SK = "lock";
export const LockStatusSchema = z.object({
// keys
PK: z.string(), // door name
SK: z.literal(LOCK_STATUS_SK).default(LOCK_STATUS_SK),
TTL: z.number(),
fingerprint: z.string(),
});
export const getLockStatusID = (door: string): string[] => {
return [door, LOCK_STATUS_SK];
}
export type LockStatus = z.infer<typeof LockStatusSchema>;
export const LockStatusEntity: DynaBridgeEntity<LockStatus> = {
tableName: "doorman",
id: ["PK", "SK"],
};
export const isLockOpen = (lock?: LockStatus) => {
// ttl is a UTC ms time for how long it is unlocked
const ttl = lock?.TTL || 0;
return parseInt("" + ttl) > Date.now();
};
export const createLockStatusWithTimeout = (door: string, timeoutSeconds: number, fingerprint: any): LockStatus => {
return {
PK: door,
SK: LOCK_STATUS_SK,
TTL: Date.now() + timeoutSeconds * 1000,
fingerprint: JSON.stringify(fingerprint),
};
};

View File

@ -1,14 +0,0 @@
export interface DoorConfig {
buzzer: string;
buzzerCode: string;
discordUsers: string[];
fallbackNumbers: string[];
pin: string;
pressKey: string;
timeout: number;
greeting: string;
}
export interface EditDoorConfig extends DoorConfig {
approvalId: string;
}

View File

@ -1,3 +0,0 @@
export interface Lock {
fingerprint: any;
}

View File

@ -1,25 +1,16 @@
import { randomUUID } from "crypto";
import { DynamoDBClient, GetItemCommand, DeleteItemCommand, PutItemCommand, UpdateItemCommand, GetItemOutput } from "@aws-sdk/client-dynamodb";
import { TwilioContext } from "../types/TwilioContext";
import { DoorConfig } from "../types/DoorConfig";
import { DynaBridge } from "dynabridge";
import { DoorConfigEntity, EditDoorConfigEntity } from "../schema/DoorConfig";
export const createDDBClient = (context: TwilioContext) => {
return new DynamoDBClient({
region: "us-east-1" ,
credentials: {
accessKeyId: context.AWS_ACCESS_KEY,
secretAccessKey: context.AWS_SECRET_ACCESS_KEY,
},
});
};
import { DoorAliasEntity } from "../schema/DoorAlias";
import { LockStatusEntity } from "../schema/LockStatus";
export const createDynaBridgeClient = (context: TwilioContext) => {
// register all entities here
return new DynaBridge({
doorConfig: DoorConfigEntity,
editDoorConfig: EditDoorConfigEntity,
doorAlias: DoorAliasEntity,
lockStatus: LockStatusEntity,
}, {
serialize: (entity) => entity,
deserialize: (entity) => {
@ -42,170 +33,3 @@ export const createDynaBridgeClient = (context: TwilioContext) => {
},
});
};
export const getLockStatusCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: door,
},
"SK": {
S: "lock",
}
},
});
};
export const getDoorAliasCommand = (buzzerNumber: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: buzzerNumber,
},
"SK": {
S: "alias",
}
},
});
};
export const getDoorConfigCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: `door-${door}`,
},
"SK": {
S: "config",
},
},
});
};
export const isLockOpen = (lock: GetItemOutput) => {
// ttl is a UTC ms time for how long it is unlocked
const ttl = lock.Item?.TTL?.N || 0;
return parseInt("" + ttl) > Date.now();
};
export const clearLockStatusCommand = (lock: GetItemOutput) => {
return new DeleteItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: lock?.Item?.PK.S as string,
},
"SK": {
S: "lock",
}
}
});
};
export const setLockStatusCommand = (door: string, timeoutSeconds: number, fingerprintObj: any) => {
return new PutItemCommand({
TableName: "doorman",
Item: {
"PK": {
S: door,
},
"SK": {
S: "lock",
},
"TTL": {
N: `${Date.now() + timeoutSeconds * 1000}`,
},
"fingerprint": {
S: JSON.stringify(fingerprintObj),
}
}
});
};
export const putDoorUpdateConfigCommand = (door: string, config: DoorConfig) => {
return new PutItemCommand({
TableName: "doorman",
Item: {
"PK": {
S: "door-" + door,
},
"SK": {
S: "config-update",
},
"buzzer": {
S: config.buzzer,
},
"buzzerCode": {
S: config.buzzerCode,
},
"discordUsers": {
SS: config.discordUsers,
},
"fallbackNumbers": {
SS: config.fallbackNumbers,
},
"pin": {
S: config.pin,
},
"pressKey": {
S: config.pressKey,
},
"timeout": {
N: `${config.timeout}`,
},
"greeting": {
S: config.greeting,
},
"approvalId": {
S: randomUUID().toString(),
}
}
});
};
export function ddbItemToJSON<T extends Record<string, any>>(ddbItem: GetItemOutput): T {
let obj: any = {};
if (!ddbItem.Item) {
return obj;
}
const Item = ddbItem.Item;
Object.keys(Item).forEach(key => {
obj[key] = Object.values(Item[key])[0];
});
return obj;
};
export const getDoorConfigUpdateCommand = (door: string) => {
return new GetItemCommand({
TableName: "doorman",
Key: {
"PK": {
S: `door-${door}`,
},
"SK": {
S: "config-update",
},
},
});
};
export const replaceDoorConfigWithUpdateItem = (newConfigItem: GetItemOutput & { Item: { approvalId?: { S: string } }}) => {
const newItem = {
...newConfigItem.Item,
SK: { S: "config" },
};
delete newItem.approvalId;
return new PutItemCommand({
TableName: "doorman",
Item: newItem,
});
};

View File

@ -15,7 +15,7 @@
"@twilio/runtime-handler": "1.3.0",
"node-fetch": "^2.7.0",
"prom-client": "^15.1.3",
"prometheus-remote-write": "^0.5.0",
"prometheus-remote-write": "^0.5.1",
"promise.timeout": "^1.2.0",
"twilio": "^3.84.1",
"winston": "^3.17.0",
@ -23,7 +23,7 @@
},
"devDependencies": {
"@types/bun": "latest",
"concurrently": "^9.1.0",
"concurrently": "^9.1.2",
"twilio-run": "^3.5.4"
},
"engines": {

View File

@ -1,8 +1,10 @@
import { serve } from "bun";
import { Hono } from "hono";
import { prettyJSON } from "hono/pretty-json";
import { createDDBClient, getDoorConfigCommand, getLockStatusCommand, isLockOpen } from "../../doorman-api/src/utils/ddb";
import { createDynaBridgeClient } from "../../doorman-api/src/utils/ddb";
import { DoorStatus } from "../../doorman-api/src/types/DoorStatus";
import { getLockStatusID, isLockOpen } from "../../doorman-api/src/schema/LockStatus";
import { getDoorConfigID } from "../../doorman-api/src/schema/DoorConfig";
const app = new Hono();
@ -15,28 +17,23 @@ app.get("/api/door/info", async (c) => {
err: "Must specify a door",
}, 400);
}
const db = createDynaBridgeClient(Bun.env as any);
const client = createDDBClient(Bun.env as any);
const config = await client.send(getDoorConfigCommand(door));
const config = await db.entities.doorConfig.findById(getDoorConfigID(door));
if (!config.Item) {
if (!config) {
return c.json({
err: "This buzzer is not registered properly",
}, 404);
}
return await client.send(getLockStatusCommand(door))
.then(async (lock) => {
const status = isLockOpen(lock) ? DoorStatus.OPEN: "CLOSED";
return c.json({
id: door,
status,
});
}).catch((e) => {
console.log(e);
return c.json({
err: e,
}, 500);
});
const lock = await db.entities.lockStatus.findById(getLockStatusID(door));
const status = isLockOpen(lock) ? DoorStatus.OPEN: "CLOSED";
return c.json({
id: door,
status,
});
});
serve({