From 25d38f2cc42fc74f470742c7b3719e0937be6fda Mon Sep 17 00:00:00 2001 From: aln730 Date: Sun, 28 Jun 2026 13:20:59 -0400 Subject: [PATCH 01/13] feat: add query by date range --- routes/logs.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/routes/logs.js b/routes/logs.js index 326e312..f25429d 100644 --- a/routes/logs.js +++ b/routes/logs.js @@ -2,17 +2,23 @@ import { Router } from "express"; const router = Router(); router.get("/", async (req, res) => { - const cursor = req.query.cursor; - const query = cursor ? { timestamp: { $lt: new Date(cursor) } } : {}; + const { cursor, since, until } = req.query; + const query = {}; + if (since || until || cursor) { + query.timestamp = {}; + if (cursor) query.timestamp.$lt = new Date(cursor); + if (since) query.timestamp.$gte = new Date(since); + if (until) query.timestamp.$lte = new Date(until); + } const logs = await req.ctx.db .collection("accessLogs") .find(query) .sort({ timestamp: -1 }) - .limit(100) + .limit(50) .toArray(); - const nextCursor = logs.length > 0 + const nextCursor = logs.length === 50 ? logs[logs.length - 1].timestamp.toISOString() : null; From 1cb97220ad4c6a2ba88b7b1729505a8f0022046b Mon Sep 17 00:00:00 2001 From: aln730 Date: Wed, 8 Jul 2026 20:22:00 -0400 Subject: [PATCH 02/13] feat: keys by user, and more --- routes/keys.js | 216 ++++++++++++++++++++------------------- routes/memberProjects.js | 1 + server.js | 2 +- 3 files changed, 114 insertions(+), 105 deletions(-) diff --git a/routes/keys.js b/routes/keys.js index 886d287..8b1e57d 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -1,123 +1,131 @@ -import { Router } from "express"; -import crypto from "crypto"; -import { REALM_NAMES } from "../constants.js"; + import { Router } from "express"; + import crypto from "crypto"; + import { REALM_NAMES } from "../constants.js"; -const router = Router(); + const router = Router(); -// First, PUT /keys with details of user key is for -// Receive a keyId back which is our association -// Register key using association and send back the now-randomised UID -// with PATCH /keys/:id + router.get("/by-user", async (req, res) => { + if (typeof req.query.userId != "string") { + return res.status(422).json({ message: "Missing 'userId' query param" }); + } + const keys = await req.ctx.db.collection("keys").find({ userId: req.query.userId }).toArray(); + res.json(keys); + }); -router.put("/", async (req, res) => { - console.log(req.body); - if (typeof req.body.userId != "string") { - res.status(422).json({ - message: "No 'userId' field specified", - }); - return; - } - if (typeof req.body.uid != "string") { - res.status(422).json({ - message: "No 'uid' field specified", - }); - return; - } + // First, PUT /keys with details of user key is for + // Receive a keyId back which is our association + // Register key using association and send back the now-randomised UID + // with PATCH /keys/:id - const keys = {}; - for (const name of REALM_NAMES) { - keys[name + "Id"] = crypto.randomBytes(18).toString("hex"); - } + router.put("/", async (req, res) => { + console.log(req.body); + if (typeof req.body.userId != "string") { + res.status(422).json({ + message: "No 'userId' field specified", + }); + return; + } + if (typeof req.body.uid != "string") { + res.status(422).json({ + message: "No 'uid' field specified", + }); + return; + } - const insertedKey = await req.ctx.db.collection("keys").insertOne({ - // Make sure it's something at least reasonable... - _id: crypto.randomBytes(18).toString("hex"), - userId: req.body.userId, - uid: req.body.uid, - // Not created yet, so we'll just leave it disabled for now - enabled: false, + const keys = {}; + for (const name of REALM_NAMES) { + keys[name + "Id"] = crypto.randomBytes(18).toString("hex"); + } - ...keys, - }); - res.json({ - keyId: insertedKey.insertedId, - uid: req.body.uid, - ...keys, + const insertedKey = await req.ctx.db.collection("keys").insertOne({ + // Make sure it's something at least reasonable... + _id: crypto.randomBytes(18).toString("hex"), + userId: req.body.userId, + uid: req.body.uid, + // Not created yet, so we'll just leave it disabled for now + enabled: false, + + ...keys, + }); + res.json({ + keyId: insertedKey.insertedId, + uid: req.body.uid, + ...keys, + }); }); -}); -router.patch("/:id", async (req, res) => { - const updates = {}; - for (const key of ["uid", "userId", "enabled"]) { - if (key in req.body) { - updates[key] = req.body[key]; - } - } - await req.ctx.db.collection("keys").updateOne( - { - _id: {$eq: req.params.id}, - }, - { - $set: updates, + router.patch("/:id", async (req, res) => { + const updates = {}; + for (const key of ["uid", "userId", "enabled"]) { + if (key in req.body) { + updates[key] = req.body[key]; + } } - ); - res.status(204).send(null); -}); - -router.get("/by-association/:id", async (req, res) => { - const key = await req.ctx.db.collection("keys").findOne({ - $or: REALM_NAMES.map((key) => ({ - [key + "Id"]: { - $eq: req.params.id, + await req.ctx.db.collection("keys").updateOne( + { + _id: {$eq: req.params.id}, }, - })), + { + $set: updates, + } + ); + res.status(204).send(null); }); - if (key) { - const data = { - keyId: key._id, - uid: key.uid, - }; - for (const name of REALM_NAMES) { - data[name + "Id"] = key[name + "Id"]; - } - return res.json(data); - } else { - return res.status(404).json({ - message: "No such key!", + router.get("/by-association/:id", async (req, res) => { + const key = await req.ctx.db.collection("keys").findOne({ + $or: REALM_NAMES.map((key) => ({ + [key + "Id"]: { + $eq: req.params.id, + }, + })), }); - } -}); -router.delete("/by-user", async (req, res) => { - if (typeof req.body.userId != "string") { - return res.status(422).json({ - message: "Missing 'userId'", - }); - } - const results = await req.ctx.db.collection("keys").deleteMany({ - userId: {$eq: req.body.userId}, + if (key) { + const data = { + keyId: key._id, + uid: key.uid, + }; + for (const name of REALM_NAMES) { + data[name + "Id"] = key[name + "Id"]; + } + return res.json(data); + } else { + return res.status(404).json({ + message: "No such key!", + }); + } }); - if (results.deletedCount) { - return res.status(204).send(null); - } else { - return res.status(404).json({ - message: "No keys attached to user!", - }); - } -}); -router.delete("/:keyId", async (req, res) => { - const results = await req.ctx.db.collection("keys").deleteOne({ - _id: {$eq: req.params.keyId}, + router.delete("/by-user", async (req, res) => { + if (typeof req.body.userId != "string") { + return res.status(422).json({ + message: "Missing 'userId'", + }); + } + const results = await req.ctx.db.collection("keys").deleteMany({ + userId: {$eq: req.body.userId}, + }); + if (results.deletedCount) { + return res.status(204).send(null); + } else { + return res.status(404).json({ + message: "No keys attached to user!", + }); + } }); - if (results.deletedCount) { - return res.status(204).send(null); - } else { - return res.status(404).json({ - message: "No keys attached to user!", + + router.delete("/:keyId", async (req, res) => { + const results = await req.ctx.db.collection("keys").deleteOne({ + _id: {$eq: req.params.keyId}, }); - } -}); + if (results.deletedCount) { + return res.status(204).send(null); + } else { + return res.status(404).json({ + message: "No keys attached to user!", + }); + } + }); -export default router; + export default router; diff --git a/routes/memberProjects.js b/routes/memberProjects.js index c2589da..7ee04a4 100644 --- a/routes/memberProjects.js +++ b/routes/memberProjects.js @@ -16,6 +16,7 @@ const ARRAYS = new Set([ router.get("/by-key/:associationId", async (req, res) => { const key = await req.ctx.db.collection("keys").findOne({ [req.associationType]: {$eq: req.params.associationId}, + enabled: { $eq: true }, }); if (!key) { diff --git a/server.js b/server.js index 3f545ac..2af2a77 100644 --- a/server.js +++ b/server.js @@ -89,7 +89,7 @@ connectionPromise.then(async () => { ); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); + res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH"); res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); if (req.method === "OPTIONS") return res.sendStatus(204); next(); From 5b546128331c7a48fdb013364582100a2bb62424 Mon Sep 17 00:00:00 2001 From: aln730 Date: Sun, 12 Jul 2026 20:06:50 -0400 Subject: [PATCH 03/13] feat: datadog metrics --- metrics.js | 7 +++++++ package.json | 1 + server.js | 2 ++ 3 files changed, 10 insertions(+) create mode 100644 metrics.js diff --git a/metrics.js b/metrics.js new file mode 100644 index 0000000..3ff3aae --- /dev/null +++ b/metrics.js @@ -0,0 +1,7 @@ +import StatsD from 'hot-shots'; + +export const statsd = new StatsD({ + prefix: 'gatekeeper.', + globalTags: { service: 'gatekeeper-mqtt' }, + errorHandler: (err) => console.error('StatsD error:', err), +}); diff --git a/package.json b/package.json index 46a5ab0..4ac65e9 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "body-parser": "^1.19.0", "express": "^4.17.1", + "hot-shots": "^17.0.0", "jose": "^5.0.0", "ldapjs": "^2.3.1", "mongodb": "^7.1.0", diff --git a/server.js b/server.js index 3f545ac..deacc1a 100644 --- a/server.js +++ b/server.js @@ -1,3 +1,4 @@ +import { statsd } from "./metrics.js"; import express from "express"; import mqtt from "mqtt"; import { MongoClient, ObjectId } from "mongodb"; @@ -220,6 +221,7 @@ connectionPromise.then(async () => { } else if (topic.endsWith("/heartbeat")) { const doorId = topic.slice(3, -10); doorHeartbeats.set(doorId, Date.now()); + statsd.increment('door.heartbeat', { door: doorId }); console.log(`ACKing a heartbeat from ${doorId}!`); } }); From ea3e59dc4942d2ed4411a60de05792ab8d9a6434 Mon Sep 17 00:00:00 2001 From: aln730 Date: Sun, 12 Jul 2026 23:22:11 -0400 Subject: [PATCH 04/13] fix: Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index c388d55..cee88c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -9,7 +9,7 @@ RUN corepack enable COPY package.json /app/package.json COPY pnpm-lock.yaml /app/pnpm-lock.yaml WORKDIR /app -RUN pnpm i --production=true +RUN pnpm i --production=true --no-optional COPY . /app CMD pnpm run kube From b372fc3ac384a199cf204254c54680749db4699e Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 03:14:27 -0400 Subject: [PATCH 05/13] feat: access route --- middleware/hybridAuth.js | 3 ++- middleware/oidc.js | 3 ++- routes/keys.js | 15 +++++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/middleware/hybridAuth.js b/middleware/hybridAuth.js index f93d69c..ff9e435 100644 --- a/middleware/hybridAuth.js +++ b/middleware/hybridAuth.js @@ -10,9 +10,10 @@ export function hybridAuth(realm) { if (authHeader.startsWith("Bearer ")) { try { - const { userId, groups } = await validateToken(authHeader.slice(7), USER_SCOPE); + const { userId, groups, username } = await validateToken(authHeader.slice(7), USER_SCOPE); req.ctx.userId = userId; req.ctx.groups = groups; + req.ctx.username = username; req.ctx.authMethod = "oidc"; next(); } catch (err) { diff --git a/middleware/oidc.js b/middleware/oidc.js index 7454834..fa83616 100644 --- a/middleware/oidc.js +++ b/middleware/oidc.js @@ -56,7 +56,8 @@ export async function validateToken(token, requiredScope = null) { } const groups = payload.groups ?? []; - return { userId, groups }; + const username = payload.preferred_username + return { userId, groups, username }; } export function oidcAuth(scope) { diff --git a/routes/keys.js b/routes/keys.js index 8b1e57d..85ef9a4 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -4,6 +4,15 @@ const router = Router(); + router.post("/access", async (req, res) => { + const { reason } = req.body; + if (typeof reason != "string" || !reason.trim()) { + return res.status(422).json({ message: "Missing reason field" }); + } + console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); + res.status(204).send(null); + }); + router.get("/by-user", async (req, res) => { if (typeof req.query.userId != "string") { return res.status(422).json({ message: "Missing 'userId' query param" }); @@ -61,6 +70,9 @@ updates[key] = req.body[key]; } } + + const owner = req.body.username ?? "not found"; + console.log(`access: ${req.ctx.username} updated ${owner}'s key ${req.params.id}`); await req.ctx.db.collection("keys").updateOne( { _id: {$eq: req.params.id}, @@ -116,10 +128,13 @@ }); router.delete("/:keyId", async (req, res) => { + const owner = req.body?.username ?? "unknown"; + const results = await req.ctx.db.collection("keys").deleteOne({ _id: {$eq: req.params.keyId}, }); if (results.deletedCount) { + console.log(`keys: ${req.ctx.username} deleted ${owner}'s key ${req.params.keyId}`); return res.status(204).send(null); } else { return res.status(404).json({ From a104ccac3b47616a81c8ee41bffefe256f67eae6 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:10:49 -0400 Subject: [PATCH 06/13] feat: audit route + uid in logEntry + username --- access.js | 6 ++++++ middleware/hybridAuth.js | 5 +++-- middleware/oidc.js | 5 +++-- routes/keys.js | 20 ++++++++++++++++++++ routes/logs.js | 37 ++++++++++++++++++++++++++++++++++++- server.js | 3 +++ 6 files changed, 71 insertions(+), 5 deletions(-) diff --git a/access.js b/access.js index 9e0f839..301d64f 100644 --- a/access.js +++ b/access.js @@ -21,3 +21,9 @@ export async function checkAccess(db, userId, doorId) { ); return groupTicket?.granted; } + +export async function recordAudit(db, entry) { + db.collection("auditLogs").insertOne({ ...entry, timestamp: new Date() }).catch((err) => { + console.error("Failed to write audit entry", err); + }); +} \ No newline at end of file diff --git a/middleware/hybridAuth.js b/middleware/hybridAuth.js index ff9e435..044b53f 100644 --- a/middleware/hybridAuth.js +++ b/middleware/hybridAuth.js @@ -10,10 +10,11 @@ export function hybridAuth(realm) { if (authHeader.startsWith("Bearer ")) { try { - const { userId, groups, username } = await validateToken(authHeader.slice(7), USER_SCOPE); + const { userId, groups, username, name } = await validateToken(authHeader.slice(7), USER_SCOPE); req.ctx.userId = userId; req.ctx.groups = groups; req.ctx.username = username; + req.ctx.name = name; req.ctx.authMethod = "oidc"; next(); } catch (err) { @@ -29,4 +30,4 @@ export function hybridAuth(realm) { next(); } }; -} +} \ No newline at end of file diff --git a/middleware/oidc.js b/middleware/oidc.js index fa83616..267fa8f 100644 --- a/middleware/oidc.js +++ b/middleware/oidc.js @@ -56,8 +56,9 @@ export async function validateToken(token, requiredScope = null) { } const groups = payload.groups ?? []; - const username = payload.preferred_username - return { userId, groups, username }; + const username = payload.preferred_username; + const name = payload.name; + return { userId, groups, username, name }; } export function oidcAuth(scope) { diff --git a/routes/keys.js b/routes/keys.js index 85ef9a4..d5f07fb 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -1,6 +1,7 @@ import { Router } from "express"; import crypto from "crypto"; import { REALM_NAMES } from "../constants.js"; + import { recordAudit } from "../access.js"; const router = Router(); @@ -10,6 +11,12 @@ return res.status(422).json({ message: "Missing reason field" }); } console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Viewed Keys", + reason, + }); res.status(204).send(null); }); @@ -81,6 +88,13 @@ $set: updates, } ); + const change = updates.enabled === false ? "Disabled" : updates.enabled === true ? "Enabled" : "updated"; + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Updated Keys", + reason: `${change} ${owner}'s key ${req.params.id}`, + }); res.status(204).send(null); }); @@ -135,6 +149,12 @@ }); if (results.deletedCount) { console.log(`keys: ${req.ctx.username} deleted ${owner}'s key ${req.params.keyId}`); + await recordAudit(req.ctx.db,{ + username: req.ctx.username, + name: req.ctx.name, + action: "Deleted Keys", + reason: `deleted ${owner}'s key ${req.params.keyId}`, + }); return res.status(204).send(null); } else { return res.status(404).json({ diff --git a/routes/logs.js b/routes/logs.js index f25429d..588274c 100644 --- a/routes/logs.js +++ b/routes/logs.js @@ -1,8 +1,30 @@ import { Router } from "express"; +import { recordAudit } from "../access.js"; + const router = Router(); +router.post("/access", async (req, res) => { + const { reason } = req.body; + if (typeof reason != "string" || !reason.trim()) { + return res.status(422).json({ message: "Missing 'reason' field" }); + } + console.log(`access: ${req.ctx.username} viewed logs || reason: ${reason}`); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Viewed Logs", + reason, + }); + res.status(204).send(null); +}); + +router.get("/doors", async (req, res) => { + const doors = await req.ctx.db.collection("accessLogs").distinct("doorName"); + res.json(doors.filter(Boolean)); +}); + router.get("/", async (req, res) => { - const { cursor, since, until } = req.query; + const { cursor, since, until, search, door, granted } = req.query; const query = {}; if (since || until || cursor) { query.timestamp = {}; @@ -11,6 +33,19 @@ router.get("/", async (req, res) => { if (until) query.timestamp.$lte = new Date(until); } + const ands = []; + if (door && door !== "all") { + ands.push({ $or: [{ doorName: door }, { door }] }); + } + if (search) { + const re = new RegExp(search, "i"); + ands.push({ $or: [{ doorName: re }, { door: re }, { username: re }, { name: re }] }); + } + if (ands.length) query.$and = ands; + + if (granted === "granted") query.granted = true; + if (granted === "denied") query.granted = false; + const logs = await req.ctx.db .collection("accessLogs") .find(query) diff --git a/server.js b/server.js index 75a6a66..41b3121 100644 --- a/server.js +++ b/server.js @@ -19,6 +19,7 @@ import keys from "./routes/keys.js"; import users from "./routes/users.js"; import mobile from "./routes/mobile.js"; import logs from "./routes/logs.js"; +import audit from "./routes/audit.js"; //fetch user from the https endpoint async function fetchUser(endpoint, token, memberProjectsId) { @@ -127,6 +128,7 @@ connectionPromise.then(async () => { app.use("/admin/keys", hybridAuth("admin"), requireGroup("rtp"), keys); app.use("/admin/users", hybridAuth("admin"), requireGroup("rtp"), users); app.use("/admin/logs", hybridAuth("admin"), requireGroup("rtp"), logs); + app.use("/admin/audit", hybridAuth("admin"), requireGroup("rtp"), audit); app.use("/mobile", mobile); client.on("connect", async () => { @@ -199,6 +201,7 @@ connectionPromise.then(async () => { name, doorsId: payload.association, keyId: key._id, + uid: key.uid ?? null, granted: !!granted, }; From cfc75b176a03f68f2e58adb17327a508af0e7685 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:11:21 -0400 Subject: [PATCH 07/13] feat: audit route (mostly stuff from logs route but for audit logs) --- routes/audit.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 routes/audit.js diff --git a/routes/audit.js b/routes/audit.js new file mode 100644 index 0000000..fbf90ac --- /dev/null +++ b/routes/audit.js @@ -0,0 +1,34 @@ +import { Router } from "express"; +const router = Router(); + +router.get("/", async (req, res) => { + const { cursor, search, action } = req.query; + const query = {}; + + if (cursor) { + query.timestamp = { $lt: new Date(cursor) }; + } + + const ands = []; + if (action && action !== "all") ands.push({ action }); + if (search) { + const re = new RegExp(search, "i"); + ands.push({ $or: [{ username: re }, { name: re }, { reason: re }] }); + } + if (ands.length) query.$and = ands; + + const entries = await req.ctx.db + .collection("auditLogs") + .find(query) + .sort({ timestamp: -1 }) + .limit(50) + .toArray(); + + const nextCursor = entries.length === 50 + ? entries[entries.length - 1].timestamp.toISOString() + : null; + + res.json({ entries, cursor: nextCursor }); +}); + +export default router; \ No newline at end of file From 313b1849d2343d2924c6fbd8a3d7eb77370d3fc9 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:36:38 -0400 Subject: [PATCH 08/13] docker stuff --- .dockerignore | 4 ++++ Dockerfile | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0c8ddf6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +*.log +.env diff --git a/Dockerfile b/Dockerfile index cee88c7..88fcc05 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,4 +12,4 @@ WORKDIR /app RUN pnpm i --production=true --no-optional COPY . /app -CMD pnpm run kube +CMD ["pnpm", "run", "kube"] From e0de99a675cc3075f5d9bdc9128a3d3772792e41 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sat, 25 Jul 2026 21:52:13 -0400 Subject: [PATCH 09/13] docs: new routes --- docs.org | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs.org b/docs.org index bced348..609e791 100644 --- a/docs.org +++ b/docs.org @@ -17,17 +17,30 @@ *** GET /admin/users/uuid-by-uid/:uid Fetch user UUID by CSH username ** /admin/keys +*** POST /admin/keys/access + State a reason to view keys +*** GET /admin/keys/by-user + Fetch keys attached to a user *** PUT /admin/keys Add a key for a user *** PATCH /admin/keys/:id Update a key +*** GET /admin/keys/by-association/:id + Fetch key by ID *** DELETE /admin/keys/by-user Delete all keys attached to a user *** DELETE /admin/keys/:keyId Delete a key ** /admin/logs +*** POST /admin/logs/access + State a reason to view logs *** GET /admin/logs Fetch paginated access logs +*** GET /admin/logs/doors + List distinct doors that appear in access logs +** /admin/audit +*** GET /admin/audit + Fetch paginated audit logs * /projects ** GET /projects/by-key/:keyId - Fetches user details from LDAP attached to the key with the given ID (from the member project realm) + Fetches user details from LDAP attached to the key with the given ID (from the member project realm) \ No newline at end of file From 7da12c8972b5c808870419044f966ce886c274f5 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Mon, 27 Jul 2026 21:30:15 -0400 Subject: [PATCH 10/13] feat: accessLog expires after 6 months --- server.js | 1 + 1 file changed, 1 insertion(+) diff --git a/server.js b/server.js index 41b3121..37f7a09 100644 --- a/server.js +++ b/server.js @@ -49,6 +49,7 @@ connectionPromise.then(async () => { db.collection("keys").createIndex("drinkId", {unique: true}), db.collection("keys").createIndex("memberProjectsId", {unique: true}), db.collection("accessLogs").createIndex({ timestamp: -1 }), + db.collection("accessLogs").createIndex({ timestamp: 1 }, { expireAfterSeconds: 15778476 }), ]); async function scheduledTasks() { From 6a6b81f8ddced98ca86baeab96317fd16cefc272 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Tue, 28 Jul 2026 03:10:07 -0400 Subject: [PATCH 11/13] feat: rejects request if door is offline --- routes/doors.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/routes/doors.js b/routes/doors.js index 820e0e7..049aa4d 100644 --- a/routes/doors.js +++ b/routes/doors.js @@ -4,20 +4,24 @@ import { checkAccess } from "../access.js"; const router = Router(); -router.get("/:doorId/status", (req, res) => { +function getDoorStatus(doorId){ // If it's been more than 1 minute, we assume something is broken... - const lastHeartbeat = doorHeartbeats.get(req.params.doorId); + const lastHeartbeat = doorHeartbeats.get(doorId); if (lastHeartbeat) { - res.json({ + return { guess: Date.now() - lastHeartbeat > 1000 * 60 ? "offline" : "online", lastHeartbeat, - }); + }; } else { - res.json({ + return { guess: "offline", lastHeartbeat: 0, - }); + }; } +} + +router.get("/:doorId/status", (req, res) => { + res.json(getDoorStatus(req.params.doorId)); }); router.get("/", async (req, res) => { @@ -46,6 +50,11 @@ router.post("/:doorId/unlock", async (req, res) => { return res.status(403).json({ message: "Access denied" }); } } + + if (getDoorStatus(req.params.doorId).guess === "offline") { + return res.status(409).json({ message: "Door is offline" }); + } + req.ctx.mqtt.publish(`gk/${req.params.doorId}/unlock`, ""); res.status(204).send(null); }); From 6f1d8f21938e54fcf457480367ce93abd7883183 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sun, 2 Aug 2026 12:47:35 -0400 Subject: [PATCH 12/13] feat: logs for remote access --- access.js | 20 ++++++++++++++++- routes/doors.js | 20 ++++++++++++++++- routes/keys.js | 60 ++++++++++++++++++++++++------------------------- 3 files changed, 68 insertions(+), 32 deletions(-) diff --git a/access.js b/access.js index 301d64f..bd7a636 100644 --- a/access.js +++ b/access.js @@ -23,7 +23,25 @@ export async function checkAccess(db, userId, doorId) { } export async function recordAudit(db, entry) { - db.collection("auditLogs").insertOne({ ...entry, timestamp: new Date() }).catch((err) => { + return db.collection("auditLogs").insertOne({ ...entry, timestamp: new Date() }).catch((err) => { console.error("Failed to write audit entry", err); + throw err; + }); +} + +export async function recordDoorUnlock(db, { doorId, doorName, username, name }) { + return db.collection("accessLogs").insertOne({ + timestamp: new Date(), + door: doorId, + doorName: doorName, + username, + name: name, + doorsId: null, + keyId: "Remote Access", + uid: null, + granted: true, + }).catch((err) => { + console.error("Failed to write accessLogs", err); + throw err; }); } \ No newline at end of file diff --git a/routes/doors.js b/routes/doors.js index 049aa4d..e8b9fd9 100644 --- a/routes/doors.js +++ b/routes/doors.js @@ -1,6 +1,7 @@ import { Router } from "express"; import { doorHeartbeats } from "../state.js"; -import { checkAccess } from "../access.js"; +import { checkAccess, recordDoorUnlock } from "../access.js"; +import { ObjectId } from "mongodb"; const router = Router(); @@ -55,6 +56,23 @@ router.post("/:doorId/unlock", async (req, res) => { return res.status(409).json({ message: "Door is offline" }); } + if (req.ctx.authMethod === "oidc") { + const doorId = req.params.doorId; + const doorDoc = await req.ctx.db.collection("doors").findOne({ + $or: [ + { _id: doorId }, + ...(ObjectId.isValid(doorId) ? [{ _id: new ObjectId(doorId) }] : []), + ], + }); + + await recordDoorUnlock(req.ctx.db, { + doorId: req.params.doorId, + doorName: doorDoc?.name, + username: req.ctx.username ?? req.ctx.userId, + name: req.ctx.name, + }); + } + req.ctx.mqtt.publish(`gk/${req.params.doorId}/unlock`, ""); res.status(204).send(null); }); diff --git a/routes/keys.js b/routes/keys.js index d5f07fb..57c1321 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -1,36 +1,36 @@ - import { Router } from "express"; - import crypto from "crypto"; - import { REALM_NAMES } from "../constants.js"; - import { recordAudit } from "../access.js"; +import { Router } from "express"; +import crypto from "crypto"; +import { REALM_NAMES } from "../constants.js"; +import { recordAudit } from "../access.js"; - const router = Router(); +const router = Router(); - router.post("/access", async (req, res) => { - const { reason } = req.body; - if (typeof reason != "string" || !reason.trim()) { - return res.status(422).json({ message: "Missing reason field" }); - } - console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); - await recordAudit(req.ctx.db, { - username: req.ctx.username, - name: req.ctx.name, - action: "Viewed Keys", - reason, - }); - res.status(204).send(null); - }); - - router.get("/by-user", async (req, res) => { - if (typeof req.query.userId != "string") { - return res.status(422).json({ message: "Missing 'userId' query param" }); +router.post("/access", async (req, res) => { + const { reason } = req.body; + if (typeof reason != "string" || !reason.trim()) { + return res.status(422).json({ message: "Missing reason field" }); } - const keys = await req.ctx.db.collection("keys").find({ userId: req.query.userId }).toArray(); - res.json(keys); - }); - - // First, PUT /keys with details of user key is for - // Receive a keyId back which is our association - // Register key using association and send back the now-randomised UID + console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Viewed Keys", + reason, + }); + res.status(204).send(null); +}); + +router.get("/by-user", async (req, res) => { + if (typeof req.query.userId != "string") { + return res.status(422).json({ message: "Missing 'userId' query param" }); + } + const keys = await req.ctx.db.collection("keys").find({ userId: req.query.userId }).toArray(); + res.json(keys); +}); + +// First, PUT /keys with details of user key is for +// Receive a keyId back which is our association +// Register key using association and send back the now-randomised UID // with PATCH /keys/:id router.put("/", async (req, res) => { From ab94458a90f9d375fb38b982abcb4271e43fae36 Mon Sep 17 00:00:00 2001 From: Arnav Gawas Date: Sun, 2 Aug 2026 17:37:17 -0400 Subject: [PATCH 13/13] feat: workflows and ci stuff, new type field, refactored logs --- .github/workflows/docker-build.yml | 23 + .github/workflows/lint.yml | 21 + access.js | 28 +- eslint.config.js | 17 + ldap.js | 4 +- metrics.js | 8 +- middleware/hybridAuth.js | 7 +- middleware/oidc.js | 32 +- package.json | 14 +- pnpm-lock.yaml | 917 +++++++++++++++++++++-------- routes/audit.js | 19 +- routes/doors.js | 87 +-- routes/keys.js | 273 +++++---- routes/logs.js | 51 +- routes/memberProjects.js | 14 +- routes/mobile.js | 2 +- routes/users.js | 26 +- server.js | 86 ++- sync.js | 14 +- util.js | 2 +- 20 files changed, 1087 insertions(+), 558 deletions(-) create mode 100644 .github/workflows/docker-build.yml create mode 100644 .github/workflows/lint.yml create mode 100644 eslint.config.js diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..79d6858 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,23 @@ +name: Docker build + +on: + push: + branches: + - master + + pull_request: + branches: + - master + +env: + IMAGE_NAME: gatekeeper-mqtt + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - name: Run test + run: docker build . --file Dockerfile diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..3a1bd6a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,21 @@ +name: Lint + +on: + push: + branches: + - master + pull_request: + branches: + - master + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: lint + run: | + npm ci + npm run lint + npm run format:check \ No newline at end of file diff --git a/access.js b/access.js index bd7a636..445bd34 100644 --- a/access.js +++ b/access.js @@ -8,7 +8,7 @@ export async function checkAccess(db, userId, doorId) { userId: { $in: [userId, "*"] }, doorId: { $in: [doorId, "*"] }, }, - { sort: { priority: -1 } } + { sort: { priority: -1 } }, ); if (userTicket !== null) return userTicket.granted; @@ -17,31 +17,7 @@ export async function checkAccess(db, userId, doorId) { doorId: { $in: ["*", doorId] }, groupId: { $in: dbUser.groups ? dbUser.groups.concat("*") : ["*"] }, }, - { sort: { priority: -1 } } + { sort: { priority: -1 } }, ); return groupTicket?.granted; } - -export async function recordAudit(db, entry) { - return db.collection("auditLogs").insertOne({ ...entry, timestamp: new Date() }).catch((err) => { - console.error("Failed to write audit entry", err); - throw err; - }); -} - -export async function recordDoorUnlock(db, { doorId, doorName, username, name }) { - return db.collection("accessLogs").insertOne({ - timestamp: new Date(), - door: doorId, - doorName: doorName, - username, - name: name, - doorsId: null, - keyId: "Remote Access", - uid: null, - granted: true, - }).catch((err) => { - console.error("Failed to write accessLogs", err); - throw err; - }); -} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..57566e9 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,17 @@ +import js from "@eslint/js"; +import globals from "globals"; +import prettier from "eslint-config-prettier"; + +export default [ + js.configs.recommended, + { + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + globals: { + ...globals.node, + }, + }, + }, + prettier, +]; diff --git a/ldap.js b/ldap.js index ace6ae9..17c5c31 100644 --- a/ldap.js +++ b/ldap.js @@ -34,7 +34,7 @@ export function searchOne(base, filter, attributes) { res.once("searchEntry", onSearchEntry); res.once("end", onEnd); } - } + }, ); }); } @@ -54,7 +54,7 @@ resolve("_ldap._tcp.csh.rit.edu").then((records) => { throw err; } console.log("LDAP is bound!"); - } + }, ); }); }); diff --git a/metrics.js b/metrics.js index 3ff3aae..c13143e 100644 --- a/metrics.js +++ b/metrics.js @@ -1,7 +1,7 @@ -import StatsD from 'hot-shots'; +import StatsD from "hot-shots"; export const statsd = new StatsD({ - prefix: 'gatekeeper.', - globalTags: { service: 'gatekeeper-mqtt' }, - errorHandler: (err) => console.error('StatsD error:', err), + prefix: "gatekeeper.", + globalTags: { service: "gatekeeper-mqtt" }, + errorHandler: (err) => console.error("StatsD error:", err), }); diff --git a/middleware/hybridAuth.js b/middleware/hybridAuth.js index 044b53f..5486f30 100644 --- a/middleware/hybridAuth.js +++ b/middleware/hybridAuth.js @@ -10,7 +10,10 @@ export function hybridAuth(realm) { if (authHeader.startsWith("Bearer ")) { try { - const { userId, groups, username, name } = await validateToken(authHeader.slice(7), USER_SCOPE); + const { userId, groups, username, name } = await validateToken( + authHeader.slice(7), + USER_SCOPE, + ); req.ctx.userId = userId; req.ctx.groups = groups; req.ctx.username = username; @@ -30,4 +33,4 @@ export function hybridAuth(realm) { next(); } }; -} \ No newline at end of file +} diff --git a/middleware/oidc.js b/middleware/oidc.js index 267fa8f..0a02aa7 100644 --- a/middleware/oidc.js +++ b/middleware/oidc.js @@ -6,7 +6,7 @@ export const USER_SCOPE = "gatekeeper_user"; const USER_ID_CLAIM = "uuid"; const issuerUrl = new URL( - process.env.GK_OIDC_ISSUER || "https://sso.csh.rit.edu/auth/realms/csh" + process.env.GK_OIDC_ISSUER || "https://sso.csh.rit.edu/auth/realms/csh", ); export const clientId = process.env.GK_OIDC_CLIENT_ID; @@ -35,7 +35,7 @@ export async function validateToken(token, requiredScope = null) { let payload; try { ({ payload } = await jwtVerify(token, JWKS, verifyOptions)); - } catch (err) { + } catch { throw new AuthError("Invalid or expired token", 401); } @@ -45,7 +45,7 @@ export async function validateToken(token, requiredScope = null) { if (!scopes.includes(requiredScope)) { throw new AuthError( `Token missing required scope: ${requiredScope}`, - 403 + 403, ); } } @@ -62,19 +62,19 @@ export async function validateToken(token, requiredScope = null) { } export function oidcAuth(scope) { -return async function oidcAuth(req, res, next) { - const authHeader = req.headers.authorization; - if (!authHeader?.startsWith("Bearer ")) { - return res.status(401).json({ message: "Bearer token required" }); - } - try { - const { userId } = await validateToken(authHeader.slice(7), scope); - req.ctx.userId = userId; - next(); - } catch (err) { - return res.status(err.status || 401).json({ message: err.message }); - } -} + return async function oidcAuth(req, res, next) { + const authHeader = req.headers.authorization; + if (!authHeader?.startsWith("Bearer ")) { + return res.status(401).json({ message: "Bearer token required" }); + } + try { + const { userId } = await validateToken(authHeader.slice(7), scope); + req.ctx.userId = userId; + next(); + } catch (err) { + return res.status(err.status || 401).json({ message: err.message }); + } + }; } export function requireGroup(group) { diff --git a/package.json b/package.json index 4ac65e9..c7907a5 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ "main": "index.js", "scripts": { "start": "node server.js", - "kube": "GK_MQTT_SERVER=mqtts://$MOSQUITTO_SERVICE_HOST:$MOSQUITTO_SERVICE_PORT node server.js" + "kube": "GK_MQTT_SERVER=mqtts://$MOSQUITTO_SERVICE_HOST:$MOSQUITTO_SERVICE_PORT node server.js", + "lint": "eslint .", + "format": "prettier --write .", + "format:check": "prettier --check ." }, "author": "Mary Strodl", "license": "MIT", @@ -21,5 +24,12 @@ "mqtt": "^4.2.6", "openid-client": "^6.0.0" }, - "packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215" + "packageManager": "pnpm@11.3.0+sha512.2c403d6594527287672b1f7056343a1f7c3634036a67ffabfcc2b3d7595d843768f8787148d1b57cf7956c90606bbd192857c363af19e96d2d0ec9ec5741d215", + "devDependencies": { + "@eslint/js": "^10.0.1", + "eslint": "^10.8.0", + "eslint-config-prettier": "^10.1.8", + "globals": "^17.9.0", + "prettier": "^3.9.6" + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b863f69..6ddd984 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,11 +1,10 @@ -lockfileVersion: '9.0' +lockfileVersion: "9.0" settings: autoInstallPeers: true excludeLinksFromLockfile: false importers: - .: dependencies: body-parser: @@ -34,325 +33,570 @@ importers: version: 6.8.4 packages: - - '@mongodb-js/saslprep@1.4.11': - resolution: {integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==} - - '@types/webidl-conversions@7.0.3': - resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} - - '@types/whatwg-url@13.0.0': - resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==} + "@mongodb-js/saslprep@1.4.11": + resolution: + { + integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==, + } + + "@types/webidl-conversions@7.0.3": + resolution: + { + integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==, + } + + "@types/whatwg-url@13.0.0": + resolution: + { + integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==, + } abstract-logging@2.0.1: - resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + resolution: + { + integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==, + } accepts@1.3.8: - resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==, + } + engines: { node: ">= 0.6" } array-flatten@1.1.1: - resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + resolution: + { + integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==, + } asn1@0.2.6: - resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + resolution: + { + integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==, + } assert-plus@1.0.0: - resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} - engines: {node: '>=0.8'} + resolution: + { + integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==, + } + engines: { node: ">=0.8" } backoff@2.5.0: - resolution: {integrity: sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==, + } + engines: { node: ">= 0.6" } balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + resolution: + { + integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==, + } basic-auth@2.0.1: - resolution: {integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==, + } + engines: { node: ">= 0.8" } bl@4.1.0: - resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + resolution: + { + integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==, + } body-parser@1.20.5: - resolution: {integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + resolution: + { + integrity: sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==, + } + engines: { node: ">= 0.8", npm: 1.2.8000 || >= 1.4.16 } brace-expansion@1.1.14: - resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + resolution: + { + integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==, + } bson@7.2.0: - resolution: {integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==, + } + engines: { node: ">=20.19.0" } buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + resolution: + { + integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, + } buffer@5.7.1: - resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + resolution: + { + integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==, + } bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==, + } + engines: { node: ">= 0.8" } call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, + } + engines: { node: ">= 0.4" } call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==, + } + engines: { node: ">= 0.4" } commist@1.1.0: - resolution: {integrity: sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==} + resolution: + { + integrity: sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==, + } concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} + resolution: + { + integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==, + } + engines: { "0": node >= 6.0 } content-disposition@0.5.4: - resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==, + } + engines: { node: ">= 0.6" } content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==, + } + engines: { node: ">= 0.6" } cookie-signature@1.0.7: - resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + resolution: + { + integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==, + } cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==, + } + engines: { node: ">= 0.6" } core-util-is@1.0.2: - resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + resolution: + { + integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==, + } debug@2.6.9: - resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + resolution: + { + integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==, + } peerDependencies: - supports-color: '*' + supports-color: "*" peerDependenciesMeta: supports-color: optional: true debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} + resolution: + { + integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, + } + engines: { node: ">=6.0" } peerDependencies: - supports-color: '*' + supports-color: "*" peerDependenciesMeta: supports-color: optional: true depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==, + } + engines: { node: ">= 0.8" } destroy@1.2.0: - resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} - engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + resolution: + { + integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==, + } + engines: { node: ">= 0.8", npm: 1.2.8000 || >= 1.4.16 } dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, + } + engines: { node: ">= 0.4" } duplexify@4.1.3: - resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==} + resolution: + { + integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==, + } ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + resolution: + { + integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==, + } encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==, + } + engines: { node: ">= 0.8" } end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + resolution: + { + integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==, + } es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, + } + engines: { node: ">= 0.4" } es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: ">= 0.4" } es-object-atoms@1.1.2: - resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==, + } + engines: { node: ">= 0.4" } escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + resolution: + { + integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==, + } etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==, + } + engines: { node: ">= 0.6" } express@4.22.2: - resolution: {integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==} - engines: {node: '>= 0.10.0'} + resolution: + { + integrity: sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==, + } + engines: { node: ">= 0.10.0" } extsprintf@1.4.1: - resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==} - engines: {'0': node >=0.6.0} + resolution: + { + integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==, + } + engines: { "0": node >=0.6.0 } finalhandler@1.3.2: - resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==, + } + engines: { node: ">= 0.8" } forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==, + } + engines: { node: ">= 0.6" } fresh@0.5.2: - resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==, + } + engines: { node: ">= 0.6" } fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + resolution: + { + integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, + } function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, + } + engines: { node: ">= 0.4" } get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, + } + engines: { node: ">= 0.4" } glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + resolution: + { + integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, + } deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, + } + engines: { node: ">= 0.4" } has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, + } + engines: { node: ">= 0.4" } hasown@2.0.3: - resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==, + } + engines: { node: ">= 0.4" } help-me@3.0.0: - resolution: {integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==} + resolution: + { + integrity: sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==, + } http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==, + } + engines: { node: ">= 0.8" } iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==, + } + engines: { node: ">=0.10.0" } ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + resolution: + { + integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, + } inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + resolution: + { + integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, + } deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==, + } + engines: { node: ">= 0.10" } jose@5.10.0: - resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + resolution: + { + integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==, + } jose@6.2.3: - resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + resolution: + { + integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==, + } js-sdsl@4.3.0: - resolution: {integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==} + resolution: + { + integrity: sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==, + } ldap-filter@0.3.3: - resolution: {integrity: sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==} - engines: {node: '>=0.8'} + resolution: + { + integrity: sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==, + } + engines: { node: ">=0.8" } ldapjs@2.3.3: - resolution: {integrity: sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==} - engines: {node: '>=10.13.0'} + resolution: + { + integrity: sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==, + } + engines: { node: ">=10.13.0" } deprecated: This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md leven@2.1.0: - resolution: {integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==, + } + engines: { node: ">=0.10.0" } lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} + resolution: + { + integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==, + } + engines: { node: ">=10" } math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, + } + engines: { node: ">= 0.4" } media-typer@0.3.0: - resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==, + } + engines: { node: ">= 0.6" } memory-pager@1.5.0: - resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==} + resolution: + { + integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==, + } merge-descriptors@1.0.3: - resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + resolution: + { + integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==, + } methods@1.1.2: - resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==, + } + engines: { node: ">= 0.6" } mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, + } + engines: { node: ">= 0.6" } mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, + } + engines: { node: ">= 0.6" } mime@1.6.0: - resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} - engines: {node: '>=4'} + resolution: + { + integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==, + } + engines: { node: ">=4" } hasBin: true minimatch@3.1.5: - resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + resolution: + { + integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==, + } minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + resolution: + { + integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==, + } mongodb-connection-string-url@7.0.1: - resolution: {integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==, + } + engines: { node: ">=20.19.0" } mongodb@7.2.0: - resolution: {integrity: sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==} - engines: {node: '>=20.19.0'} + resolution: + { + integrity: sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==, + } + engines: { node: ">=20.19.0" } peerDependencies: - '@aws-sdk/credential-providers': ^3.806.0 - '@mongodb-js/zstd': ^7.0.0 + "@aws-sdk/credential-providers": ^3.806.0 + "@mongodb-js/zstd": ^7.0.0 gcp-metadata: ^7.0.1 kerberos: ^7.0.0 - mongodb-client-encryption: '>=7.0.0 <7.1.0' + mongodb-client-encryption: ">=7.0.0 <7.1.0" snappy: ^7.3.2 socks: ^2.8.6 peerDependenciesMeta: - '@aws-sdk/credential-providers': + "@aws-sdk/credential-providers": optional: true - '@mongodb-js/zstd': + "@mongodb-js/zstd": optional: true gcp-metadata: optional: true @@ -366,214 +610,388 @@ packages: optional: true morgan@1.10.1: - resolution: {integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==, + } + engines: { node: ">= 0.8.0" } mqtt-packet@6.10.0: - resolution: {integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==} + resolution: + { + integrity: sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==, + } mqtt@4.3.8: - resolution: {integrity: sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==} - engines: {node: '>=10.0.0'} + resolution: + { + integrity: sha512-2xT75uYa0kiPEF/PE0VPdavmEkoBzMT/UL9moid0rAvlCtV48qBwxD62m7Ld/4j8tSkIO1E/iqRl/S72SEOhOw==, + } + engines: { node: ">=10.0.0" } hasBin: true ms@2.0.0: - resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + resolution: + { + integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==, + } ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } negotiator@0.6.3: - resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==, + } + engines: { node: ">= 0.6" } number-allocator@1.0.14: - resolution: {integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==} + resolution: + { + integrity: sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA==, + } oauth4webapi@3.8.6: - resolution: {integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==} + resolution: + { + integrity: sha512-iwemM91xz8nryHti2yTmg5fhyEMVOkOXwHNqbvcATjyajb5oQxCQzrNOA6uElRHuMhQQTKUyFKV9y/CNyg25BQ==, + } object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==, + } + engines: { node: ">= 0.4" } on-finished@2.3.0: - resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==, + } + engines: { node: ">= 0.8" } on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==, + } + engines: { node: ">= 0.8" } on-headers@1.1.0: - resolution: {integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==, + } + engines: { node: ">= 0.8" } once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } openid-client@6.8.4: - resolution: {integrity: sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==} + resolution: + { + integrity: sha512-QSw0BA08piujetEwfZsHoTrDpMEha7GDZDicQqVwX4u0ChCjefvjDB++TZ8BTg76UpwhzIQgdvvfgfl3HpCSAw==, + } parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==, + } + engines: { node: ">= 0.8" } path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} + resolution: + { + integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, + } + engines: { node: ">=0.10.0" } path-to-regexp@0.1.13: - resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==} + resolution: + { + integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==, + } precond@0.2.3: - resolution: {integrity: sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==, + } + engines: { node: ">= 0.6" } process-nextick-args@2.0.1: - resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + resolution: + { + integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==, + } proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + resolution: + { + integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==, + } + engines: { node: ">= 0.10" } pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + resolution: + { + integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==, + } punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} - engines: {node: '>=6'} + resolution: + { + integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, + } + engines: { node: ">=6" } qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} - engines: {node: '>=0.6'} + resolution: + { + integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==, + } + engines: { node: ">=0.6" } range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==, + } + engines: { node: ">= 0.6" } raw-body@2.5.3: - resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==, + } + engines: { node: ">= 0.8" } readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + resolution: + { + integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, + } + engines: { node: ">= 6" } reinterval@1.1.0: - resolution: {integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==} + resolution: + { + integrity: sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==, + } rfdc@1.4.1: - resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + resolution: + { + integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==, + } safe-buffer@5.1.2: - resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + resolution: + { + integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==, + } safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + resolution: + { + integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, + } safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, + } send@0.19.2: - resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==, + } + engines: { node: ">= 0.8.0" } serve-static@1.16.3: - resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} - engines: {node: '>= 0.8.0'} + resolution: + { + integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==, + } + engines: { node: ">= 0.8.0" } setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + resolution: + { + integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==, + } side-channel-list@1.0.1: - resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==, + } + engines: { node: ">= 0.4" } side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==, + } + engines: { node: ">= 0.4" } side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==, + } + engines: { node: ">= 0.4" } side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} + resolution: + { + integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==, + } + engines: { node: ">= 0.4" } sparse-bitfield@3.0.3: - resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + resolution: + { + integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==, + } split2@3.2.2: - resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + resolution: + { + integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==, + } statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==, + } + engines: { node: ">= 0.8" } stream-shift@1.0.3: - resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + resolution: + { + integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==, + } string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, + } toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} + resolution: + { + integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==, + } + engines: { node: ">=0.6" } tr46@5.1.1: - resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==, + } + engines: { node: ">=18" } type-is@1.6.18: - resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} - engines: {node: '>= 0.6'} + resolution: + { + integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==, + } + engines: { node: ">= 0.6" } typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + resolution: + { + integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==, + } unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==, + } + engines: { node: ">= 0.8" } util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } utils-merge@1.0.1: - resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} - engines: {node: '>= 0.4.0'} + resolution: + { + integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==, + } + engines: { node: ">= 0.4.0" } vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} + resolution: + { + integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==, + } + engines: { node: ">= 0.8" } vasync@2.2.1: - resolution: {integrity: sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==} - engines: {'0': node >=0.6.0} + resolution: + { + integrity: sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==, + } + engines: { "0": node >=0.6.0 } verror@1.10.0: - resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} - engines: {'0': node >=0.6.0} + resolution: + { + integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==, + } + engines: { "0": node >=0.6.0 } verror@1.10.1: - resolution: {integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==} - engines: {node: '>=0.6.0'} + resolution: + { + integrity: sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==, + } + engines: { node: ">=0.6.0" } webidl-conversions@7.0.0: - resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} - engines: {node: '>=12'} + resolution: + { + integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==, + } + engines: { node: ">=12" } whatwg-url@14.2.0: - resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} - engines: {node: '>=18'} + resolution: + { + integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==, + } + engines: { node: ">=18" } wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } ws@7.5.11: - resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} - engines: {node: '>=8.3.0'} + resolution: + { + integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==, + } + engines: { node: ">=8.3.0" } peerDependencies: bufferutil: ^4.0.1 utf-8-validate: ^5.0.2 @@ -584,23 +1002,28 @@ packages: optional: true xtend@4.0.2: - resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} - engines: {node: '>=0.4'} + resolution: + { + integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==, + } + engines: { node: ">=0.4" } yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + resolution: + { + integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==, + } snapshots: - - '@mongodb-js/saslprep@1.4.11': + "@mongodb-js/saslprep@1.4.11": dependencies: sparse-bitfield: 3.0.3 - '@types/webidl-conversions@7.0.3': {} + "@types/webidl-conversions@7.0.3": {} - '@types/whatwg-url@13.0.0': + "@types/whatwg-url@13.0.0": dependencies: - '@types/webidl-conversions': 7.0.3 + "@types/webidl-conversions": 7.0.3 abstract-logging@2.0.1: {} @@ -923,12 +1346,12 @@ snapshots: mongodb-connection-string-url@7.0.1: dependencies: - '@types/whatwg-url': 13.0.0 + "@types/whatwg-url": 13.0.0 whatwg-url: 14.2.0 mongodb@7.2.0: dependencies: - '@mongodb-js/saslprep': 1.4.11 + "@mongodb-js/saslprep": 1.4.11 bson: 7.2.0 mongodb-connection-string-url: 7.0.1 diff --git a/routes/audit.js b/routes/audit.js index fbf90ac..0293edd 100644 --- a/routes/audit.js +++ b/routes/audit.js @@ -1,6 +1,16 @@ import { Router } from "express"; const router = Router(); +export async function recordAudit(db, entry) { + return db + .collection("auditLogs") + .insertOne({ ...entry, timestamp: new Date() }) + .catch((err) => { + console.error("Failed to write audit entry", err); + throw err; + }); +} + router.get("/", async (req, res) => { const { cursor, search, action } = req.query; const query = {}; @@ -24,11 +34,12 @@ router.get("/", async (req, res) => { .limit(50) .toArray(); - const nextCursor = entries.length === 50 - ? entries[entries.length - 1].timestamp.toISOString() - : null; + const nextCursor = + entries.length === 50 + ? entries[entries.length - 1].timestamp.toISOString() + : null; res.json({ entries, cursor: nextCursor }); }); -export default router; \ No newline at end of file +export default router; diff --git a/routes/doors.js b/routes/doors.js index e8b9fd9..ec11239 100644 --- a/routes/doors.js +++ b/routes/doors.js @@ -1,24 +1,23 @@ import { Router } from "express"; import { doorHeartbeats } from "../state.js"; -import { checkAccess, recordDoorUnlock } from "../access.js"; -import { ObjectId } from "mongodb"; +import { checkAccess } from "../access.js"; +import { recordDoorUnlock } from "./logs.js"; const router = Router(); -function getDoorStatus(doorId){ +function isDoorOffline(doorId) { // If it's been more than 1 minute, we assume something is broken... const lastHeartbeat = doorHeartbeats.get(doorId); - if (lastHeartbeat) { - return { - guess: Date.now() - lastHeartbeat > 1000 * 60 ? "offline" : "online", - lastHeartbeat, - }; - } else { - return { - guess: "offline", - lastHeartbeat: 0, - }; - } + if (!lastHeartbeat) return true; + return Date.now() - lastHeartbeat > 1000 * 60; +} + +function getDoorStatus(doorId) { + const lastHeartbeat = doorHeartbeats.get(doorId); + return { + guess: isDoorOffline(doorId) ? "offline" : "online", + lastHeartbeat: lastHeartbeat || 0, + }; } router.get("/:doorId/status", (req, res) => { @@ -27,9 +26,13 @@ router.get("/:doorId/status", (req, res) => { router.get("/", async (req, res) => { const doors = await req.ctx.db.collection("doors").find({}).toArray(); - const accessResults = req.ctx.authMethod === "oidc" - ? await Promise.all(doors.map((d) => checkAccess(req.ctx.db, req.ctx.userId, String(d._id)))) - : doors.map(() => true); + const accessResults = req.ctx.userId + ? await Promise.all( + doors.map((d) => + checkAccess(req.ctx.db, req.ctx.userId, String(d._id)), + ), + ) + : doors.map(() => false); res.json({ doors: doors.map((door, i) => ({ @@ -41,38 +44,36 @@ router.get("/", async (req, res) => { }); router.post("/:doorId/unlock", async (req, res) => { - if (req.ctx.authMethod === "oidc") { - const granted = await checkAccess( - req.ctx.db, - req.ctx.userId, - req.params.doorId - ); - if (!granted) { - return res.status(403).json({ message: "Access denied" }); - } + if (!req.ctx.userId) { + //auth method should always have a user identity + return res.status(403).json({ message: "Access denied" }); } - if (getDoorStatus(req.params.doorId).guess === "offline") { - return res.status(409).json({ message: "Door is offline" }); - } + const granted = await checkAccess( + req.ctx.db, + req.ctx.userId, + req.params.doorId, + ); - if (req.ctx.authMethod === "oidc") { - const doorId = req.params.doorId; - const doorDoc = await req.ctx.db.collection("doors").findOne({ - $or: [ - { _id: doorId }, - ...(ObjectId.isValid(doorId) ? [{ _id: new ObjectId(doorId) }] : []), - ], - }); + if (!granted) { + return res.status(403).json({ message: "Access denied" }); + } - await recordDoorUnlock(req.ctx.db, { - doorId: req.params.doorId, - doorName: doorDoc?.name, - username: req.ctx.username ?? req.ctx.userId, - name: req.ctx.name, - }); + if (isDoorOffline(req.params.doorId)) { + return res.status(400).json({ message: "Door is offline" }); } + const doorDoc = await req.ctx.db + .collection("doors") + .findOne({ _id: req.params.doorId }); + await recordDoorUnlock(req.ctx.db, { + doorId: req.params.doorId, + doorName: doorDoc?.name, + username: req.ctx.username ?? req.ctx.userId, + name: req.ctx.name, + accessType: req.ctx.authMethod, + }); + req.ctx.mqtt.publish(`gk/${req.params.doorId}/unlock`, ""); res.status(204).send(null); }); diff --git a/routes/keys.js b/routes/keys.js index 57c1321..30ff5fd 100644 --- a/routes/keys.js +++ b/routes/keys.js @@ -1,166 +1,179 @@ import { Router } from "express"; import crypto from "crypto"; import { REALM_NAMES } from "../constants.js"; -import { recordAudit } from "../access.js"; +import { recordAudit } from "./audit.js"; const router = Router(); router.post("/access", async (req, res) => { - const { reason } = req.body; - if (typeof reason != "string" || !reason.trim()) { - return res.status(422).json({ message: "Missing reason field" }); - } - console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); - await recordAudit(req.ctx.db, { - username: req.ctx.username, - name: req.ctx.name, - action: "Viewed Keys", - reason, - }); - res.status(204).send(null); + const { reason } = req.body; + if (typeof reason !== "string" || !reason.trim()) { + return res.status(422).json({ message: "Missing reason field" }); + } + console.log(`access: ${req.ctx.username} viewed keys || reason: ${reason}`); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Viewed Keys", + reason, + }); + res.status(204).send(null); }); router.get("/by-user", async (req, res) => { - if (typeof req.query.userId != "string") { + if (typeof req.query.userId !== "string") { return res.status(422).json({ message: "Missing 'userId' query param" }); } - const keys = await req.ctx.db.collection("keys").find({ userId: req.query.userId }).toArray(); + const keys = await req.ctx.db + .collection("keys") + .find({ userId: req.query.userId }) + .toArray(); res.json(keys); }); // First, PUT /keys with details of user key is for // Receive a keyId back which is our association // Register key using association and send back the now-randomised UID - // with PATCH /keys/:id - - router.put("/", async (req, res) => { - console.log(req.body); - if (typeof req.body.userId != "string") { - res.status(422).json({ - message: "No 'userId' field specified", - }); - return; - } - if (typeof req.body.uid != "string") { - res.status(422).json({ - message: "No 'uid' field specified", - }); - return; - } +// with PATCH /keys/:id - const keys = {}; - for (const name of REALM_NAMES) { - keys[name + "Id"] = crypto.randomBytes(18).toString("hex"); - } - - const insertedKey = await req.ctx.db.collection("keys").insertOne({ - // Make sure it's something at least reasonable... - _id: crypto.randomBytes(18).toString("hex"), - userId: req.body.userId, - uid: req.body.uid, - // Not created yet, so we'll just leave it disabled for now - enabled: false, - - ...keys, +router.put("/", async (req, res) => { + console.log(req.body); + if (typeof req.body.userId != "string") { + res.status(422).json({ + message: "No 'userId' field specified", }); - res.json({ - keyId: insertedKey.insertedId, - uid: req.body.uid, - ...keys, + return; + } + if (typeof req.body.uid != "string") { + res.status(422).json({ + message: "No 'uid' field specified", }); - }); + return; + } - router.patch("/:id", async (req, res) => { - const updates = {}; - for (const key of ["uid", "userId", "enabled"]) { - if (key in req.body) { - updates[key] = req.body[key]; - } - } + const keys = {}; + for (const name of REALM_NAMES) { + keys[name + "Id"] = crypto.randomBytes(18).toString("hex"); + } - const owner = req.body.username ?? "not found"; - console.log(`access: ${req.ctx.username} updated ${owner}'s key ${req.params.id}`); - await req.ctx.db.collection("keys").updateOne( - { - _id: {$eq: req.params.id}, - }, - { - $set: updates, - } - ); - const change = updates.enabled === false ? "Disabled" : updates.enabled === true ? "Enabled" : "updated"; - await recordAudit(req.ctx.db, { - username: req.ctx.username, - name: req.ctx.name, - action: "Updated Keys", - reason: `${change} ${owner}'s key ${req.params.id}`, + const insertedKey = await req.ctx.db.collection("keys").insertOne({ + // Make sure it's something at least reasonable... + _id: crypto.randomBytes(18).toString("hex"), + userId: req.body.userId, + uid: req.body.uid, + type: "physical", + // Not created yet, so we'll just leave it disabled for now + enabled: false, + + ...keys, }); - res.status(204).send(null); + res.json({ + keyId: insertedKey.insertedId, + uid: req.body.uid, + ...keys, }); +}); - router.get("/by-association/:id", async (req, res) => { - const key = await req.ctx.db.collection("keys").findOne({ - $or: REALM_NAMES.map((key) => ({ - [key + "Id"]: { - $eq: req.params.id, - }, - })), - }); - - if (key) { - const data = { - keyId: key._id, - uid: key.uid, - }; - for (const name of REALM_NAMES) { - data[name + "Id"] = key[name + "Id"]; - } - return res.json(data); - } else { - return res.status(404).json({ - message: "No such key!", - }); +router.patch("/:id", async (req, res) => { + const updates = {}; + for (const key of ["uid", "userId", "enabled"]) { + if (key in req.body) { + updates[key] = req.body[key]; } + } + + const owner = req.body.username ?? "not found"; + console.log( + `access: ${req.ctx.username} updated ${owner}'s key ${req.params.id}`, + ); + await req.ctx.db.collection("keys").updateOne( + { + _id: { $eq: req.params.id }, + }, + { + $set: updates, + }, + ); + const change = + updates.enabled === false + ? "Disabled" + : updates.enabled === true + ? "Enabled" + : "updated"; + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Updated Keys", + reason: `${change} ${owner}'s key ${req.params.id}`, }); + res.status(204).send(null); +}); - router.delete("/by-user", async (req, res) => { - if (typeof req.body.userId != "string") { - return res.status(422).json({ - message: "Missing 'userId'", - }); +router.get("/by-association/:id", async (req, res) => { + const key = await req.ctx.db.collection("keys").findOne({ + $or: REALM_NAMES.map((key) => ({ + [key + "Id"]: { + $eq: req.params.id, + }, + })), + }); + + if (key) { + const data = { + keyId: key._id, + uid: key.uid, + }; + for (const name of REALM_NAMES) { + data[name + "Id"] = key[name + "Id"]; } - const results = await req.ctx.db.collection("keys").deleteMany({ - userId: {$eq: req.body.userId}, + return res.json(data); + } else { + return res.status(404).json({ + message: "No such key!", }); - if (results.deletedCount) { - return res.status(204).send(null); - } else { - return res.status(404).json({ - message: "No keys attached to user!", - }); - } + } +}); + +router.delete("/by-user", async (req, res) => { + if (typeof req.body.userId != "string") { + return res.status(422).json({ + message: "Missing 'userId'", + }); + } + const results = await req.ctx.db.collection("keys").deleteMany({ + userId: { $eq: req.body.userId }, }); + if (results.deletedCount) { + return res.status(204).send(null); + } else { + return res.status(404).json({ + message: "No keys attached to user!", + }); + } +}); - router.delete("/:keyId", async (req, res) => { - const owner = req.body?.username ?? "unknown"; +router.delete("/:keyId", async (req, res) => { + const owner = req.body?.username ?? "unknown"; - const results = await req.ctx.db.collection("keys").deleteOne({ - _id: {$eq: req.params.keyId}, - }); - if (results.deletedCount) { - console.log(`keys: ${req.ctx.username} deleted ${owner}'s key ${req.params.keyId}`); - await recordAudit(req.ctx.db,{ - username: req.ctx.username, - name: req.ctx.name, - action: "Deleted Keys", - reason: `deleted ${owner}'s key ${req.params.keyId}`, - }); - return res.status(204).send(null); - } else { - return res.status(404).json({ - message: "No keys attached to user!", - }); - } + const results = await req.ctx.db.collection("keys").deleteOne({ + _id: { $eq: req.params.keyId }, }); + if (results.deletedCount) { + console.log( + `keys: ${req.ctx.username} deleted ${owner}'s key ${req.params.keyId}`, + ); + await recordAudit(req.ctx.db, { + username: req.ctx.username, + name: req.ctx.name, + action: "Deleted Keys", + reason: `deleted ${owner}'s key ${req.params.keyId}`, + }); + return res.status(204).send(null); + } else { + return res.status(404).json({ + message: "No keys attached to user!", + }); + } +}); - export default router; +export default router; diff --git a/routes/logs.js b/routes/logs.js index 588274c..ca2939f 100644 --- a/routes/logs.js +++ b/routes/logs.js @@ -1,8 +1,42 @@ import { Router } from "express"; -import { recordAudit } from "../access.js"; +import { recordAudit } from "./audit.js"; const router = Router(); +export async function recordDoorUnlock( + db, + { + doorId, + doorName, + username, + name, + accessType, + doorsId = null, + keyId = null, + uid = null, + granted = true, + }, +) { + return db + .collection("accessLogs") + .insertOne({ + timestamp: new Date(), + door: doorId, + doorName, + username, + name, + doorsId, + keyId, + uid, + granted, + accessType, + }) + .catch((err) => { + console.error("Failed to write accessLogs", err); + throw err; + }); +} + router.post("/access", async (req, res) => { const { reason } = req.body; if (typeof reason != "string" || !reason.trim()) { @@ -29,8 +63,8 @@ router.get("/", async (req, res) => { if (since || until || cursor) { query.timestamp = {}; if (cursor) query.timestamp.$lt = new Date(cursor); - if (since) query.timestamp.$gte = new Date(since); - if (until) query.timestamp.$lte = new Date(until); + if (since) query.timestamp.$gte = new Date(since); + if (until) query.timestamp.$lte = new Date(until); } const ands = []; @@ -39,7 +73,9 @@ router.get("/", async (req, res) => { } if (search) { const re = new RegExp(search, "i"); - ands.push({ $or: [{ doorName: re }, { door: re }, { username: re }, { name: re }] }); + ands.push({ + $or: [{ doorName: re }, { door: re }, { username: re }, { name: re }], + }); } if (ands.length) query.$and = ands; @@ -53,11 +89,10 @@ router.get("/", async (req, res) => { .limit(50) .toArray(); - const nextCursor = logs.length === 50 - ? logs[logs.length - 1].timestamp.toISOString() - : null; + const nextCursor = + logs.length === 50 ? logs[logs.length - 1].timestamp.toISOString() : null; res.json({ logs, cursor: nextCursor }); }); -export default router; \ No newline at end of file +export default router; diff --git a/routes/memberProjects.js b/routes/memberProjects.js index 7ee04a4..c52f289 100644 --- a/routes/memberProjects.js +++ b/routes/memberProjects.js @@ -15,29 +15,29 @@ const ARRAYS = new Set([ router.get("/by-key/:associationId", async (req, res) => { const key = await req.ctx.db.collection("keys").findOne({ - [req.associationType]: {$eq: req.params.associationId}, + [req.associationType]: { $eq: req.params.associationId }, enabled: { $eq: true }, }); if (!key) { - res.status(404).json({message: "Not found"}); + res.status(404).json({ message: "Not found" }); return; } const userDocument = await req.ctx.db.collection("users").findOne({ - id: {$eq: key.userId}, - disabled: {$ne: true}, + id: { $eq: key.userId }, + disabled: { $ne: true }, }); if (!userDocument) { - res.status(404).json({message: "User not found or disabled"}); + res.status(404).json({ message: "User not found or disabled" }); return; } let user; try { user = await searchOne(USER_BASE, `(ipaUniqueID=${key.userId})`); - } catch (err) { - res.status(500).json({message: "Internal server error"}); + } catch { + res.status(500).json({ message: "Internal server error" }); return; } diff --git a/routes/mobile.js b/routes/mobile.js index 8f09aa1..268fb28 100644 --- a/routes/mobile.js +++ b/routes/mobile.js @@ -8,7 +8,7 @@ const router = Router(); router.use(oidcAuth(PROVISION_SCOPE)); router.get("/provision", async (req, res) => { - const stem = { userId: req.ctx.userId, mobile: true }; + const stem = { userId: req.ctx.userId, type: "mobile" }; let key = await req.ctx.db.collection("keys").findOne(stem); if (!key) { key = { diff --git a/routes/users.js b/routes/users.js index 4d34d8d..4d1be21 100644 --- a/routes/users.js +++ b/routes/users.js @@ -8,7 +8,7 @@ const USER_BASE = "cn=users,cn=accounts,dc=csh,dc=rit,dc=edu"; const USER_ATTRS = ["memberOf", "ipaUniqueID", "nsAccountLock"]; function validId(id) { - return id.match(/^[a-zA-Z0-9\-]+$/m); + return id.match(/^[a-zA-Z0-9]+$/m); } router.get("/:id", async (req, res) => { @@ -18,13 +18,19 @@ router.get("/:id", async (req, res) => { }); return; } - let user = await req.ctx.db.collection("users").findOne({id: req.params.id}); + let user = await req.ctx.db + .collection("users") + .findOne({ id: req.params.id }); if (!user) { let userData = null; try { - userData = await searchOne(USER_BASE, `(ipaUniqueID=${req.body.id})`, USER_ATTRS); - } catch (err) { - res.status(404).json({message: "Not found"}); + userData = await searchOne( + USER_BASE, + `(ipaUniqueID=${req.body.id})`, + USER_ATTRS, + ); + } catch { + res.status(404).json({ message: "Not found" }); } user = await syncUser(req.ctx.db, userData); } @@ -45,7 +51,11 @@ router.get("/uuid-by-uid/:uid", async (req, res) => { } console.log("uuid by uid:", req.params.uid); try { - const user = await searchOne(USER_BASE, `(uid=${req.params.uid})`, USER_ATTRS); + const user = await searchOne( + USER_BASE, + `(uid=${req.params.uid})`, + USER_ATTRS, + ); // Ensure we're updated on DB-side const userDocument = await syncUser(req.ctx.db, user); console.log("Got user", userDocument); @@ -53,10 +63,10 @@ router.get("/uuid-by-uid/:uid", async (req, res) => { return res.json(userDocument); } catch (err) { if (err.message == "User not found!") { - return res.status(404).json({message: "User not found"}); + return res.status(404).json({ message: "User not found" }); } else { console.error("Error looking up user", err); - res.status(500).json({error: err.stack}); + res.status(500).json({ error: err.stack }); throw err; } } diff --git a/server.js b/server.js index 37f7a09..ca22aed 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ import { statsd } from "./metrics.js"; import express from "express"; import mqtt from "mqtt"; -import { MongoClient, ObjectId } from "mongodb"; +import { MongoClient } from "mongodb"; import bodyParser from "body-parser"; import morgan from "morgan"; @@ -11,6 +11,7 @@ import auth from "./auth.js"; import { hybridAuth } from "./middleware/hybridAuth.js"; import { checkAccess } from "./access.js"; import { requireGroup } from "./middleware/oidc.js"; +import { recordDoorUnlock } from "./routes/logs.js"; // API routes import memberProjects from "./routes/memberProjects.js"; @@ -44,12 +45,14 @@ connectionPromise.then(async () => { const db = mongoClient.db("gatekeeper"); await Promise.all([ - db.collection("users").createIndex("id", {unique: true}), - db.collection("keys").createIndex("doorsId", {unique: true}), - db.collection("keys").createIndex("drinkId", {unique: true}), - db.collection("keys").createIndex("memberProjectsId", {unique: true}), + db.collection("users").createIndex("id", { unique: true }), + db.collection("keys").createIndex("doorsId", { unique: true }), + db.collection("keys").createIndex("drinkId", { unique: true }), + db.collection("keys").createIndex("memberProjectsId", { unique: true }), db.collection("accessLogs").createIndex({ timestamp: -1 }), - db.collection("accessLogs").createIndex({ timestamp: 1 }, { expireAfterSeconds: 15778476 }), + db + .collection("accessLogs") + .createIndex({ timestamp: 1 }, { expireAfterSeconds: 15778476 }), ]); async function scheduledTasks() { @@ -64,7 +67,7 @@ connectionPromise.then(async () => { } else { const backoff = Math.floor(Math.random() * 1000 * 60 * 60); console.log( - `Production. Running our work tasks in ${backoff / 1000 / 60} minutes` + `Production. Running our work tasks in ${backoff / 1000 / 60} minutes`, ); setTimeout(scheduledTasks, backoff); } @@ -86,14 +89,20 @@ connectionPromise.then(async () => { }); const app = express(); - app.listen(process.env.GK_HTTP_PORT || 3000, '0.0.0.0'); + app.listen(process.env.GK_HTTP_PORT || 3000, "0.0.0.0"); app.use( - morgan(":method :url :status :res[content-length] - :response-time ms") + morgan(":method :url :status :res[content-length] - :response-time ms"), ); app.use((req, res, next) => { res.setHeader("Access-Control-Allow-Origin", "*"); - res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization"); + res.setHeader( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, OPTIONS, PATCH", + ); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, Authorization", + ); if (req.method === "OPTIONS") return res.sendStatus(204); next(); }); @@ -113,7 +122,7 @@ connectionPromise.then(async () => { req.associationType = "memberProjectsId"; next(); }, - memberProjects + memberProjects, ); // Make life easier for drink admins for now... app.use( @@ -123,7 +132,7 @@ connectionPromise.then(async () => { req.associationType = "drinkId"; next(); }, - memberProjects + memberProjects, ); app.use("/doors", hybridAuth("admin"), doors); app.use("/admin/keys", hybridAuth("admin"), requireGroup("rtp"), keys); @@ -134,7 +143,9 @@ connectionPromise.then(async () => { client.on("connect", async () => { console.log("Connected to MQTT broker!"); - const doors = await db.collection("doors").find({}, {projection: {_id: 1}}); + const doors = await db + .collection("doors") + .find({}, { projection: { _id: 1 } }); for await (const door of doors) { console.log("Subscribing to door", door._id); const prefix = `gk/${door._id}/`; @@ -149,7 +160,7 @@ connectionPromise.then(async () => { let payload; try { payload = JSON.parse(message.toString("utf8")); - } catch (err) { + } catch { console.error("Got an invalid packet!", topic, message.toString("utf8")); return; } @@ -157,8 +168,8 @@ connectionPromise.then(async () => { if (topic.endsWith("/access_requested")) { const doorId = topic.slice(3, -17); const key = await db.collection("keys").findOne({ - doorsId: {$eq: payload.association}, - enabled: {$eq: true}, + doorsId: { $eq: payload.association }, + enabled: { $eq: true }, }); // Doesn't exist?? if (!key) return; @@ -173,30 +184,19 @@ connectionPromise.then(async () => { return {}; }) : Promise.resolve({}), - db.collection("doors").findOne({ - $or: [ - { _id: doorId }, - ...(ObjectId.isValid(doorId) - ? [{ _id: new ObjectId(doorId) }] - : []), - ], - }), + db.collection("doors").findOne({ _id: doorId }), checkAccess(db, key.userId, doorId), ]); const user = userData?.user || {}; const username = user.uid || null; const name = user.cn || null; - // Resolve door name const doorName = doorDoc?.name || null; - - //timestamps (DUHHH?) - const timestamp = new Date(); + const accessType = key.type; // Structured log - const logEntry = { - timestamp, - door: doorId, + await recordDoorUnlock(db, { + doorId, doorName, username, name, @@ -204,28 +204,14 @@ connectionPromise.then(async () => { keyId: key._id, uid: key.uid ?? null, granted: !!granted, - }; - - console.log(logEntry); - db.collection("accessLogs").insertOne(logEntry).catch((err) => { - console.error("Failed to insert into DB", err); + accessType, + }).catch((err) => { + console.error("Failed to insert into accessLogs", err); }); - - - if (granted) { - console.log( - `[${timestamp}] ${name} (${username}) is unlocking ${doorName || doorId}` - ); - client.publish(`gk/${doorId}/unlock`); - } else { - console.log( - `[${timestamp}] Attempted unlock of ${doorName || doorId} by ${name} (${username})! Not allowed...` - ); - } } else if (topic.endsWith("/heartbeat")) { const doorId = topic.slice(3, -10); doorHeartbeats.set(doorId, Date.now()); - statsd.increment('door.heartbeat', { door: doorId }); + statsd.increment("door.heartbeat", { door: doorId }); console.log(`ACKing a heartbeat from ${doorId}!`); } }); diff --git a/sync.js b/sync.js index 091548a..8ef9a23 100644 --- a/sync.js +++ b/sync.js @@ -4,10 +4,10 @@ export async function syncUser(db, user) { const id = user.attributes .find((attribute) => attribute.type == "ipaUniqueID") ._vals[0].toString("utf8"); - let memberOf = user.attributes - .find((attribute) => attribute.type == "memberOf"); - if (!memberOf) - return; + let memberOf = user.attributes.find( + (attribute) => attribute.type == "memberOf", + ); + if (!memberOf) return; const document = { groups: memberOf._vals.map((value) => value.toString("utf8")), disabled: @@ -25,9 +25,9 @@ export async function syncUser(db, user) { { $set: document, }, - {upsert: true} + { upsert: true }, ); - return {...document, id}; + return { ...document, id }; } export async function syncUsers(db) { @@ -41,7 +41,7 @@ export async function syncUsers(db) { timeLimit: 60 * 30, // 30 minutes attributes: ["memberOf", "ipaUniqueID", "nsAccountLock"], sizeLimit: 0, // unlimited - } + }, ); const promises = []; diff --git a/util.js b/util.js index 4e68804..1fe3725 100644 --- a/util.js +++ b/util.js @@ -52,7 +52,7 @@ export function iterableSearch(base, options) { } if (!foundEnd) { // Waits for next chunk to be dispatched from the system - await new Promise((resolve, reject) => { + await new Promise((resolve) => { // Safety check, make sure we don't deadlock // (even though this call should be immediate) if (entries.length) {