mindoodb 0.0.10 → 0.0.12

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.
Files changed (56) hide show
  1. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts +1 -0
  2. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.d.ts.map +1 -1
  3. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js +10 -0
  4. package/dist/appendonlystores/network/ClientNetworkContentAddressedStore.js.map +1 -1
  5. package/dist/appendonlystores/network/HttpTransport.d.ts +1 -0
  6. package/dist/appendonlystores/network/HttpTransport.d.ts.map +1 -1
  7. package/dist/appendonlystores/network/HttpTransport.js +4 -3
  8. package/dist/appendonlystores/network/HttpTransport.js.map +1 -1
  9. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts +34 -0
  10. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.d.ts.map +1 -1
  11. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js +194 -9
  12. package/dist/browser/appendonlystores/IndexedDBContentAddressedStore.js.map +1 -1
  13. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts +22 -0
  14. package/dist/browser/cache/IndexedDBLocalCacheStore.d.ts.map +1 -1
  15. package/dist/browser/cache/IndexedDBLocalCacheStore.js +185 -24
  16. package/dist/browser/cache/IndexedDBLocalCacheStore.js.map +1 -1
  17. package/dist/browser/index.d.ts +2 -2
  18. package/dist/browser/index.d.ts.map +1 -1
  19. package/dist/browser/index.js +3 -1
  20. package/dist/browser/index.js.map +1 -1
  21. package/dist/browser/index.mjs +2 -0
  22. package/dist/core/BaseMindooDB.d.ts +30 -0
  23. package/dist/core/BaseMindooDB.d.ts.map +1 -1
  24. package/dist/core/BaseMindooDB.js +155 -38
  25. package/dist/core/BaseMindooDB.js.map +1 -1
  26. package/dist/core/BaseMindooTenant.d.ts +0 -1
  27. package/dist/core/BaseMindooTenant.d.ts.map +1 -1
  28. package/dist/core/BaseMindooTenant.js +3 -13
  29. package/dist/core/BaseMindooTenant.js.map +1 -1
  30. package/dist/core/BaseMindooTenantDirectory.d.ts +1 -0
  31. package/dist/core/BaseMindooTenantDirectory.d.ts.map +1 -1
  32. package/dist/core/BaseMindooTenantDirectory.js +24 -8
  33. package/dist/core/BaseMindooTenantDirectory.js.map +1 -1
  34. package/dist/core/appendonlystores/AttachmentReadPlanner.d.ts.map +1 -1
  35. package/dist/core/appendonlystores/AttachmentReadPlanner.js +1 -1
  36. package/dist/core/appendonlystores/AttachmentReadPlanner.js.map +1 -1
  37. package/dist/core/types.d.ts +17 -8
  38. package/dist/core/types.d.ts.map +1 -1
  39. package/dist/core/types.js.map +1 -1
  40. package/dist/node/server/MindooDBServer.d.ts +104 -0
  41. package/dist/node/server/MindooDBServer.d.ts.map +1 -1
  42. package/dist/node/server/MindooDBServer.js +192 -32
  43. package/dist/node/server/MindooDBServer.js.map +1 -1
  44. package/dist/node/server/TenantManager.d.ts +2 -0
  45. package/dist/node/server/TenantManager.d.ts.map +1 -1
  46. package/dist/node/server/TenantManager.js +21 -12
  47. package/dist/node/server/TenantManager.js.map +1 -1
  48. package/dist/node/server/config.d.ts.map +1 -1
  49. package/dist/node/server/config.js +4 -0
  50. package/dist/node/server/config.js.map +1 -1
  51. package/dist/node/server/server.js +2 -2
  52. package/dist/node/server/server.js.map +1 -1
  53. package/dist/node/server/types.d.ts +1 -0
  54. package/dist/node/server/types.d.ts.map +1 -1
  55. package/dist/node/server/types.js.map +1 -1
  56. package/package.json +2 -1
@@ -51,7 +51,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
51
51
  Object.defineProperty(exports, "__esModule", { value: true });
52
52
  exports.MindooDBServer = void 0;
