neotoma 0.6.0 → 0.7.0

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 (78) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +226 -2
  3. package/dist/actions.js.map +1 -1
  4. package/dist/cli/bootstrap.js +0 -0
  5. package/dist/cli/index.d.ts.map +1 -1
  6. package/dist/cli/index.js +2 -17
  7. package/dist/cli/index.js.map +1 -1
  8. package/dist/scripts/reset_sandbox.js +180 -0
  9. package/dist/scripts/reset_sandbox.js.map +1 -0
  10. package/dist/scripts/seed_sandbox.js +254 -0
  11. package/dist/scripts/seed_sandbox.js.map +1 -0
  12. package/dist/server.d.ts.map +1 -1
  13. package/dist/server.js +28 -1
  14. package/dist/server.js.map +1 -1
  15. package/dist/services/feedback/admin_proxy.d.ts +59 -0
  16. package/dist/services/feedback/admin_proxy.d.ts.map +1 -0
  17. package/dist/services/feedback/admin_proxy.js +166 -0
  18. package/dist/services/feedback/admin_proxy.js.map +1 -0
  19. package/dist/services/feedback/seed_schema.d.ts.map +1 -1
  20. package/dist/services/feedback/seed_schema.js +5 -0
  21. package/dist/services/feedback/seed_schema.js.map +1 -1
  22. package/dist/services/local_auth.d.ts +14 -0
  23. package/dist/services/local_auth.d.ts.map +1 -1
  24. package/dist/services/local_auth.js +31 -0
  25. package/dist/services/local_auth.js.map +1 -1
  26. package/dist/services/root_landing/harness_snippets.d.ts +70 -0
  27. package/dist/services/root_landing/harness_snippets.d.ts.map +1 -0
  28. package/dist/services/root_landing/harness_snippets.js +279 -0
  29. package/dist/services/root_landing/harness_snippets.js.map +1 -0
  30. package/dist/services/root_landing/html_template.d.ts +33 -0
  31. package/dist/services/root_landing/html_template.d.ts.map +1 -0
  32. package/dist/services/root_landing/html_template.js +370 -0
  33. package/dist/services/root_landing/html_template.js.map +1 -0
  34. package/dist/services/root_landing/index.d.ts +58 -0
  35. package/dist/services/root_landing/index.d.ts.map +1 -0
  36. package/dist/services/root_landing/index.js +261 -0
  37. package/dist/services/root_landing/index.js.map +1 -0
  38. package/dist/services/root_landing/md_template.d.ts +7 -0
  39. package/dist/services/root_landing/md_template.d.ts.map +1 -0
  40. package/dist/services/root_landing/md_template.js +116 -0
  41. package/dist/services/root_landing/md_template.js.map +1 -0
  42. package/dist/services/root_landing/site_nav.d.ts +32 -0
  43. package/dist/services/root_landing/site_nav.d.ts.map +1 -0
  44. package/dist/services/root_landing/site_nav.js +116 -0
  45. package/dist/services/root_landing/site_nav.js.map +1 -0
  46. package/dist/services/sandbox/local_store.d.ts +47 -0
  47. package/dist/services/sandbox/local_store.d.ts.map +1 -0
  48. package/dist/services/sandbox/local_store.js +92 -0
  49. package/dist/services/sandbox/local_store.js.map +1 -0
  50. package/dist/services/sandbox/seed_schema.d.ts +21 -0
  51. package/dist/services/sandbox/seed_schema.d.ts.map +1 -0
  52. package/dist/services/sandbox/seed_schema.js +97 -0
  53. package/dist/services/sandbox/seed_schema.js.map +1 -0
  54. package/dist/services/sandbox/terms.d.ts +18 -0
  55. package/dist/services/sandbox/terms.d.ts.map +1 -0
  56. package/dist/services/sandbox/terms.js +19 -0
  57. package/dist/services/sandbox/terms.js.map +1 -0
  58. package/dist/services/sandbox/transport.d.ts +33 -0
  59. package/dist/services/sandbox/transport.d.ts.map +1 -0
  60. package/dist/services/sandbox/transport.js +174 -0
  61. package/dist/services/sandbox/transport.js.map +1 -0
  62. package/dist/services/sandbox/types.d.ts +47 -0
  63. package/dist/services/sandbox/types.d.ts.map +1 -0
  64. package/dist/services/sandbox/types.js +18 -0
  65. package/dist/services/sandbox/types.js.map +1 -0
  66. package/dist/services/sandbox_mode.d.ts +28 -0
  67. package/dist/services/sandbox_mode.d.ts.map +1 -0
  68. package/dist/services/sandbox_mode.js +69 -0
  69. package/dist/services/sandbox_mode.js.map +1 -0
  70. package/dist/shared/sandbox_terms_content.d.ts +14 -0
  71. package/dist/shared/sandbox_terms_content.d.ts.map +1 -0
  72. package/dist/shared/sandbox_terms_content.js +57 -0
  73. package/dist/shared/sandbox_terms_content.js.map +1 -0
  74. package/package.json +3 -2
  75. package/dist/services/llm_extraction.d.ts +0 -82
  76. package/dist/services/llm_extraction.d.ts.map +0 -1
  77. package/dist/services/llm_extraction.js +0 -435
  78. package/dist/services/llm_extraction.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAmH9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AAkQ7B;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAU5D;AAs5LD,wBAAsB,eAAe;;;eAgCpC"}
