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.
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +226 -2
- package/dist/actions.js.map +1 -1
- package/dist/cli/bootstrap.js +0 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -17
- package/dist/cli/index.js.map +1 -1
- package/dist/scripts/reset_sandbox.js +180 -0
- package/dist/scripts/reset_sandbox.js.map +1 -0
- package/dist/scripts/seed_sandbox.js +254 -0
- package/dist/scripts/seed_sandbox.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +28 -1
- package/dist/server.js.map +1 -1
- package/dist/services/feedback/admin_proxy.d.ts +59 -0
- package/dist/services/feedback/admin_proxy.d.ts.map +1 -0
- package/dist/services/feedback/admin_proxy.js +166 -0
- package/dist/services/feedback/admin_proxy.js.map +1 -0
- package/dist/services/feedback/seed_schema.d.ts.map +1 -1
- package/dist/services/feedback/seed_schema.js +5 -0
- package/dist/services/feedback/seed_schema.js.map +1 -1
- package/dist/services/local_auth.d.ts +14 -0
- package/dist/services/local_auth.d.ts.map +1 -1
- package/dist/services/local_auth.js +31 -0
- package/dist/services/local_auth.js.map +1 -1
- package/dist/services/root_landing/harness_snippets.d.ts +70 -0
- package/dist/services/root_landing/harness_snippets.d.ts.map +1 -0
- package/dist/services/root_landing/harness_snippets.js +279 -0
- package/dist/services/root_landing/harness_snippets.js.map +1 -0
- package/dist/services/root_landing/html_template.d.ts +33 -0
- package/dist/services/root_landing/html_template.d.ts.map +1 -0
- package/dist/services/root_landing/html_template.js +370 -0
- package/dist/services/root_landing/html_template.js.map +1 -0
- package/dist/services/root_landing/index.d.ts +58 -0
- package/dist/services/root_landing/index.d.ts.map +1 -0
- package/dist/services/root_landing/index.js +261 -0
- package/dist/services/root_landing/index.js.map +1 -0
- package/dist/services/root_landing/md_template.d.ts +7 -0
- package/dist/services/root_landing/md_template.d.ts.map +1 -0
- package/dist/services/root_landing/md_template.js +116 -0
- package/dist/services/root_landing/md_template.js.map +1 -0
- package/dist/services/root_landing/site_nav.d.ts +32 -0
- package/dist/services/root_landing/site_nav.d.ts.map +1 -0
- package/dist/services/root_landing/site_nav.js +116 -0
- package/dist/services/root_landing/site_nav.js.map +1 -0
- package/dist/services/sandbox/local_store.d.ts +47 -0
- package/dist/services/sandbox/local_store.d.ts.map +1 -0
- package/dist/services/sandbox/local_store.js +92 -0
- package/dist/services/sandbox/local_store.js.map +1 -0
- package/dist/services/sandbox/seed_schema.d.ts +21 -0
- package/dist/services/sandbox/seed_schema.d.ts.map +1 -0
- package/dist/services/sandbox/seed_schema.js +97 -0
- package/dist/services/sandbox/seed_schema.js.map +1 -0
- package/dist/services/sandbox/terms.d.ts +18 -0
- package/dist/services/sandbox/terms.d.ts.map +1 -0
- package/dist/services/sandbox/terms.js +19 -0
- package/dist/services/sandbox/terms.js.map +1 -0
- package/dist/services/sandbox/transport.d.ts +33 -0
- package/dist/services/sandbox/transport.d.ts.map +1 -0
- package/dist/services/sandbox/transport.js +174 -0
- package/dist/services/sandbox/transport.js.map +1 -0
- package/dist/services/sandbox/types.d.ts +47 -0
- package/dist/services/sandbox/types.d.ts.map +1 -0
- package/dist/services/sandbox/types.js +18 -0
- package/dist/services/sandbox/types.js.map +1 -0
- package/dist/services/sandbox_mode.d.ts +28 -0
- package/dist/services/sandbox_mode.d.ts.map +1 -0
- package/dist/services/sandbox_mode.js +69 -0
- package/dist/services/sandbox_mode.js.map +1 -0
- package/dist/shared/sandbox_terms_content.d.ts +14 -0
- package/dist/shared/sandbox_terms_content.d.ts.map +1 -0
- package/dist/shared/sandbox_terms_content.js +57 -0
- package/dist/shared/sandbox_terms_content.js.map +1 -0
- package/package.json +3 -2
- package/dist/services/llm_extraction.d.ts +0 -82
- package/dist/services/llm_extraction.d.ts.map +0 -1
- package/dist/services/llm_extraction.js +0 -435
- package/dist/services/llm_extraction.js.map +0 -1
package/dist/actions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;
|
|
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
|
-
|
|
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;
|