53
53
  const express_1 = __importStar(require("express"));
54
- const https_1 = __importDefault(require("https"));
54
+ const http2_1 = __importDefault(require("http2"));
55
55
  const path_1 = __importDefault(require("path"));
56
56
  const fs_1 = require("fs");
57
57
  const helmet_1 = __importDefault(require("helmet"));
@@ -65,8 +65,19 @@ const SystemAdminAuth_1 = require("./SystemAdminAuth");
65
65
  const config_1 = require("./config");
66
66
  const SystemIpAllowlist_1 = require("./SystemIpAllowlist");
67
67
  const validation_1 = require("./validation");
68
+ /**
69
+ * Rate-limit defaults. Auth and sync have separate tiers because a single
70
+ * challenge/authenticate handshake is two requests; with multiple tenants,
71
+ * store kinds, and token renewals the auth budget exhausts quickly if set
72
+ * too low. These can be overridden in config.json under `rateLimits.auth`
73
+ * and `rateLimits.sync`.
74
+ */
75
+ const DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS = 60000;
76
+ const DEFAULT_AUTH_RATE_LIMIT_MAX = 120;
68
77
  const DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS = 60000;
69
78
  const DEFAULT_SYNC_RATE_LIMIT_MAX = 1000;
79
+ const DEFAULT_SERVER_SOCKET_TIMEOUT_MS = 120000;
80
+ /** Parse a human-readable size string ("5mb", "1024", "100kb") into bytes. */
70
81
  function parseBodySizeLimitToBytes(limit) {
71
82
  const normalized = limit.trim().toLowerCase();
72
83
  if (!normalized) {
@@ -91,6 +102,15 @@ function parseBodySizeLimitToBytes(limit) {
91
102
  return Math.round(value * multiplier);
92
103
  }
93
104
  class MindooDBServer {
105
+ /**
106
+ * @param dataDir Root directory for on-disk tenant data and server identity
107
+ * @param serverPassword Password used to decrypt the server identity private keys
108
+ * and per-tenant key bags at startup
109
+ * @param staticDir Optional directory served at /statics/ (e.g. the Haven web UI)
110
+ * @param config Pre-loaded ServerConfig; when omitted, an empty config
111
+ * is used (all /system/* endpoints locked down)
112
+ * @param configPath Explicit path to config.json; defaults to `<dataDir>/config.json`
113
+ */
94
114
  constructor(dataDir, serverPassword, staticDir, config, configPath) {
95
115
  this.app = (0, express_1.default)();
96
116
  this.tenantManager = new TenantManager_1.TenantManager(dataDir, serverPassword);
@@ -100,6 +120,8 @@ class MindooDBServer {
100
120
  this.jsonBodyLimitBytes = parseBodySizeLimitToBytes(this.jsonBodyLimit);
101
121
  this.serverConfig = config ?? { capabilities: {} };
102
122
  this.capabilityMatcher = new CapabilityMatcher_1.CapabilityMatcher(this.serverConfig);
123
+ // SystemAdminAuthService generates a random HMAC-SHA256 secret for JWTs
124
+ // on each server start — existing tokens become invalid after restart.
103
125
  const cryptoAdapter = this.tenantManager.getCryptoAdapter();
104
126
  this.systemAdminAuth = new SystemAdminAuth_1.SystemAdminAuthService(cryptoAdapter, this.serverConfig);
105
127
  const principalCount = Object.values(this.serverConfig.capabilities).reduce((sum, arr) => sum + arr.length, 0);
@@ -127,6 +149,11 @@ class MindooDBServer {
127
149
  getConfigPath() {
128
150
  return this.configPath;
129
151
  }
152
+ /**
153
+ * Hot-swap the server config without restarting. In-flight JWTs remain
154
+ * valid (same HMAC secret), but removed principals will fail the
155
+ * CapabilityMatcher check on their next request.
156
+ */
130
157
  reloadConfig(newConfig) {
131
158
  this.serverConfig = newConfig;
132
159
  this.capabilityMatcher.reload(newConfig);
@@ -136,19 +163,37 @@ class MindooDBServer {
136
163
  const server = this.app.listen(port, () => {
137
164
  console.log(`[MindooDBServer] Listening on port ${port}`);
138
165
  });
139
- server.setTimeout(30000);
166
+ server.setTimeout(DEFAULT_SERVER_SOCKET_TIMEOUT_MS);
140
167
  }
168
+ /**
169
+ * Start the server with TLS using HTTP/2 (with automatic HTTP/1.1 ALPN
170
+ * fallback for clients that don't support h2). Express is mounted as the
171
+ * raw handler because http2.createSecureServer doesn't natively integrate
172
+ * with Express; the `as unknown` casts bridge the type gap.
173
+ */
141
174
  listenTls(port, certPath, keyPath) {
142
175
  const tlsOptions = {
176
+ allowHTTP1: true,
143
177
  cert: (0, fs_1.readFileSync)(certPath),
144
178
  key: (0, fs_1.readFileSync)(keyPath),
145
179
  };
146
- const server = https_1.default.createServer(tlsOptions, this.app);
180
+ const server = http2_1.default.createSecureServer(tlsOptions, (req, res) => {
181
+ this.app(req, res);
182
+ });
147
183
  server.listen(port, () => {
148
- console.log(`[MindooDBServer] Listening on HTTPS port ${port}`);
184
+ console.log(`[MindooDBServer] Listening on HTTPS port ${port} with HTTP/2 enabled (HTTP/1 fallback active)`);
149
185
  });
150
- server.setTimeout(30000);
186
+ server.setTimeout(DEFAULT_SERVER_SOCKET_TIMEOUT_MS);
151
187
  }
188
+ /**
189
+ * Global middleware stack applied to every request, in order:
190
+ * 1. helmet — security headers (CSP, X-Frame-Options, etc.)
191
+ * 2. cors — controlled cross-origin access for web clients (e.g. Haven)
192
+ * 3. global rate limit — coarse IP-based throttle as a safety net;
193
+ * individual route tiers (auth, sync, system) provide finer control
194
+ * 4. JSON body parser — captures raw byte size for diagnostics
195
+ * 5. request logger
196
+ */
152
197
  setupMiddleware() {
153
198
  this.app.use((0, helmet_1.default)());
154
199
  const corsOrigin = this.readCorsOriginsFromEnv();
@@ -173,10 +218,11 @@ class MindooDBServer {
173
218
  this.app.use((req, res, next) => {
174
219
  const contentType = req.headers["content-type"] ?? "-";
175
220
  const contentLength = req.headers["content-length"] ?? "-";
176
- console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} content-type=${contentType} content-length=${contentLength}`);
221
+ console.log(`[${new Date().toISOString()}] HTTP/${req.httpVersion} ${req.method} ${req.path} content-type=${contentType} content-length=${contentLength}`);
177
222
  next();
178
223
  });
179
224
  }
225
+ /** Parse MINDOODB_CORS_ORIGIN env var; supports comma-separated multiple origins. */
180
226
  readCorsOriginsFromEnv() {
181
227
  const raw = process.env.MINDOODB_CORS_ORIGIN?.trim();
182
228
  if (!raw) {
@@ -191,8 +237,16 @@ class MindooDBServer {
191
237
  }
192
238
  return origins;
193
239
  }
240
+ /**
241
+ * Route hierarchy:
242
+ * / — redirect to static UI (if present)
243
+ * /health — unauthenticated health probe
244
+ * /.well-known/* — public discovery endpoints (server info, tenant fingerprints)
245
+ * /statics/* — optional static file serving (Haven web UI)
246
+ * /system/* — admin API: IP-allowlisted, JWT-authenticated, capability-checked
247
+ * /:tenantId/* — per-tenant sync & auth API
248
+ */
194
249
  setupRoutes() {
195
- // Root redirect
196
250
  this.app.get("/", (req, res) => {
197
251
  if (this.staticDir && (0, fs_1.existsSync)(path_1.default.join(this.staticDir, "index.html"))) {
198
252
  res.redirect(302, "/statics/index.html");
@@ -204,6 +258,8 @@ class MindooDBServer {
204
258
  this.app.get("/health", (req, res) => {
205
259
  res.json({ status: "ok", timestamp: Date.now() });
206
260
  });
261
+ // .well-known endpoints are unauthenticated so clients can discover
262
+ // server identity and tenant availability before attempting to authenticate.
207
263
  this.app.get("/.well-known/mindoodb-server-info", (req, res) => {
208
264
  const info = this.tenantManager.getServerPublicInfo();
209
265
  if (!info) {
@@ -256,7 +312,10 @@ class MindooDBServer {
256
312
  }, express_1.default.static(resolvedStaticDir, { dotfiles: "deny", etag: true }));
257
313
  console.log(`[MindooDBServer] Serving static files from ${resolvedStaticDir} at /statics/`);
258
314
  }
259
- // System admin routes (replaces former /admin/*)
315
+ // /system/* admin routes are protected by three layers (applied in order):
316
+ // 1. IP allowlist — optional whitelist via MINDOODB_ADMIN_ALLOWED_IPS env var
317
+ // 2. Rate limit — aggressive per-IP throttle (30 req/min)
318
+ // 3. JWT auth — applied per-route (auth endpoints exempt; they *produce* JWTs)
260
319
  const systemIpRaw = (0, SystemIpAllowlist_1.readSystemIpAllowListFromEnv)();
261
320
  if (!(0, SystemIpAllowlist_1.isSystemIpAllowListDisabled)(systemIpRaw)) {
262
321
  console.log(`[MindooDBServer] MINDOODB_ADMIN_ALLOWED_IPS is set — /system/* limited to: ${systemIpRaw}`);
@@ -271,8 +330,10 @@ class MindooDBServer {
271
330
  const systemRouter = (0, express_1.Router)();
272
331
  this.setupSystemRoutes(systemRouter);
273
332
  this.app.use("/system", SystemIpAllowlist_1.systemIpAllowlistMiddleware, systemRateLimit, systemRouter);
274
- // Tenant-scoped routes
333
+ // Tenant-scoped routes: tenantMiddleware validates the :tenantId param
334
+ // and rejects unknown tenants before any handler runs.
275
335
  this.app.use("/:tenantId", this.tenantMiddleware.bind(this), this.createTenantRouter());
336
+ // Express error handler — must be registered last and have the 4-arg signature
276
337
  this.app.use(this.errorHandler.bind(this));
277
338
  }
278
339
  // ==================== System Admin Auth Middleware ====================
@@ -329,18 +390,17 @@ class MindooDBServer {
329
390
  next();
330
391
  }
331
392
  // ==================== System Routes ====================
393
+ /**
394
+ * Wire up all /system/* routes. Auth endpoints sit outside the JWT
395
+ * middleware because they implement the challenge-response handshake
396
+ * that *produces* the JWT. Every other route requires a valid JWT and
397
+ * passes through the CapabilityMatcher for fine-grained authorization.
398
+ */
332
399
  setupSystemRoutes(router) {
333
- // Auth endpoints — no JWT required (these *produce* JWTs)
334
- const authRateLimit = (0, express_rate_limit_1.default)({
335
- windowMs: 60000,
336
- max: 20,
337
- standardHeaders: true,
338
- legacyHeaders: false,
339
- message: { error: "Too many authentication attempts, please try again later" },
340
- });
341
- router.post("/auth/challenge", authRateLimit, this.handleSystemChallenge.bind(this));
342
- router.post("/auth/authenticate", authRateLimit, this.handleSystemAuthenticate.bind(this));
343
- // All other /system/* routes require JWT + capability check
400
+ const challengeRateLimit = this.createAuthRateLimit("system", "challenge");
401
+ const authenticateRateLimit = this.createAuthRateLimit("system", "authenticate");
402
+ router.post("/auth/challenge", challengeRateLimit, this.handleSystemChallenge.bind(this));
403
+ router.post("/auth/authenticate", authenticateRateLimit, this.handleSystemAuthenticate.bind(this));
344
404
  const authMiddleware = this.systemAdminMiddleware.bind(this);
345
405
  // Tenant CRUD
346
406
  router.get("/tenants", authMiddleware, (req, res) => {
@@ -354,6 +414,8 @@ class MindooDBServer {
354
414
  res.status(500).json({ error: "Failed to list tenants" });
355
415
  }
356
416
  });
417
+ // Tenant registration is idempotent: re-POSTing with the same tenantId
418
+ // and matching $publicinfos key returns 200 instead of 409.
357
419
  router.post("/tenants/:tenantId", authMiddleware, async (req, res) => {
358
420
  try {
359
421
  const tenantId = req.params.tenantId.toLowerCase();
@@ -725,7 +787,8 @@ class MindooDBServer {
725
787
  });
726
788
  return;
727
789
  }
728
- // Self-lockout protection: verify the calling admin retains PUT /system/config access
790
+ // Self-lockout protection: simulate the proposed config against the calling
791
+ // admin's identity — reject if they would lose their own config access.
729
792
  const callingAdmin = req.systemAdmin;
730
793
  const proposedMatcher = new CapabilityMatcher_1.CapabilityMatcher(newConfig);
731
794
  if (!proposedMatcher.isAuthorized("PUT", "/system/config", callingAdmin.username, callingAdmin.publicsignkey)) {
@@ -757,18 +820,20 @@ class MindooDBServer {
757
820
  }
758
821
  }
759
822
  // ==================== Tenant Routes ====================
823
+ /**
824
+ * Build the per-tenant router. Rate limiters are scoped: auth endpoints
825
+ * use a per-user composite key (see createAuthRateLimit) while sync
826
+ * endpoints use a plain IP-based bucket with a higher ceiling.
827
+ *
828
+ * Both "docs" and "attachments" store kinds expose the same set of sync
829
+ * operations, mounted at /sync/docs/* and /sync/attachments/*.
830
+ */
760
831
  createTenantRouter() {
761
832
  const router = (0, express_1.Router)({ mergeParams: true });
833
+ const authRateLimitConfig = this.getAuthRateLimitConfig();
762
834
  const syncRateLimitConfig = this.getSyncRateLimitConfig();
763
- // Auth endpoints get stricter rate limiting
764
- const authRateLimit = (0, express_rate_limit_1.default)({
765
- windowMs: 60000,
766
- max: 20,
767
- standardHeaders: true,
768
- legacyHeaders: false,
769
- message: { error: "Too many authentication attempts, please try again later" },
770
- });
771
- // Sync endpoints get a higher limit
835
+ const challengeRateLimit = this.createAuthRateLimit("tenant", "challenge");
836
+ const authenticateRateLimit = this.createAuthRateLimit("tenant", "authenticate");
772
837
  const syncRateLimit = (0, express_rate_limit_1.default)({
773
838
  windowMs: syncRateLimitConfig.windowMs,
774
839
  max: syncRateLimitConfig.max,
@@ -776,8 +841,9 @@ class MindooDBServer {
776
841
  legacyHeaders: false,
777
842
  message: { error: "Too many sync requests, please try again later" },
778
843
  });
779
- router.post("/auth/challenge", authRateLimit, this.handleChallenge.bind(this));
780
- router.post("/auth/authenticate", authRateLimit, this.handleAuthenticate.bind(this));
844
+ console.log(`[MindooDBServer] Tenant auth rate limit configured windowMs=${authRateLimitConfig.windowMs} max=${authRateLimitConfig.max}`);
845
+ router.post("/auth/challenge", challengeRateLimit, this.handleChallenge.bind(this));
846
+ router.post("/auth/authenticate", authenticateRateLimit, this.handleAuthenticate.bind(this));
781
847
  for (const storeKind of ["docs", "attachments"]) {
782
848
  const syncBase = `/sync/${storeKind}`;
783
849
  router.post(`${syncBase}/findNewEntries`, syncRateLimit, this.handleFindNewEntries.bind(this));
@@ -799,12 +865,83 @@ class MindooDBServer {
799
865
  }
800
866
  return router;
801
867
  }
868
+ /** Resolved auth rate-limit config; falls back to defaults when not set in config.json. */
869
+ getAuthRateLimitConfig() {
870
+ return {
871
+ windowMs: this.serverConfig.rateLimits?.auth?.windowMs ?? DEFAULT_AUTH_RATE_LIMIT_WINDOW_MS,
872
+ max: this.serverConfig.rateLimits?.auth?.max ?? DEFAULT_AUTH_RATE_LIMIT_MAX,
873
+ };
874
+ }
875
+ /** Resolved sync rate-limit config; falls back to defaults when not set in config.json. */
802
876
  getSyncRateLimitConfig() {
803
877
  return {
804
878
  windowMs: this.serverConfig.rateLimits?.sync?.windowMs ?? DEFAULT_SYNC_RATE_LIMIT_WINDOW_MS,
805
879
  max: this.serverConfig.rateLimits?.sync?.max ?? DEFAULT_SYNC_RATE_LIMIT_MAX,
806
880
  };
807
881
  }
882
+ /**
883
+ * Create a rate-limiter for authentication endpoints. Challenge and
884
+ * authenticate get separate limiters so that one phase can't starve the
885
+ * other. The custom keyGenerator buckets by (scope, tenant, phase, IP,
886
+ * username) — this prevents users behind a shared IP (NAT / reverse
887
+ * proxy) from exhausting each other's budgets.
888
+ */
889
+ createAuthRateLimit(scope, phase) {
890
+ const authRateLimitConfig = this.getAuthRateLimitConfig();
891
+ return (0, express_rate_limit_1.default)({
892
+ windowMs: authRateLimitConfig.windowMs,
893
+ max: authRateLimitConfig.max,
894
+ standardHeaders: true,
895
+ legacyHeaders: false,
896
+ keyGenerator: (req) => this.getAuthRateLimitKey(req, scope, phase),
897
+ handler: (req, res, _next, options) => {
898
+ this.logAuthRateLimitHit(req, scope, phase);
899
+ res.status(options.statusCode).json(options.message);
900
+ },
901
+ message: { error: "Too many authentication attempts, please try again later" },
902
+ });
903
+ }
904
+ /**
905
+ * Composite rate-limit key. Challenge requests include the username so
906
+ * that brute-forcing one account doesn't block legitimate users on the
907
+ * same IP. Authenticate requests omit username because the challenge
908
+ * token is already scoped to a single principal.
909
+ */
910
+ getAuthRateLimitKey(req, scope, phase) {
911
+ const tenantId = scope === "tenant"
912
+ ? req.tenantId ?? req.params.tenantId?.toLowerCase() ?? "-"
913
+ : "system";
914
+ const clientIp = this.getClientIpForRateLimit(req);
915
+ if (phase === "challenge") {
916
+ const username = typeof req.body?.username === "string"
917
+ ? req.body.username.trim().toLowerCase()
918
+ : "-";
919
+ return `${scope}:${tenantId}:${phase}:${clientIp}:${username}`;
920
+ }
921
+ return `${scope}:${tenantId}:${phase}:${clientIp}`;
922
+ }
923
+ /**
924
+ * Emit a structured warning when an auth rate limit is triggered.
925
+ * The forwarded-for header is logged to help diagnose situations where
926
+ * a reverse proxy collapses many clients onto a single visible IP.
927
+ */
928
+ logAuthRateLimitHit(req, scope, phase) {
929
+ const forwardedForHeader = req.headers["x-forwarded-for"];
930
+ const forwardedFor = Array.isArray(forwardedForHeader)
931
+ ? forwardedForHeader.join(", ")
932
+ : forwardedForHeader;
933
+ const username = typeof req.body?.username === "string" ? req.body.username : "-";
934
+ console.warn(`[MindooDBServer] Auth rate limit exceeded scope=${scope} phase=${phase} tenant=${req.tenantId ?? req.params.tenantId?.toLowerCase() ?? "-"} ip=${this.getClientIpForRateLimit(req)} forwarded-for=${forwardedFor ?? "-"} username=${username} path=${req.path}`);
935
+ }
936
+ /**
937
+ * Best-effort client IP. Express populates `req.ip` from the trust-proxy
938
+ * setting; fall back to the raw socket address when it's unavailable.
939
+ */
940
+ getClientIpForRateLimit(req) {
941
+ return ((typeof req.ip === "string" && req.ip.length > 0
942
+ ? req.ip
943
+ : req.socket?.remoteAddress) ?? "unknown");
944
+ }
808
945
  // ==================== Validation Helpers ====================
809
946
  validateDbId(dbId) {
810
947
  return (0, validation_1.validateIdentifier)(dbId, "dbId");
@@ -814,6 +951,10 @@ class MindooDBServer {
814
951
  return `Tenant ${tenantId} not found on server ${serverName}`;
815
952
  }
816
953
  // ==================== Tenant Auth Handlers ====================
954
+ // Two-step challenge-response flow:
955
+ // 1. POST /auth/challenge → client sends username, server returns a random nonce
956
+ // 2. POST /auth/authenticate → client signs the nonce with its Ed25519 private key,
957
+ // server verifies the signature and returns a JWT for subsequent sync calls
817
958
  async handleChallenge(req, res) {
818
959
  try {
819
960
  const { username } = req.body;
@@ -990,6 +1131,11 @@ class MindooDBServer {
990
1131
  this.handleRequestError(error, res);
991
1132
  }
992
1133
  }
1134
+ /**
1135
+ * Ingest encrypted entries from a client. The `stage` variable tracks
1136
+ * progress through the pipeline so that error logs pinpoint the failing
1137
+ * step (token extraction → body parsing → deserialization → storage).
1138
+ */
993
1139
  async handlePutEntries(req, res) {
994
1140
  let stage = "extract-token";
995
1141
  try {
@@ -1153,6 +1299,11 @@ class MindooDBServer {
1153
1299
  res.json({ success: true, message: "Sync triggered (not yet implemented)" });
1154
1300
  }
1155
1301
  // ==================== Helper Methods ====================
1302
+ /**
1303
+ * Derive the StoreKind from the request path. Both "docs" and
1304
+ * "attachments" share the same handler implementations; the store kind
1305
+ * selects which on-disk append-only store to operate against.
1306
+ */
1156
1307
  getStoreKindFromRequest(req) {
1157
1308
  const trimmedPath = req.path.replace(/\/$/, "");
1158
1309
  if (trimmedPath.startsWith("/sync/docs/") || trimmedPath === "/sync/docs/capabilities" || trimmedPath === "/sync/docs/getAllIds") {
@@ -1170,6 +1321,7 @@ class MindooDBServer {
1170
1321
  }
1171
1322
  return auth.substring(7);
1172
1323
  }
1324
+ /** Map domain errors to HTTP status codes; unknown errors become 500. */
1173
1325
  handleRequestError(error, res) {
1174
1326
  if (error instanceof validation_1.ValidationError) {
1175
1327
  res.status(400).json({ error: error.message });
@@ -1200,6 +1352,11 @@ class MindooDBServer {
1200
1352
  return 500;
1201
1353
  }
1202
1354
  }
1355
+ /**
1356
+ * Express error handler (4-arg signature). Handles body-parser errors
1357
+ * (413 entity too large, 400 parse failed) with structured JSON responses;
1358
+ * everything else is logged and returned as a generic 500.
1359
+ */
1203
1360
  errorHandler(err, req, res, next) {
1204
1361
  const parserError = err;
1205
1362
  const status = parserError.statusCode ?? parserError.status;
@@ -1235,6 +1392,9 @@ class MindooDBServer {
1235
1392
  res.status(500).json({ error: "Internal server error" });
1236
1393
  }
1237
1394
  // ==================== Serialization ====================
1395
+ // Entries are stored with binary fields (Uint8Array) internally but
1396
+ // transmitted as base64 strings over JSON. These methods handle the
1397
+ // conversion in both directions.
1238
1398
  serializeEntryMetadata(metadata) {
1239
1399
  return {
1240
1400
  entryType: metadata.entryType,