@codixus/server 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  ErrorCodes: () => import_shared4.ErrorCodes,
36
36
  Model: () => Model,
37
37
  Query: () => Query,
38
+ RestErrorCode: () => RestErrorCode,
38
39
  SubCollection: () => SubCollection,
39
40
  model: () => model,
40
41
  validate: () => validate
@@ -180,7 +181,7 @@ var BanService = class {
180
181
  };
181
182
 
182
183
  // src/auth/guard.ts
183
- function createGuard(jwt) {
184
+ function createGuard(jwt, banService) {
184
185
  return function guard(options = {}) {
185
186
  return async (req, res, next) => {
186
187
  const authHeader = req.headers.authorization;
@@ -195,9 +196,14 @@ function createGuard(jwt) {
195
196
  const token = authHeader.substring(7);
196
197
  try {
197
198
  const payload = await jwt.verify(token);
199
+ const deviceId = payload.deviceId;
200
+ if (banService && await banService.isBanned(deviceId)) {
201
+ res.status(403).json({ success: false, error: "DEVICE_BANNED" });
202
+ return;
203
+ }
198
204
  req.user = {
199
205
  uid: payload.sub,
200
- deviceId: payload.deviceId
206
+ deviceId
201
207
  };
202
208
  next();
203
209
  } catch (error) {
@@ -798,12 +804,12 @@ function createRateLimit(config = {}) {
798
804
  }
799
805
  entry.count++;
800
806
  const remaining = Math.max(0, maxRequests - entry.count);
801
- res.setHeader("X-RateLimit-Limit", maxRequests);
802
- res.setHeader("X-RateLimit-Remaining", remaining);
803
- res.setHeader("X-RateLimit-Reset", Math.ceil(entry.resetAt / 1e3));
807
+ res.setHeader("X-RateLimit-Limit", String(maxRequests));
808
+ res.setHeader("X-RateLimit-Remaining", String(remaining));
809
+ res.setHeader("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1e3)));
804
810
  if (entry.count > maxRequests) {
805
811
  const retryAfter = Math.ceil((entry.resetAt - now) / 1e3);
806
- res.setHeader("Retry-After", retryAfter);
812
+ res.setHeader("Retry-After", String(retryAfter));
807
813
  res.status(429).json({
808
814
  success: false,
809
815
  error: message,
@@ -823,6 +829,31 @@ function defaultKeyGenerator(req) {
823
829
  // src/db/rest.ts
824
830
  var import_express2 = require("express");
825
831
  var import_node_crypto4 = __toESM(require("crypto"), 1);
832
+ var RestErrorCode = {
833
+ CreateDisabled: "CREATE_DISABLED",
834
+ UpdateDisabled: "UPDATE_DISABLED",
835
+ DeleteDisabled: "DELETE_DISABLED",
836
+ ReadForbidden: "READ_FORBIDDEN",
837
+ WriteForbidden: "WRITE_FORBIDDEN",
838
+ DeleteForbidden: "DELETE_FORBIDDEN",
839
+ NotFound: "NOT_FOUND",
840
+ InvalidFilter: "INVALID_FILTER",
841
+ InvalidSort: "INVALID_SORT",
842
+ InvalidCursor: "INVALID_CURSOR",
843
+ InvalidUpdate: "INVALID_UPDATE",
844
+ InvalidBody: "INVALID_BODY",
845
+ AuthRequired: "AUTH_REQUIRED"
846
+ };
847
+ var DEFAULT_ALLOWED_OPS = /* @__PURE__ */ new Set([
848
+ "$eq",
849
+ "$ne",
850
+ "$in",
851
+ "$nin",
852
+ "$gt",
853
+ "$gte",
854
+ "$lt",
855
+ "$lte"
856
+ ]);
826
857
  var SAFE_UPDATE_OPS = /* @__PURE__ */ new Set([
827
858
  "$set",
828
859
  "$unset",
@@ -837,41 +868,199 @@ var SAFE_UPDATE_OPS = /* @__PURE__ */ new Set([
837
868
  "$currentDate",
838
869
  "$setOnInsert"
839
870
  ]);
840
- var BLOCKED_OPS = /* @__PURE__ */ new Set([
841
- "$where",
842
- "$function",
843
- "$accumulator",
844
- "$expr",
845
- "$jsonSchema",
846
- "$comment"
847
- ]);
848
- function sanitizeObject(obj, depth = 0) {
849
- if (depth > 10) throw new Error("Object too deeply nested");
850
- if (obj === null || obj === void 0) return {};
851
- if (typeof obj !== "object" || Array.isArray(obj)) throw new Error("Expected object");
871
+ var RestError = class extends Error {
872
+ constructor(code, message) {
873
+ super(message);
874
+ this.code = code;
875
+ }
876
+ };
877
+ function param(req, name) {
878
+ const val = req.params[name];
879
+ return Array.isArray(val) ? val[0] : val ?? "";
880
+ }
881
+ function fail(res, status, code, error) {
882
+ res.status(status).json({ success: false, error, code });
883
+ }
884
+ function handleCaught(res, err) {
885
+ if (err instanceof RestError) {
886
+ const status = err.code === RestErrorCode.AuthRequired ? 401 : 400;
887
+ fail(res, status, err.code, err.message);
888
+ return;
889
+ }
890
+ const message = err instanceof Error ? err.message : String(err);
891
+ fail(res, 400, RestErrorCode.InvalidBody, message);
892
+ }
893
+ function buildFilterValidator(queryableFields, allowedOps) {
894
+ return function sanitizeFilter(obj, depth = 0) {
895
+ if (depth > 10)
896
+ throw new RestError(RestErrorCode.InvalidFilter, "Filter too deeply nested");
897
+ if (obj === null || obj === void 0) return {};
898
+ if (typeof obj !== "object" || Array.isArray(obj))
899
+ throw new RestError(RestErrorCode.InvalidFilter, "Filter must be an object");
900
+ const result = {};
901
+ for (const [key, value] of Object.entries(obj)) {
902
+ if (key.startsWith("$")) {
903
+ if (!allowedOps.has(key)) {
904
+ throw new RestError(
905
+ RestErrorCode.InvalidFilter,
906
+ `Operator "${key}" is not allowed`
907
+ );
908
+ }
909
+ result[key] = sanitizeFilterValue(value, allowedOps, depth + 1);
910
+ } else {
911
+ if (queryableFields && !queryableFields.has(key)) {
912
+ throw new RestError(
913
+ RestErrorCode.InvalidFilter,
914
+ `Field "${key}" is not queryable`
915
+ );
916
+ }
917
+ result[key] = sanitizeFilterValue(value, allowedOps, depth + 1);
918
+ }
919
+ }
920
+ return result;
921
+ };
922
+ }
923
+ function sanitizeFilterValue(value, allowedOps, depth) {
924
+ if (depth > 10)
925
+ throw new RestError(RestErrorCode.InvalidFilter, "Filter too deeply nested");
926
+ if (value === null) return null;
927
+ if (Array.isArray(value)) {
928
+ return value.map((v) => sanitizeFilterValue(v, allowedOps, depth + 1));
929
+ }
930
+ if (value instanceof Date) return value;
931
+ if (typeof value !== "object") return value;
852
932
  const result = {};
853
- for (const [key, value] of Object.entries(obj)) {
854
- if (BLOCKED_OPS.has(key)) throw new Error(`Operator "${key}" is not allowed`);
855
- if (value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) {
856
- result[key] = sanitizeObject(value, depth + 1);
857
- } else {
858
- result[key] = value;
933
+ for (const [k, v] of Object.entries(value)) {
934
+ if (k.startsWith("$") && !allowedOps.has(k)) {
935
+ throw new RestError(
936
+ RestErrorCode.InvalidFilter,
937
+ `Operator "${k}" is not allowed`
938
+ );
859
939
  }
940
+ result[k] = sanitizeFilterValue(v, allowedOps, depth + 1);
860
941
  }
861
942
  return result;
862
943
  }
863
- function sanitizeUpdate(obj) {
864
- if (!obj || typeof obj !== "object") throw new Error("Update must be an object");
865
- for (const key of Object.keys(obj)) {
866
- if (key.startsWith("$") && !SAFE_UPDATE_OPS.has(key)) {
867
- throw new Error(`Update operator "${key}" is not allowed`);
944
+ function assertNoNestedOperators(value, path, depth = 0) {
945
+ if (depth > 10) return;
946
+ if (value === null || typeof value !== "object") return;
947
+ if (value instanceof Date) return;
948
+ if (Array.isArray(value)) {
949
+ for (const v of value) assertNoNestedOperators(v, path, depth + 1);
950
+ return;
951
+ }
952
+ for (const [k, v] of Object.entries(value)) {
953
+ if (k.startsWith("$")) {
954
+ throw new RestError(
955
+ RestErrorCode.InvalidUpdate,
956
+ `Nested operator "${k}" is not allowed inside update value for "${path}"`
957
+ );
868
958
  }
959
+ assertNoNestedOperators(v, path, depth + 1);
869
960
  }
870
- return sanitizeObject(obj);
871
961
  }
872
- function param(req, name) {
873
- const val = req.params[name];
874
- return Array.isArray(val) ? val[0] : val ?? "";
962
+ function sanitizeUpdate(obj, writableFields) {
963
+ if (!obj || typeof obj !== "object" || Array.isArray(obj))
964
+ throw new RestError(RestErrorCode.InvalidUpdate, "Update must be an object");
965
+ const input = obj;
966
+ const hasOperators = Object.keys(input).some((k) => k.startsWith("$"));
967
+ const wrapped = hasOperators ? input : { $set: input };
968
+ const result = {};
969
+ for (const [op, fields] of Object.entries(wrapped)) {
970
+ if (!op.startsWith("$")) {
971
+ throw new RestError(
972
+ RestErrorCode.InvalidUpdate,
973
+ "Update must use $-prefixed operators"
974
+ );
975
+ }
976
+ if (!SAFE_UPDATE_OPS.has(op)) {
977
+ throw new RestError(
978
+ RestErrorCode.InvalidUpdate,
979
+ `Update operator "${op}" is not allowed`
980
+ );
981
+ }
982
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
983
+ throw new RestError(
984
+ RestErrorCode.InvalidUpdate,
985
+ `Operator "${op}" requires an object`
986
+ );
987
+ }
988
+ const validatedFields = {};
989
+ for (const [field, value] of Object.entries(
990
+ fields
991
+ )) {
992
+ if (field.startsWith("$")) {
993
+ throw new RestError(
994
+ RestErrorCode.InvalidUpdate,
995
+ `Nested operator "${field}" is not allowed`
996
+ );
997
+ }
998
+ if (writableFields) {
999
+ if (!writableFields.has(field)) {
1000
+ throw new RestError(
1001
+ RestErrorCode.InvalidUpdate,
1002
+ `Field "${field}" is not writable`
1003
+ );
1004
+ }
1005
+ }
1006
+ if (!writableFields && field.includes(".")) {
1007
+ throw new RestError(
1008
+ RestErrorCode.InvalidUpdate,
1009
+ `Dotted-path writes require explicit writableFields`
1010
+ );
1011
+ }
1012
+ assertNoNestedOperators(value, field);
1013
+ validatedFields[field] = value;
1014
+ }
1015
+ result[op] = validatedFields;
1016
+ }
1017
+ return result;
1018
+ }
1019
+ function projectDoc(doc, publicFields) {
1020
+ if (!doc || !publicFields) return doc;
1021
+ const out = {};
1022
+ for (const f of publicFields) {
1023
+ if (f in doc) out[f] = doc[f];
1024
+ }
1025
+ if ("_id" in doc) out._id = doc._id;
1026
+ return out;
1027
+ }
1028
+ function encodeCursor(key, payload) {
1029
+ const iv = import_node_crypto4.default.randomBytes(12);
1030
+ const cipher = import_node_crypto4.default.createCipheriv("aes-256-gcm", key, iv);
1031
+ const plaintext = Buffer.from(JSON.stringify(payload), "utf8");
1032
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
1033
+ const tag = cipher.getAuthTag();
1034
+ return [
1035
+ iv.toString("base64url"),
1036
+ tag.toString("base64url"),
1037
+ ciphertext.toString("base64url")
1038
+ ].join(".");
1039
+ }
1040
+ function decodeCursor(key, token) {
1041
+ const parts = token.split(".");
1042
+ if (parts.length !== 3)
1043
+ throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor");
1044
+ const [ivB64, tagB64, ctB64] = parts;
1045
+ let iv, tag, ct;
1046
+ try {
1047
+ iv = Buffer.from(ivB64, "base64url");
1048
+ tag = Buffer.from(tagB64, "base64url");
1049
+ ct = Buffer.from(ctB64, "base64url");
1050
+ } catch {
1051
+ throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor encoding");
1052
+ }
1053
+ if (iv.length !== 12 || tag.length !== 16) {
1054
+ throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor");
1055
+ }
1056
+ try {
1057
+ const decipher = import_node_crypto4.default.createDecipheriv("aes-256-gcm", key, iv);
1058
+ decipher.setAuthTag(tag);
1059
+ const plaintext = Buffer.concat([decipher.update(ct), decipher.final()]);
1060
+ return JSON.parse(plaintext.toString("utf8"));
1061
+ } catch {
1062
+ throw new RestError(RestErrorCode.InvalidCursor, "Invalid cursor signature");
1063
+ }
875
1064
  }
876
1065
  function createRestRouter(model2, options = {}) {
877
1066
  const {
@@ -880,8 +1069,64 @@ function createRestRouter(model2, options = {}) {
880
1069
  allowDelete = true,
881
1070
  maxLimit = 100,
882
1071
  defaultLimit = 20,
883
- defaultSort
1072
+ defaultSort,
1073
+ ownershipField,
1074
+ queryableFields,
1075
+ sortableFields,
1076
+ writableFields,
1077
+ publicFields,
1078
+ allowedOperators,
1079
+ allowRead,
1080
+ allowWrite
884
1081
  } = options;
1082
+ const queryableSet = queryableFields ? new Set(queryableFields) : null;
1083
+ const sortableSet = sortableFields ? new Set(sortableFields) : null;
1084
+ const writableSet = writableFields ? new Set(writableFields) : null;
1085
+ const publicSet = publicFields ? new Set(publicFields) : null;
1086
+ const opsSet = allowedOperators ? new Set(allowedOperators) : DEFAULT_ALLOWED_OPS;
1087
+ const sanitizeFilter = buildFilterValidator(queryableSet, opsSet);
1088
+ const cursorKey = import_node_crypto4.default.randomBytes(32);
1089
+ function scopedFilter(req, filter) {
1090
+ if (!ownershipField) return filter;
1091
+ const owner = req.user?.deviceId;
1092
+ if (!owner)
1093
+ throw new RestError(
1094
+ RestErrorCode.AuthRequired,
1095
+ "Authentication required"
1096
+ );
1097
+ if (ownershipField in filter) {
1098
+ throw new RestError(
1099
+ RestErrorCode.InvalidFilter,
1100
+ `Field "${ownershipField}" cannot be used in filter (auto-scoped to caller)`
1101
+ );
1102
+ }
1103
+ return { ...filter, [ownershipField]: owner };
1104
+ }
1105
+ function validateSort(sort) {
1106
+ if (!sort) return void 0;
1107
+ if (typeof sort !== "object" || Array.isArray(sort))
1108
+ throw new RestError(RestErrorCode.InvalidSort, "Sort must be an object");
1109
+ const out = {};
1110
+ for (const [field, direction] of Object.entries(sort)) {
1111
+ if (field.startsWith("$"))
1112
+ throw new RestError(
1113
+ RestErrorCode.InvalidSort,
1114
+ `Invalid sort field "${field}"`
1115
+ );
1116
+ if (sortableSet && !sortableSet.has(field))
1117
+ throw new RestError(
1118
+ RestErrorCode.InvalidSort,
1119
+ `Field "${field}" is not sortable`
1120
+ );
1121
+ if (direction !== 1 && direction !== -1)
1122
+ throw new RestError(
1123
+ RestErrorCode.InvalidSort,
1124
+ `Sort direction for "${field}" must be 1 or -1`
1125
+ );
1126
+ out[field] = direction;
1127
+ }
1128
+ return out;
1129
+ }
885
1130
  const router = (0, import_express2.Router)();
886
1131
  router.get("/", async (req, res) => {
887
1132
  try {
@@ -889,107 +1134,172 @@ function createRestRouter(model2, options = {}) {
889
1134
  const sortStr = req.query.sort;
890
1135
  const limitStr = req.query.limit;
891
1136
  const skipStr = req.query.skip;
892
- const cursor = req.query.cursor;
893
- let filter = {};
894
- if (filterStr) {
895
- filter = sanitizeObject(JSON.parse(filterStr));
896
- }
897
- const sort = sortStr ? JSON.parse(sortStr) : defaultSort;
1137
+ const cursorToken = req.query.cursor;
1138
+ const rawFilter = filterStr ? JSON.parse(filterStr) : {};
1139
+ let filter = sanitizeFilter(rawFilter);
1140
+ filter = scopedFilter(req, filter);
1141
+ const rawSort = sortStr ? JSON.parse(sortStr) : defaultSort;
1142
+ const sort = validateSort(rawSort);
898
1143
  const limit = Math.min(Number(limitStr) || defaultLimit, maxLimit);
899
1144
  const skip = Number(skipStr) || 0;
900
1145
  let query = model2.find(filter);
901
1146
  if (sort) query = query.sort(sort);
902
- if (cursor && sort) {
903
- const sortField = Object.keys(sort)[0];
904
- const cursorValue = Number.isNaN(Number(cursor)) ? cursor : Number(cursor);
905
- query = query.startAfter({ [sortField]: cursorValue });
1147
+ if (cursorToken && sort) {
1148
+ const decoded = decodeCursor(cursorKey, cursorToken);
1149
+ query = query.startAfter(decoded);
906
1150
  }
907
1151
  if (skip) query = query.skip(skip);
908
1152
  query = query.limit(limit);
909
1153
  const docs = await query.exec();
910
1154
  let nextCursor = null;
911
1155
  if (docs.length === limit && sort) {
912
- const sortField = Object.keys(sort)[0];
913
- nextCursor = docs[docs.length - 1]?.[sortField] ?? null;
1156
+ const last = docs[docs.length - 1];
1157
+ const cursorPayload = {};
1158
+ for (const field of Object.keys(sort)) {
1159
+ cursorPayload[field] = last[field];
1160
+ }
1161
+ nextCursor = encodeCursor(cursorKey, cursorPayload);
914
1162
  }
915
- res.json({ success: true, data: docs, nextCursor });
1163
+ const projected = publicSet ? docs.map((d) => projectDoc(d, publicSet)) : docs;
1164
+ res.json({ success: true, data: projected, nextCursor });
916
1165
  } catch (error) {
917
- res.status(400).json({ success: false, error: error.message });
1166
+ handleCaught(res, error);
918
1167
  }
919
1168
  });
920
1169
  router.get("/:id", async (req, res) => {
921
- const id = param(req, "id");
922
- const doc = await model2.findById(id);
923
- if (!doc) {
924
- res.status(404).json({ success: false, error: "Not found" });
925
- return;
1170
+ try {
1171
+ const id = param(req, "id");
1172
+ const doc = await model2.findById(id);
1173
+ if (!doc) {
1174
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
1175
+ return;
1176
+ }
1177
+ if (ownershipField) {
1178
+ const owner = req.user?.deviceId;
1179
+ if (!owner || doc[ownershipField] !== owner) {
1180
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
1181
+ return;
1182
+ }
1183
+ }
1184
+ if (allowRead && !await allowRead(req, doc)) {
1185
+ fail(res, 403, RestErrorCode.ReadForbidden, "Read not allowed");
1186
+ return;
1187
+ }
1188
+ res.json({ success: true, data: projectDoc(doc, publicSet) });
1189
+ } catch (error) {
1190
+ handleCaught(res, error);
926
1191
  }
927
- res.json({ success: true, data: doc });
928
1192
  });
929
1193
  router.post("/", async (req, res) => {
930
1194
  if (!allowCreate) {
931
- res.status(403).json({ success: false, error: "Create not allowed" });
1195
+ fail(res, 403, RestErrorCode.CreateDisabled, "Create not allowed");
932
1196
  return;
933
1197
  }
934
1198
  try {
935
- const data = req.body;
1199
+ const data = { ...req.body };
1200
+ if (ownershipField) {
1201
+ const owner = req.user?.deviceId;
1202
+ if (!owner) {
1203
+ fail(res, 401, RestErrorCode.AuthRequired, "Authentication required");
1204
+ return;
1205
+ }
1206
+ data[ownershipField] = owner;
1207
+ }
936
1208
  if (!data._id) data._id = import_node_crypto4.default.randomUUID();
937
1209
  const doc = await model2.create(data);
938
- res.status(201).json({ success: true, data: doc });
1210
+ res.status(201).json({ success: true, data: projectDoc(doc, publicSet) });
939
1211
  } catch (error) {
940
- res.status(400).json({ success: false, error: error.message });
1212
+ handleCaught(res, error);
941
1213
  }
942
1214
  });
943
1215
  router.post("/count", async (req, res) => {
944
1216
  try {
945
- const filter = req.body.filter ? sanitizeObject(req.body.filter) : {};
1217
+ const rawFilter = req.body.filter ?? {};
1218
+ let filter = sanitizeFilter(rawFilter);
1219
+ filter = scopedFilter(req, filter);
946
1220
  const count = await model2.count(filter);
947
1221
  res.json({ success: true, count });
948
1222
  } catch (error) {
949
- res.status(400).json({ success: false, error: error.message });
1223
+ handleCaught(res, error);
950
1224
  }
951
1225
  });
952
1226
  router.patch("/:id", async (req, res) => {
953
1227
  if (!allowUpdate) {
954
- res.status(403).json({ success: false, error: "Update not allowed" });
1228
+ fail(res, 403, RestErrorCode.UpdateDisabled, "Update not allowed");
955
1229
  return;
956
1230
  }
957
1231
  const id = param(req, "id");
958
1232
  try {
959
- const update = sanitizeUpdate(req.body.update ?? req.body);
1233
+ const update = sanitizeUpdate(req.body.update ?? req.body, writableSet);
1234
+ if (ownershipField || allowWrite) {
1235
+ const existing = await model2.findById(id);
1236
+ if (!existing) {
1237
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
1238
+ return;
1239
+ }
1240
+ if (ownershipField) {
1241
+ const owner = req.user?.deviceId;
1242
+ if (!owner || existing[ownershipField] !== owner) {
1243
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
1244
+ return;
1245
+ }
1246
+ }
1247
+ if (allowWrite && !await allowWrite(req, existing, update)) {
1248
+ fail(res, 403, RestErrorCode.WriteForbidden, "Write not allowed");
1249
+ return;
1250
+ }
1251
+ }
960
1252
  const success = await model2.updateById(id, update);
961
1253
  if (!success) {
962
- res.status(404).json({ success: false, error: "Not found" });
1254
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
963
1255
  return;
964
1256
  }
965
1257
  const doc = await model2.findById(id);
966
- res.json({ success: true, data: doc });
1258
+ res.json({ success: true, data: projectDoc(doc, publicSet) });
967
1259
  } catch (error) {
968
- res.status(400).json({ success: false, error: error.message });
1260
+ handleCaught(res, error);
969
1261
  }
970
1262
  });
971
1263
  router.delete("/:id", async (req, res) => {
1264
+ if (allowDelete === false) {
1265
+ fail(res, 403, RestErrorCode.DeleteDisabled, "Delete not allowed");
1266
+ return;
1267
+ }
972
1268
  const id = param(req, "id");
973
- if (typeof allowDelete === "function") {
974
- const doc = await model2.findById(id);
975
- if (!doc) {
976
- res.status(404).json({ success: false, error: "Not found" });
977
- return;
1269
+ try {
1270
+ const needsDoc = !!ownershipField || typeof allowDelete === "function";
1271
+ if (needsDoc) {
1272
+ const doc = await model2.findById(id);
1273
+ if (!doc) {
1274
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
1275
+ return;
1276
+ }
1277
+ if (ownershipField) {
1278
+ const owner = req.user?.deviceId;
1279
+ if (!owner || doc[ownershipField] !== owner) {
1280
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
1281
+ return;
1282
+ }
1283
+ }
1284
+ if (typeof allowDelete === "function" && !await allowDelete(req, doc)) {
1285
+ fail(
1286
+ res,
1287
+ 403,
1288
+ RestErrorCode.DeleteForbidden,
1289
+ "Delete not allowed"
1290
+ );
1291
+ return;
1292
+ }
978
1293
  }
979
- if (!allowDelete(req, doc)) {
980
- res.status(403).json({ success: false, error: "Delete not allowed" });
1294
+ const success = await model2.deleteById(id);
1295
+ if (!success) {
1296
+ fail(res, 404, RestErrorCode.NotFound, "Not found");
981
1297
  return;
982
1298
  }
983
- } else if (!allowDelete) {
984
- res.status(403).json({ success: false, error: "Delete not allowed" });
985
- return;
986
- }
987
- const success = await model2.deleteById(id);
988
- if (!success) {
989
- res.status(404).json({ success: false, error: "Not found" });
990
- return;
1299
+ res.json({ success: true });
1300
+ } catch (error) {
1301
+ handleCaught(res, error);
991
1302
  }
992
- res.json({ success: true });
993
1303
  });
994
1304
  return router;
995
1305
  }
@@ -1019,7 +1329,7 @@ var CodixusServer = class {
1019
1329
  async connect() {
1020
1330
  this._db = await this.connection.connect();
1021
1331
  this.banService = new BanService(this._db);
1022
- this.guardFn = createGuard(this.jwt);
1332
+ this.guardFn = createGuard(this.jwt, this.banService);
1023
1333
  for (const m of getModelRegistry()) {
1024
1334
  m._bind(this._db, this.instanceId);
1025
1335
  await m._ensureIndexes();
@@ -1111,6 +1421,7 @@ var import_shared4 = require("@codixus/shared");
1111
1421
  ErrorCodes,
1112
1422
  Model,
1113
1423
  Query,
1424
+ RestErrorCode,
1114
1425
  SubCollection,
1115
1426
  model,
1116
1427
  validate