1
+ {"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AA0I9B,eAAO,MAAM,GAAG,6CAAY,CAAC;AAgY7B;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,GAAG,OAAO,CAU5D;AA2hMD,wBAAsB,eAAe;;;eAkDpC"}
package/dist/actions.js CHANGED
@@ -18,6 +18,7 @@ import { aauthVerify, getAAuthContextFromRequest, getAttributionDecisionFromRequ
18
18
  import { attributionContext } from "./middleware/attribution_context.js";
19
19
  import { buildSessionInfo } from "./services/session_info.js";
20
20
  import { AttributionPolicyError } from "./services/attribution_policy.js";
21
+ import { registerFeedbackAdminProxyRoutes } from "./services/feedback/admin_proxy.js";
21
22
  import { AgentCapabilityError, contextFromAgentIdentity, enforceAgentCapability, } from "./services/agent_capabilities.js";
22
23
  import { getCurrentAgentIdentity, runWithRequestContext, } from "./services/request_context.js";
23
24
  import { createAgentIdentity as buildAgentIdentity, getAgentIdentityFromRequest, normaliseClientName, } from "./crypto/agent_identity.js";
@@ -29,7 +30,11 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
29
30
  import { NeotomaServer } from "./server.js";
30
31
  import { logger } from "./utils/logger.js";
31
32
  import { OAuthError } from "./services/mcp_oauth_errors.js";
32
- import { ensureLocalDevUser, LOCAL_DEV_USER_ID } from "./services/local_auth.js";
33
+ import { ensureLocalDevUser, ensureSandboxPublicUser, LOCAL_DEV_USER_ID, SANDBOX_PUBLIC_USER_ID, } from "./services/local_auth.js";
34
+ import { isSandboxMode, sandboxDestructiveGuard, sandboxHeaderMiddleware, } from "./services/sandbox_mode.js";
35
+ import { buildLandingContext, buildRootLandingHtml, buildRootLandingJson, buildRootLandingMarkdown, buildRobotsTxt, wantsHtml as acceptWantsHtml, wantsMarkdown as acceptWantsMarkdown, } from "./services/root_landing/index.js";
36
+ import { getSandboxTermsResponse } from "./services/sandbox/terms.js";
37
+ import { resolveSandboxReportTransport } from "./services/sandbox/transport.js";
33
38
  import { getSqliteDb } from "./repositories/sqlite/sqlite_client.js";
34
39
  import { getMcpAuthToken } from "./crypto/mcp_auth_token.js";
35
40
  import { isOauthKeyCredentialValid, normalizeOauthNextPath, OAuthKeySessionStore, } from "./services/oauth_key_gate.js";
@@ -96,6 +101,45 @@ app.use(express.json({
96
101
  }));
97
102
  app.use(morgan("dev"));
98
103
  app.use(unknownFieldsGuard);
104
+ // Sandbox-mode response header. Stamped on every response so clients can
105
+ // detect public-sandbox deployments (sandbox.neotoma.io) without an extra
106
+ // API call. See src/services/sandbox_mode.ts.
107
+ if (isSandboxMode()) {
108
+ app.use(sandboxHeaderMiddleware);
109
+ logger.info("[Sandbox] NEOTOMA_SANDBOX_MODE=1 — bearer bypass to SANDBOX_PUBLIC_USER_ID, destructive routes gated, weekly reset expected");
110
+ }
111
+ // Inspector SPA mount. When NEOTOMA_INSPECTOR_STATIC_DIR is set, serve the
112
+ // pre-built Inspector bundle at NEOTOMA_INSPECTOR_BASE_PATH (default /app).
113
+ // Deliberately registered before all auth / rate-limit middleware so the SPA
114
+ // shell + assets are reachable without a bearer — the API calls it makes still
115
+ // flow through the normal auth stack below.
116
+ const inspectorStaticDir = (process.env.NEOTOMA_INSPECTOR_STATIC_DIR || "").trim();
117
+ const inspectorBasePath = ((process.env.NEOTOMA_INSPECTOR_BASE_PATH || "/app").trim() || "/app").replace(/\/$/, "");
118
+ if (inspectorStaticDir) {
119
+ try {
120
+ const indexHtmlPath = path.resolve(inspectorStaticDir, "index.html");
121
+ // express.static with fallthrough so 404s on unknown files fall into the
122
+ // SPA history handler below rather than short-circuiting.
123
+ app.use(inspectorBasePath, express.static(inspectorStaticDir, {
124
+ index: false,
125
+ fallthrough: true,
126
+ maxAge: "1h",
127
+ }));
128
+ app.get([`${inspectorBasePath}`, `${inspectorBasePath}/*`], (req, res, next) => {
129
+ // Only respond if the request was headed for the SPA (accepts html).
130
+ if (req.method !== "GET")
131
+ return next();
132
+ res.sendFile(indexHtmlPath, (err) => {
133
+ if (err)
134
+ next(err);
135
+ });
136
+ });
137
+ logger.info(`[Inspector] Serving SPA from ${inspectorStaticDir} at ${inspectorBasePath}`);
138
+ }
139
+ catch (err) {
140
+ logger.warn(`[Inspector] Failed to mount SPA: ${err.message}`);
141
+ }
142
+ }
99
143
  // Rate limiters for OAuth endpoints
100
144
  // validate.trustProxy: false — we use trust proxy behind one proxy; skip strict IP check
101
145
  const rateLimitOptions = {
@@ -134,6 +178,38 @@ const writeRateLimit = rateLimit({
134
178
  message: "Write rate limit exceeded, please slow down",
135
179
  ...rateLimitOptions,
136
180
  });
181
+ // SECURITY: sandbox write paths share a single user_id, so keying only by
182
+ // user starves legitimate callers when one IP abuses the endpoint. Sandbox
183
+ // rate limiter keys by IP so each visitor gets their own bucket, and uses a
184
+ // tighter per-minute cap tuned for the `sandbox.neotoma.io` demo.
185
+ const SANDBOX_WRITE_RATE_LIMIT_PER_MIN = Math.max(1, Number.parseInt(process.env.NEOTOMA_SANDBOX_WRITE_RATE_LIMIT_PER_MIN || "", 10) || 30);
186
+ const sandboxWriteRateLimit = rateLimit({
187
+ windowMs: 60 * 1000,
188
+ max: SANDBOX_WRITE_RATE_LIMIT_PER_MIN,
189
+ keyGenerator: (req) => `ip:${ipKeyGenerator(req.ip || "")}`,
190
+ message: "Sandbox write rate limit exceeded. Install Neotoma locally for unlimited use.",
191
+ ...rateLimitOptions,
192
+ });
193
+ /**
194
+ * Sandbox-only middleware. Applies the tighter sandbox write rate limit +
195
+ * destructive-op gate before every write-adjacent handler runs. Skipped on
196
+ * non-sandbox deployments so local/dev Fly behaviour is unchanged.
197
+ */
198
+ function sandboxWriteGate(req, res, next) {
199
+ if (!isSandboxMode())
200
+ return next();
201
+ sandboxDestructiveGuard(req, res, (destructiveErr) => {
202
+ if (destructiveErr)
203
+ return next(destructiveErr);
204
+ if (res.headersSent)
205
+ return;
206
+ // Only POST/PUT/PATCH/DELETE hit the write bucket.
207
+ if (["POST", "PUT", "PATCH", "DELETE"].includes(req.method)) {
208
+ return sandboxWriteRateLimit(req, res, next);
209
+ }
210
+ next();
211
+ });
212
+ }
137
213
  const oauthInitiateLimit = rateLimit({
138
214
  windowMs: 60 * 1000, // 1 minute
139
215
  max: 5,
@@ -160,6 +236,44 @@ const oauthRegisterLimit = rateLimit({
160
236
  });
161
237
  // Favicon (no-auth) to avoid 401 noise when not present on disk
162
238
  app.get("/favicon.ico", (_req, res) => res.status(204).end());
239
+ // ============================================================================
240
+ // Root landing page + robots.txt (no-auth, content-negotiated)
241
+ // ============================================================================
242
+ // HTML for browsers (identity, harness connect snippets, Learn index).
243
+ // JSON for agents/curl (same content, structured). See
244
+ // src/services/root_landing/index.ts.
245
+ app.get("/", (req, res) => {
246
+ try {
247
+ const ctx = buildLandingContext(req);
248
+ res.setHeader("Cache-Control", "public, max-age=60");
249
+ if (acceptWantsHtml(req.headers.accept)) {
250
+ return res.type("html").send(buildRootLandingHtml(ctx));
251
+ }
252
+ if (acceptWantsMarkdown(req.headers.accept)) {
253
+ res.type("text/markdown; charset=utf-8");
254
+ return res.send(buildRootLandingMarkdown(ctx));
255
+ }
256
+ return res.type("application/json").json(buildRootLandingJson(ctx));
257
+ }
258
+ catch (err) {
259
+ logger.error("[RootLanding] Failed to render landing page", { err });
260
+ return res.status(500).type("application/json").json({
261
+ error: "root_landing_error",
262
+ error_description: err.message,
263
+ });
264
+ }
265
+ });
266
+ app.get("/robots.txt", (req, res) => {
267
+ try {
268
+ const ctx = buildLandingContext(req);
269
+ res.setHeader("Cache-Control", "public, max-age=300");
270
+ return res.type("text/plain").send(buildRobotsTxt(ctx.mode, ctx.publicDocsUrl));
271
+ }
272
+ catch (err) {
273
+ logger.error("[RootLanding] Failed to render robots.txt", { err });
274
+ return res.status(500).type("text/plain").send("# error rendering robots.txt\n");
275
+ }
276
+ });
163
277
  // Smithery / MCP registry static metadata when automatic scan cannot finish (same host as /mcp)
164
278
  app.get("/.well-known/mcp/server-card.json", (_req, res) => {
165
279
  const override = process.env.NEOTOMA_MCP_SERVER_CARD_JSON?.trim();
@@ -924,6 +1038,82 @@ app.get("/health", (_req, res) => {
924
1038
  return res.json({ ok: true });
925
1039
  });
926
1040
  // ============================================================================
1041
+ // Sandbox endpoints
1042
+ //
1043
+ // Public-read routes that power the Inspector's /sandbox page at
1044
+ // sandbox.neotoma.io. These endpoints are mounted regardless of
1045
+ // NEOTOMA_SANDBOX_MODE so a self-hosted Neotoma can still surface its own
1046
+ // terms/report forms if operators want to.
1047
+ // ============================================================================
1048
+ app.get("/sandbox/terms", (_req, res) => {
1049
+ return res.json(getSandboxTermsResponse());
1050
+ });
1051
+ // Tight per-IP limiter for /sandbox/report so bots can't flood the forwarder.
1052
+ const SANDBOX_REPORT_RATE_LIMIT_PER_MIN = Math.max(1, Number.parseInt(process.env.NEOTOMA_SANDBOX_REPORT_RATE_LIMIT_PER_MIN || "", 10) ||
1053
+ 5);
1054
+ const sandboxReportRateLimit = rateLimit({
1055
+ windowMs: 60 * 1000,
1056
+ max: SANDBOX_REPORT_RATE_LIMIT_PER_MIN,
1057
+ keyGenerator: (req) => `ip:${ipKeyGenerator(req.ip || "")}`,
1058
+ message: "Sandbox report rate limit exceeded. Please wait a minute before submitting another report.",
1059
+ ...rateLimitOptions,
1060
+ });
1061
+ const VALID_REPORT_REASONS = [
1062
+ "abuse",
1063
+ "pii_leak",
1064
+ "illegal_content",
1065
+ "spam",
1066
+ "bug",
1067
+ "other",
1068
+ ];
1069
+ app.post("/sandbox/report", sandboxReportRateLimit, async (req, res) => {
1070
+ try {
1071
+ const body = (req.body ?? {});
1072
+ const reason = body.reason;
1073
+ if (!reason || !VALID_REPORT_REASONS.includes(reason)) {
1074
+ return sendError(res, 400, "VALIDATION_INVALID_FIELD", `reason must be one of: ${VALID_REPORT_REASONS.join(", ")}`);
1075
+ }
1076
+ const description = (body.description ?? "").toString().trim();
1077
+ if (!description) {
1078
+ return sendError(res, 400, "VALIDATION_MISSING_FIELD", "description is required");
1079
+ }
1080
+ const submitterIp = req.ip || "";
1081
+ const transport = resolveSandboxReportTransport();
1082
+ const result = await transport.submit({
1083
+ reason,
1084
+ description,
1085
+ entity_id: body.entity_id,
1086
+ url: body.url,
1087
+ reporter_contact: body.reporter_contact,
1088
+ metadata: body.metadata,
1089
+ }, submitterIp);
1090
+ return res.json(result);
1091
+ }
1092
+ catch (err) {
1093
+ logError("SandboxReportSubmit", req, err);
1094
+ return sendError(res, 500, "SANDBOX_REPORT_ERROR", err.message);
1095
+ }
1096
+ });
1097
+ app.get("/sandbox/report/status", async (req, res) => {
1098
+ try {
1099
+ const accessToken = (req.query.access_token || "").toString().trim();
1100
+ if (!accessToken) {
1101
+ return sendError(res, 400, "VALIDATION_MISSING_FIELD", "access_token is required");
1102
+ }
1103
+ const transport = resolveSandboxReportTransport();
1104
+ const result = await transport.status(accessToken);
1105
+ return res.json(result);
1106
+ }
1107
+ catch (err) {
1108
+ const msg = err.message;
1109
+ if (/not found/i.test(msg)) {
1110
+ return sendError(res, 404, "NOT_FOUND", msg);
1111
+ }
1112
+ logError("SandboxReportStatus", req, err);
1113
+ return sendError(res, 500, "SANDBOX_REPORT_ERROR", msg);
1114
+ }
1115
+ });
1116
+ // ============================================================================
927
1117
  // MCP OAuth Endpoints
928
1118
  // ============================================================================
929
1119
  // Initiate MCP OAuth flow
@@ -1567,6 +1757,17 @@ app.use(async (req, res, next) => {
1567
1757
  logger.info(`[Auth] ${req.method} ${req.path} auth_method=local_no_bearer user_id=${devUser.id}`);
1568
1758
  return next();
1569
1759
  }
1760
+ // Sandbox mode: public deployment at sandbox.neotoma.io where anonymous
1761
+ // callers are attributed to SANDBOX_PUBLIC_USER_ID without a Bearer. AAuth
1762
+ // still runs (earlier in the chain via aauthVerify) so agents exercising the
1763
+ // full AAuth roundtrip get their hardware/software tier. Destructive admin
1764
+ // routes are separately gated by sandboxDestructiveGuard.
1765
+ if (isSandboxMode() && !headerAuth.startsWith("Bearer ")) {
1766
+ const sandboxUser = ensureSandboxPublicUser();
1767
+ req.authenticatedUserId = sandboxUser.id;
1768
+ logger.info(`[Auth] ${req.method} ${req.path} auth_method=sandbox_public user_id=${sandboxUser.id}`);
1769
+ return next();
1770
+ }
1570
1771
  // MCP-style auth (aligns CLI and REST API with MCP). Local requests can skip Bearer; tunnel requires Bearer or OAuth.
1571
1772
  if (config.encryption.enabled) {
1572
1773
  const expectedToken = getMcpAuthToken();
@@ -1651,6 +1852,9 @@ app.use(async (req, res, next) => {
1651
1852
  });
1652
1853
  // Response encryption middleware (applies to all authenticated routes)
1653
1854
  app.use(encryptResponseMiddleware);
1855
+ // Sandbox-mode write gate: destructive routes blocked + tighter per-IP rate
1856
+ // limit on all write methods. No-op outside sandbox.
1857
+ app.use(sandboxWriteGate);
1654
1858
  // Current session (authenticated user details)
1655
1859
  app.get("/me", async (req, res) => {
1656
1860
  try {
@@ -1692,7 +1896,10 @@ async function getAuthenticatedUserId(req, providedUserId) {
1692
1896
  if (authenticatedUserId) {
1693
1897
  if (providedUserId && providedUserId !== authenticatedUserId) {
1694
1898
  // When authenticated as local dev user, allow body/query user_id override for CLI tests and dev flows.
1695
- if (authenticatedUserId === LOCAL_DEV_USER_ID) {
1899
+ // The sandbox public user is intentionally excluded so public sandbox
1900
+ // callers cannot pivot into other users' data by spoofing user_id.
1901
+ if (authenticatedUserId === LOCAL_DEV_USER_ID &&
1902
+ authenticatedUserId !== SANDBOX_PUBLIC_USER_ID) {
1696
1903
  return providedUserId;
1697
1904
  }
1698
1905
  throw new Error(`user_id parameter (${providedUserId}) does not match authenticated user (${authenticatedUserId})`);
@@ -4867,6 +5074,9 @@ app.get("/session", async (req, res) => {
4867
5074
  return handleApiError(req, res, error, "Failed to resolve session identity", "AUTH_REQUIRED", "APIError:session");
4868
5075
  }
4869
5076
  });
5077
+ // /admin/feedback/* — thin proxy to the agent.neotoma.io admin API. See
5078
+ // `src/services/feedback/admin_proxy.ts` for gating + env contract.
5079
+ registerFeedbackAdminProxyRoutes(app);
4870
5080
  // POST /health_check_snapshots - Check for stale entity snapshots
4871
5081
  app.post("/health_check_snapshots", async (req, res) => {
4872
5082
  const schema = z.object({
@@ -5155,6 +5365,20 @@ function tryListen(port) {
5155
5365
  export async function startHTTPServer() {
5156
5366
  // Initialize encryption service
5157
5367
  await initServerKeys();
5368
+ // Sandbox mode: ensure the `sandbox_abuse_report` entity type is registered
5369
+ // before any report comes in so forwarded records can attach cleanly to the
5370
+ // entity graph. Non-sandbox deployments still benefit from having the schema
5371
+ // available in case operators run a self-hosted abuse form.
5372
+ if (isSandboxMode()) {
5373
+ try {
5374
+ const { seedSandboxAbuseReportSchema } = await import("./services/sandbox/seed_schema.js");
5375
+ await seedSandboxAbuseReportSchema();
5376
+ logger.info("[Sandbox] sandbox_abuse_report schema seeded");
5377
+ }
5378
+ catch (err) {
5379
+ logger.warn(`[Sandbox] failed to seed sandbox_abuse_report schema: ${err.message}`);
5380
+ }
5381
+ }
5158
5382
  const httpPortEnv = process.env.NEOTOMA_HTTP_PORT || process.env.HTTP_PORT;
5159
5383
  const basePort = httpPortEnv ? parseInt(httpPortEnv, 10) : config.httpPort || 3080;
5160
5384
  const portFile = process.env.NEOTOMA_SESSION_PORT_FILE;