neotoma 0.19.0 → 0.20.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 +58 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +138 -23
- package/dist/actions.js.map +1 -1
- package/dist/cli/index.d.ts +14 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +87 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/docs/developer/cli_agent_instructions.md +37 -0
- package/dist/docs/developer/mcp/instructions.md +3 -2
- package/dist/docs/subsystems/errors.md +3 -0
- package/dist/docs/testing/automated_test_catalog.md +20 -12
- package/dist/repositories/sqlite/sqlite_client.d.ts.map +1 -1
- package/dist/repositories/sqlite/sqlite_client.js +7 -0
- package/dist/repositories/sqlite/sqlite_client.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +59 -1
- package/dist/server.js.map +1 -1
- package/dist/services/entity_cursor.d.ts +57 -0
- package/dist/services/entity_cursor.d.ts.map +1 -0
- package/dist/services/entity_cursor.js +104 -0
- package/dist/services/entity_cursor.js.map +1 -0
- package/dist/services/entity_queries.d.ts +32 -0
- package/dist/services/entity_queries.d.ts.map +1 -1
- package/dist/services/entity_queries.js +64 -2
- package/dist/services/entity_queries.js.map +1 -1
- package/dist/services/entity_query_limits.d.ts +20 -0
- package/dist/services/entity_query_limits.d.ts.map +1 -0
- package/dist/services/entity_query_limits.js +25 -0
- package/dist/services/entity_query_limits.js.map +1 -0
- package/dist/services/interpretation.d.ts.map +1 -1
- package/dist/services/interpretation.js +17 -0
- package/dist/services/interpretation.js.map +1 -1
- package/dist/services/issues/issue_operations.d.ts +4 -0
- package/dist/services/issues/issue_operations.d.ts.map +1 -1
- package/dist/services/issues/issue_operations.js +18 -0
- package/dist/services/issues/issue_operations.js.map +1 -1
- package/dist/services/issues/neotoma_client.d.ts.map +1 -1
- package/dist/services/issues/neotoma_client.js +10 -5
- package/dist/services/issues/neotoma_client.js.map +1 -1
- package/dist/services/oauth_key_gate.d.ts +15 -1
- package/dist/services/oauth_key_gate.d.ts.map +1 -1
- package/dist/services/oauth_key_gate.js +42 -3
- package/dist/services/oauth_key_gate.js.map +1 -1
- package/dist/services/schema_registry.d.ts +12 -0
- package/dist/services/schema_registry.d.ts.map +1 -1
- package/dist/services/schema_registry.js +19 -0
- package/dist/services/schema_registry.js.map +1 -1
- package/dist/services/snapshot_computation.d.ts +18 -0
- package/dist/services/snapshot_computation.d.ts.map +1 -1
- package/dist/services/snapshot_computation.js +27 -3
- package/dist/services/snapshot_computation.js.map +1 -1
- package/dist/shared/action_handlers/entity_handlers.d.ts +9 -0
- package/dist/shared/action_handlers/entity_handlers.d.ts.map +1 -1
- package/dist/shared/action_handlers/entity_handlers.js +9 -2
- package/dist/shared/action_handlers/entity_handlers.js.map +1 -1
- package/dist/shared/action_schemas.d.ts +89 -20
- package/dist/shared/action_schemas.d.ts.map +1 -1
- package/dist/shared/action_schemas.js +154 -1
- package/dist/shared/action_schemas.js.map +1 -1
- package/dist/shared/openapi_types.d.ts +119 -5
- package/dist/shared/openapi_types.d.ts.map +1 -1
- package/dist/tool_definitions.d.ts.map +1 -1
- package/dist/tool_definitions.js +24 -4
- package/dist/tool_definitions.js.map +1 -1
- package/docs/developer/mcp/instructions.md +3 -2
- package/openapi.yaml +166 -5
- package/package.json +2 -1
package/dist/actions.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import express from "express";
|
|
|
2
2
|
import { type GuestIdentity } from "./services/access_policy.js";
|
|
3
3
|
import { type AgentIdentity } from "./crypto/agent_identity.js";
|
|
4
4
|
import { type NeotomaSandboxModeName } from "./services/sandbox_mode.js";
|
|
5
|
+
import { OAuthKeySessionStore } from "./services/oauth_key_gate.js";
|
|
5
6
|
import { type StoreInterpretationInput } from "./shared/action_schemas.js";
|
|
6
7
|
/** Exposed for the /me route and any other surface that needs the boot-time mode verdict. */
|
|
7
8
|
export declare function getResolvedServerMode(): NeotomaSandboxModeName | null;
|
|
@@ -76,6 +77,51 @@ export declare function isTrustedProxyIP(ip: string, env?: NodeJS.ProcessEnv): b
|
|
|
76
77
|
* entirely and remains valid for single-host deployments without a tunnel.
|
|
77
78
|
*/
|
|
78
79
|
export declare function isLocalRequest(req: express.Request): boolean;
|
|
80
|
+
/** Exported for tests asserting the cookie maxAge and the server-side session
|
|
81
|
+
* expiry agree — the invariant setOAuthKeySessionCookie's docstring names. */
|
|
82
|
+
export declare const oauthKeySessions: OAuthKeySessionStore;
|
|
83
|
+
/**
|
|
84
|
+
* Lifetime of a SIGN-IN session (Google). Distinct from the key-entry gate's
|
|
85
|
+
* short default: a user who signs in expects to stay signed in, whereas the
|
|
86
|
+
* key-entry gate deliberately closes fast. Reusing the 15-minute gate TTL for
|
|
87
|
+
* sign-in is what logged hosted-instance users out every quarter hour (#2005).
|
|
88
|
+
*
|
|
89
|
+
* Override with NEOTOMA_SIGN_IN_SESSION_TTL_MIN (minutes). Default 7 days.
|
|
90
|
+
*/
|
|
91
|
+
export declare const DEFAULT_SIGN_IN_SESSION_TTL_MIN: number;
|
|
92
|
+
/** Upper bound for the sign-in TTL override: 365 days in minutes. A value past
|
|
93
|
+
* this is a typo (an extra zero), not an intent — without a ceiling,
|
|
94
|
+
* "9999999999999" yields a ~19-million-year session. Mirrors the low-end
|
|
95
|
+
* guard rather than guarding only one side (#2007 qa lens). */
|
|
96
|
+
export declare const MAX_SIGN_IN_SESSION_TTL_MIN: number;
|
|
97
|
+
/**
|
|
98
|
+
* Parse NEOTOMA_SIGN_IN_SESSION_TTL_MIN into milliseconds. Exported (and pure)
|
|
99
|
+
* so the override's precedence and malformed-input behavior are directly
|
|
100
|
+
* testable — a silently-wrong TTL here reintroduces exactly the desync class
|
|
101
|
+
* this change fixes.
|
|
102
|
+
*
|
|
103
|
+
* Precedence: an integer within (0, MAX_SIGN_IN_SESSION_TTL_MIN] wins.
|
|
104
|
+
* Everything else — unset, non-numeric, zero, negative, or above the ceiling —
|
|
105
|
+
* falls back to the 7-day default rather than producing a degenerate session.
|
|
106
|
+
*
|
|
107
|
+
* Note on parseInt semantics, deliberately kept: "7.5" truncates to 7 minutes
|
|
108
|
+
* and "1e10" parses as 1 minute (parseInt stops at the 'e'). Both are accepted
|
|
109
|
+
* as-is because they are in-range integers after parsing; the fallback exists
|
|
110
|
+
* for values that are absent, unparseable, or out of range, not to second-guess
|
|
111
|
+
* a caller who wrote a decimal. Asserted in tests so the behavior is documented
|
|
112
|
+
* rather than incidental.
|
|
113
|
+
*/
|
|
114
|
+
export declare function resolveSignInSessionTtlMs(raw: string | undefined): number;
|
|
115
|
+
/**
|
|
116
|
+
* Issue an OAuth key-session cookie. `ttlMs` covers BOTH the server-side
|
|
117
|
+
* session and the cookie's own maxAge — they must agree, or the browser keeps
|
|
118
|
+
* presenting a cookie the server has already forgotten (or vice versa).
|
|
119
|
+
* Omit it for the short key-entry gate; pass SIGN_IN_SESSION_TTL_MS for
|
|
120
|
+
* sign-in (#2005).
|
|
121
|
+
*/
|
|
122
|
+
export declare function setOAuthKeySessionCookie(req: express.Request, res: express.Response, ttlMs?: number): string;
|
|
123
|
+
/** Resolve the local-auth user_id a Google-verified session (if any) mapped to this request. */
|
|
124
|
+
export declare function getGoogleVerifiedUserId(req: express.Request): string | undefined;
|
|
79
125
|
/**
|
|
80
126
|
* Derive the canonical `@authority` the AAuth profile requires (AAuth §10.3.1).
|
|
81
127
|
* We trust explicit configuration first, then fall back to the host the
|
|
@@ -111,6 +157,18 @@ export declare function routeAllowsLocalIssueWriteFallback(req: Pick<express.Req
|
|
|
111
157
|
export declare function routeConsumesGuestWriteBudget(req: Pick<express.Request, "method" | "path">): boolean;
|
|
112
158
|
/** Exported for unit tests: guest-rate-limit key precedence is thumbprint -> guest token -> IP. */
|
|
113
159
|
export declare function guestWriteRateLimitKey(req: express.Request): string;
|
|
160
|
+
/**
|
|
161
|
+
* Issue *submission* is the one route a third party may reach with no identity
|
|
162
|
+
* at all: the guest access token is minted BY the submit, so requiring one to
|
|
163
|
+
* perform it is a chicken-and-egg that closes the inbox to first contact.
|
|
164
|
+
*
|
|
165
|
+
* Deliberately submit-only. `add_message` stays authenticated: by the time a
|
|
166
|
+
* reporter has a thread to append to, they hold the token returned by their
|
|
167
|
+
* submit, so anonymity buys nothing there — and letting an unidentified caller
|
|
168
|
+
* write into an existing thread is a strictly wider blast radius than letting
|
|
169
|
+
* them open a new one. The token is what proves "I opened this thread".
|
|
170
|
+
*/
|
|
171
|
+
export declare function routeAcceptsAnonymousIssueSubmit(req: Pick<express.Request, "method" | "path">): boolean;
|
|
114
172
|
/**
|
|
115
173
|
* Derive a userId for a guest principal from the validated token grant.
|
|
116
174
|
*
|
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":"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"}
|
package/dist/actions.js
CHANGED
|
@@ -22,6 +22,7 @@ import { aauthAdmission, getAAuthAdmissionFromRequest } from "./middleware/aauth
|
|
|
22
22
|
import { buildSessionInfo, normalizeSessionOrigin } from "./services/session_info.js";
|
|
23
23
|
import { AttributionPolicyError, enforceAttributionPolicy } from "./services/attribution_policy.js";
|
|
24
24
|
import { OverridePolicyViolationError } from "./services/override_validation.js";
|
|
25
|
+
import { CursorError } from "./services/entity_cursor.js";
|
|
25
26
|
import { AgentCapabilityError, contextFromAgentIdentity, enforceAgentCapability, } from "./services/agent_capabilities.js";
|
|
26
27
|
import { assertGuestWriteAllowed, AccessPolicyError, } from "./services/access_policy.js";
|
|
27
28
|
import { hashGuestAccessToken } from "./services/guest_access_token.js";
|
|
@@ -1096,17 +1097,47 @@ export function isLocalRequest(req) {
|
|
|
1096
1097
|
return !isProductionEnvironment();
|
|
1097
1098
|
}
|
|
1098
1099
|
const OAUTH_KEY_SESSION_COOKIE = "neotoma_oauth_key_session";
|
|
1099
|
-
|
|
1100
|
+
/** Exported for tests asserting the cookie maxAge and the server-side session
|
|
1101
|
+
* expiry agree — the invariant setOAuthKeySessionCookie's docstring names. */
|
|
1102
|
+
export const oauthKeySessions = new OAuthKeySessionStore();
|
|
1100
1103
|
/**
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1104
|
-
*
|
|
1105
|
-
*
|
|
1106
|
-
*
|
|
1107
|
-
|
|
1104
|
+
* Lifetime of a SIGN-IN session (Google). Distinct from the key-entry gate's
|
|
1105
|
+
* short default: a user who signs in expects to stay signed in, whereas the
|
|
1106
|
+
* key-entry gate deliberately closes fast. Reusing the 15-minute gate TTL for
|
|
1107
|
+
* sign-in is what logged hosted-instance users out every quarter hour (#2005).
|
|
1108
|
+
*
|
|
1109
|
+
* Override with NEOTOMA_SIGN_IN_SESSION_TTL_MIN (minutes). Default 7 days.
|
|
1110
|
+
*/
|
|
1111
|
+
export const DEFAULT_SIGN_IN_SESSION_TTL_MIN = 7 * 24 * 60;
|
|
1112
|
+
/** Upper bound for the sign-in TTL override: 365 days in minutes. A value past
|
|
1113
|
+
* this is a typo (an extra zero), not an intent — without a ceiling,
|
|
1114
|
+
* "9999999999999" yields a ~19-million-year session. Mirrors the low-end
|
|
1115
|
+
* guard rather than guarding only one side (#2007 qa lens). */
|
|
1116
|
+
export const MAX_SIGN_IN_SESSION_TTL_MIN = 365 * 24 * 60;
|
|
1117
|
+
/**
|
|
1118
|
+
* Parse NEOTOMA_SIGN_IN_SESSION_TTL_MIN into milliseconds. Exported (and pure)
|
|
1119
|
+
* so the override's precedence and malformed-input behavior are directly
|
|
1120
|
+
* testable — a silently-wrong TTL here reintroduces exactly the desync class
|
|
1121
|
+
* this change fixes.
|
|
1122
|
+
*
|
|
1123
|
+
* Precedence: an integer within (0, MAX_SIGN_IN_SESSION_TTL_MIN] wins.
|
|
1124
|
+
* Everything else — unset, non-numeric, zero, negative, or above the ceiling —
|
|
1125
|
+
* falls back to the 7-day default rather than producing a degenerate session.
|
|
1126
|
+
*
|
|
1127
|
+
* Note on parseInt semantics, deliberately kept: "7.5" truncates to 7 minutes
|
|
1128
|
+
* and "1e10" parses as 1 minute (parseInt stops at the 'e'). Both are accepted
|
|
1129
|
+
* as-is because they are in-range integers after parsing; the fallback exists
|
|
1130
|
+
* for values that are absent, unparseable, or out of range, not to second-guess
|
|
1131
|
+
* a caller who wrote a decimal. Asserted in tests so the behavior is documented
|
|
1132
|
+
* rather than incidental.
|
|
1108
1133
|
*/
|
|
1109
|
-
|
|
1134
|
+
export function resolveSignInSessionTtlMs(raw) {
|
|
1135
|
+
const parsed = Number.parseInt(raw || "", 10);
|
|
1136
|
+
const inRange = Number.isFinite(parsed) && parsed > 0 && parsed <= MAX_SIGN_IN_SESSION_TTL_MIN;
|
|
1137
|
+
const minutes = inRange ? parsed : DEFAULT_SIGN_IN_SESSION_TTL_MIN;
|
|
1138
|
+
return minutes * 60 * 1000;
|
|
1139
|
+
}
|
|
1140
|
+
const SIGN_IN_SESSION_TTL_MS = resolveSignInSessionTtlMs(process.env.NEOTOMA_SIGN_IN_SESSION_TTL_MIN);
|
|
1110
1141
|
function readCookie(req, name) {
|
|
1111
1142
|
const header = (req.headers["cookie"] || req.headers["Cookie"] || "");
|
|
1112
1143
|
if (!header)
|
|
@@ -1332,8 +1363,15 @@ function hasValidOAuthKeySession(req) {
|
|
|
1332
1363
|
const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
|
|
1333
1364
|
return oauthKeySessions.isValid(token);
|
|
1334
1365
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1366
|
+
/**
|
|
1367
|
+
* Issue an OAuth key-session cookie. `ttlMs` covers BOTH the server-side
|
|
1368
|
+
* session and the cookie's own maxAge — they must agree, or the browser keeps
|
|
1369
|
+
* presenting a cookie the server has already forgotten (or vice versa).
|
|
1370
|
+
* Omit it for the short key-entry gate; pass SIGN_IN_SESSION_TTL_MS for
|
|
1371
|
+
* sign-in (#2005).
|
|
1372
|
+
*/
|
|
1373
|
+
export function setOAuthKeySessionCookie(req, res, ttlMs) {
|
|
1374
|
+
const token = oauthKeySessions.create(Date.now(), ttlMs);
|
|
1337
1375
|
const forwardedProto = (req.headers["x-forwarded-proto"] || req.headers["X-Forwarded-Proto"])
|
|
1338
1376
|
?.split(",")[0]
|
|
1339
1377
|
?.trim()
|
|
@@ -1344,16 +1382,16 @@ function setOAuthKeySessionCookie(req, res) {
|
|
|
1344
1382
|
sameSite: "lax",
|
|
1345
1383
|
secure,
|
|
1346
1384
|
path: "/mcp/oauth",
|
|
1347
|
-
maxAge: 15 * 60 * 1000,
|
|
1385
|
+
maxAge: ttlMs != null && ttlMs > 0 ? ttlMs : 15 * 60 * 1000,
|
|
1348
1386
|
});
|
|
1349
1387
|
return token;
|
|
1350
1388
|
}
|
|
1351
1389
|
/** Resolve the local-auth user_id a Google-verified session (if any) mapped to this request. */
|
|
1352
|
-
function getGoogleVerifiedUserId(req) {
|
|
1390
|
+
export function getGoogleVerifiedUserId(req) {
|
|
1353
1391
|
const token = readCookie(req, OAUTH_KEY_SESSION_COOKIE);
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
return
|
|
1392
|
+
// getBoundUser enforces session validity itself, so an expired session can
|
|
1393
|
+
// never resolve a user even if a binding is still in memory.
|
|
1394
|
+
return oauthKeySessions.getBoundUser(token);
|
|
1357
1395
|
}
|
|
1358
1396
|
/**
|
|
1359
1397
|
* Derive the canonical `@authority` the AAuth profile requires (AAuth §10.3.1).
|
|
@@ -1905,9 +1943,32 @@ function sendStoreValidationError(res, issues) {
|
|
|
1905
1943
|
}
|
|
1906
1944
|
return sendValidationError(res, issues);
|
|
1907
1945
|
}
|
|
1946
|
+
/**
|
|
1947
|
+
* Lift a structured `hint` off the first Zod issue that carries one.
|
|
1948
|
+
*
|
|
1949
|
+
* Zod nests custom issue metadata under `params`, so a `ctx.addIssue({ params:
|
|
1950
|
+
* { hint } })` lands at `details.issues[].params.hint`. Clients (and the
|
|
1951
|
+
* legacy-payload replay harness, per docs/subsystems/errors.md § Tightening-change
|
|
1952
|
+
* hint obligation) read the migration hint from the flat `details.hint` slot that
|
|
1953
|
+
* every other standard-envelope error uses. Without this lift, a tightening can
|
|
1954
|
+
* populate `hint` exactly as the guardrail requires and still be invisible to the
|
|
1955
|
+
* gate meant to catch it.
|
|
1956
|
+
*/
|
|
1957
|
+
function firstIssueHint(issues) {
|
|
1958
|
+
if (!Array.isArray(issues))
|
|
1959
|
+
return undefined;
|
|
1960
|
+
for (const issue of issues) {
|
|
1961
|
+
const hint = issue?.params?.hint;
|
|
1962
|
+
if (typeof hint === "string" && hint.length > 0)
|
|
1963
|
+
return hint;
|
|
1964
|
+
}
|
|
1965
|
+
return undefined;
|
|
1966
|
+
}
|
|
1908
1967
|
function sendValidationError(res, issues) {
|
|
1968
|
+
const hint = firstIssueHint(issues);
|
|
1909
1969
|
return res.status(400).json(buildErrorEnvelope("VALIDATION_INVALID_FORMAT", "Invalid request payload.", {
|
|
1910
1970
|
issues,
|
|
1971
|
+
...(hint ? { hint } : {}),
|
|
1911
1972
|
}));
|
|
1912
1973
|
}
|
|
1913
1974
|
// Public health endpoint (no auth)
|
|
@@ -2324,8 +2385,10 @@ app.get("/mcp/oauth/google/callback", async (req, res) => {
|
|
|
2324
2385
|
// mnemonic/bearer path sets. `/mcp/oauth/authorize` -> `/mcp/oauth/
|
|
2325
2386
|
// local-login` then runs completely unmodified, except local-login now
|
|
2326
2387
|
// resolves the user via this session instead of always ensureLocalDevUser().
|
|
2327
|
-
|
|
2328
|
-
|
|
2388
|
+
// Sign-in gets the long session TTL, not the key-entry gate's 15 minutes,
|
|
2389
|
+
// and the user binding lives in the same store so it expires with it (#2005).
|
|
2390
|
+
const sessionToken = setOAuthKeySessionCookie(req, res, SIGN_IN_SESSION_TTL_MS);
|
|
2391
|
+
oauthKeySessions.bindUser(sessionToken, resolvedUserId);
|
|
2329
2392
|
return res.redirect(nextPath);
|
|
2330
2393
|
}
|
|
2331
2394
|
catch (error) {
|
|
@@ -2872,6 +2935,23 @@ function guestWriteRateLimit(req, res, next) {
|
|
|
2872
2935
|
return next();
|
|
2873
2936
|
void guestWriteRateLimitMiddleware(req, res, next);
|
|
2874
2937
|
}
|
|
2938
|
+
/**
|
|
2939
|
+
* Issue *submission* is the one route a third party may reach with no identity
|
|
2940
|
+
* at all: the guest access token is minted BY the submit, so requiring one to
|
|
2941
|
+
* perform it is a chicken-and-egg that closes the inbox to first contact.
|
|
2942
|
+
*
|
|
2943
|
+
* Deliberately submit-only. `add_message` stays authenticated: by the time a
|
|
2944
|
+
* reporter has a thread to append to, they hold the token returned by their
|
|
2945
|
+
* submit, so anonymity buys nothing there — and letting an unidentified caller
|
|
2946
|
+
* write into an existing thread is a strictly wider blast radius than letting
|
|
2947
|
+
* them open a new one. The token is what proves "I opened this thread".
|
|
2948
|
+
*/
|
|
2949
|
+
export function routeAcceptsAnonymousIssueSubmit(req) {
|
|
2950
|
+
return (req.method === "POST" && (req.path === "/issues/submit" || req.path === "/api/issues/submit"));
|
|
2951
|
+
}
|
|
2952
|
+
function buildAnonymousGuestPrincipal() {
|
|
2953
|
+
return { kind: "guest", guestId: {}, agentIdentity: null, accessToken: undefined };
|
|
2954
|
+
}
|
|
2875
2955
|
function buildGuestPrincipalFromRequest(req) {
|
|
2876
2956
|
const agentIdentity = getCurrentAgentIdentity();
|
|
2877
2957
|
const accessToken = guestAccessTokenFromRequest(req);
|
|
@@ -2895,8 +2975,21 @@ async function maybeStampGuestPrincipal(req) {
|
|
|
2895
2975
|
if (!routeAcceptsGuestPrincipal(req))
|
|
2896
2976
|
return false;
|
|
2897
2977
|
const guestPrincipal = buildGuestPrincipalFromRequest(req);
|
|
2898
|
-
if (!guestPrincipal)
|
|
2899
|
-
|
|
2978
|
+
if (!guestPrincipal) {
|
|
2979
|
+
// No AAuth signature and no guest token: for issue submission this is the
|
|
2980
|
+
// NORMAL first contact from a third party's agent, since the token is an
|
|
2981
|
+
// OUTPUT of the submit rather than a precondition for it (#1953). Remains
|
|
2982
|
+
// the operator's switch — NEOTOMA_ACCESS_POLICY_ISSUE=closed shuts it.
|
|
2983
|
+
if (!routeAcceptsAnonymousIssueSubmit(req))
|
|
2984
|
+
return false;
|
|
2985
|
+
const { resolveAccessPolicy } = await import("./services/access_policy.js");
|
|
2986
|
+
const mode = await resolveAccessPolicy("issue");
|
|
2987
|
+
if (mode !== "submit_only" && mode !== "submitter_scoped" && mode !== "open") {
|
|
2988
|
+
return false;
|
|
2989
|
+
}
|
|
2990
|
+
stampGuestPrincipal(req, buildAnonymousGuestPrincipal());
|
|
2991
|
+
return true;
|
|
2992
|
+
}
|
|
2900
2993
|
const headerAuth = (req.headers.authorization || req.headers.Authorization || "");
|
|
2901
2994
|
if (headerAuth.startsWith("Bearer ") && guestPrincipal.accessToken) {
|
|
2902
2995
|
const { validateGuestAccessToken } = await import("./services/guest_access_token.js");
|
|
@@ -3459,6 +3552,12 @@ function handleApiError(req, res, error, defaultMessage, errorCode = "DB_QUERY_F
|
|
|
3459
3552
|
.status(403)
|
|
3460
3553
|
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
3461
3554
|
}
|
|
3555
|
+
if (error instanceof CursorError) {
|
|
3556
|
+
logWarn(logContext || "CursorRejection", req, error.toErrorEnvelope());
|
|
3557
|
+
return res
|
|
3558
|
+
.status(error.statusCode)
|
|
3559
|
+
.json(buildErrorEnvelope(error.code, error.message, error.toErrorEnvelope()));
|
|
3560
|
+
}
|
|
3462
3561
|
if (error instanceof IssueValidationError) {
|
|
3463
3562
|
logWarn(logContext || "IssueValidationError", req, {
|
|
3464
3563
|
code: error.code,
|
|
@@ -3509,8 +3608,8 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
3509
3608
|
// scoping only via getAuthenticatedUserId; it is never read directly off
|
|
3510
3609
|
// the request for access control.
|
|
3511
3610
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
3512
|
-
const { entity_type, entity_types, search, limit, offset, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
|
|
3513
|
-
const { entities, total, applied_search_strategies, search_mode } = await queryEntitiesWithCount({
|
|
3611
|
+
const { entity_type, entity_types, search, limit, offset, cursor, sort_by, sort_order, published, published_after, published_before, include_snapshots, include_merged, updated_since, created_since, identity_basis, snapshot_filters, exclude_bookkeeping, } = parsed.data;
|
|
3612
|
+
const { entities, total, applied_search_strategies, search_mode, next_cursor } = await queryEntitiesWithCount({
|
|
3514
3613
|
userId,
|
|
3515
3614
|
entityType: entity_type,
|
|
3516
3615
|
entityTypes: entity_types,
|
|
@@ -3524,6 +3623,7 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
3524
3623
|
search,
|
|
3525
3624
|
limit,
|
|
3526
3625
|
offset,
|
|
3626
|
+
cursor,
|
|
3527
3627
|
updatedSince: updated_since,
|
|
3528
3628
|
createdSince: created_since,
|
|
3529
3629
|
identityBasis: identity_basis,
|
|
@@ -3537,6 +3637,7 @@ async function runEntitiesQuery(req, res, rawInput) {
|
|
|
3537
3637
|
offset,
|
|
3538
3638
|
...(applied_search_strategies ? { applied_search_strategies } : {}),
|
|
3539
3639
|
search_mode,
|
|
3640
|
+
...(next_cursor ? { next_cursor } : {}),
|
|
3540
3641
|
});
|
|
3541
3642
|
}
|
|
3542
3643
|
catch (error) {
|
|
@@ -3577,6 +3678,7 @@ function coerceEntitiesQueryParams(query) {
|
|
|
3577
3678
|
"updated_since",
|
|
3578
3679
|
"created_since",
|
|
3579
3680
|
"identity_basis",
|
|
3681
|
+
"cursor",
|
|
3580
3682
|
]) {
|
|
3581
3683
|
const value = readString(key);
|
|
3582
3684
|
if (value !== undefined)
|
|
@@ -8074,6 +8176,10 @@ const handleIssuesSubmitHttp = async (req, res) => {
|
|
|
8074
8176
|
author: parsed.data.author,
|
|
8075
8177
|
local_issue_id: parsed.data.local_issue_id,
|
|
8076
8178
|
submission_timestamp: parsed.data.submission_timestamp,
|
|
8179
|
+
reporter_git_sha: parsed.data.reporter_git_sha,
|
|
8180
|
+
reporter_git_ref: parsed.data.reporter_git_ref,
|
|
8181
|
+
reporter_channel: parsed.data.reporter_channel,
|
|
8182
|
+
reporter_app_version: parsed.data.reporter_app_version,
|
|
8077
8183
|
});
|
|
8078
8184
|
return {
|
|
8079
8185
|
issue_number: 0,
|
|
@@ -8456,7 +8562,7 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
8456
8562
|
}
|
|
8457
8563
|
try {
|
|
8458
8564
|
const userId = await getAuthenticatedUserId(req, parsed.data.user_id);
|
|
8459
|
-
const { entity_type, fields_to_add, fields_to_remove, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
|
|
8565
|
+
const { entity_type, fields_to_add, fields_to_remove, canonical_name_fields, schema_version, user_specific = false, activate = true, migrate_existing = false, force = false, } = parsed.data;
|
|
8460
8566
|
const { schemaRegistry } = await import("./services/schema_registry.js");
|
|
8461
8567
|
let newSchema;
|
|
8462
8568
|
try {
|
|
@@ -8464,6 +8570,11 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
8464
8570
|
entity_type,
|
|
8465
8571
|
fields_to_add: (fields_to_add || []),
|
|
8466
8572
|
fields_to_remove: fields_to_remove || [],
|
|
8573
|
+
// #2018: thread the identity-rule change through the HTTP/offline path.
|
|
8574
|
+
// Omitted here originally, so CLI/HTTP callers silently no-op'd the
|
|
8575
|
+
// re-key (the MCP server handler had it, but this Express route — which
|
|
8576
|
+
// the CLI offline transport uses — did not).
|
|
8577
|
+
canonical_name_fields,
|
|
8467
8578
|
schema_version,
|
|
8468
8579
|
user_specific,
|
|
8469
8580
|
user_id: userId,
|
|
@@ -8490,6 +8601,10 @@ app.post("/update_schema_incremental", async (req, res) => {
|
|
|
8490
8601
|
schema: newSchema,
|
|
8491
8602
|
schema_version: newSchema.schema_version,
|
|
8492
8603
|
fields_removed: fields_to_remove || [],
|
|
8604
|
+
// Echo the resolved identity rule so CLI/HTTP callers can confirm the
|
|
8605
|
+
// re-key without a second describe_entity_type round trip (#2020 ux).
|
|
8606
|
+
canonical_name_fields: newSchema.schema_definition
|
|
8607
|
+
.canonical_name_fields ?? null,
|
|
8493
8608
|
});
|
|
8494
8609
|
}
|
|
8495
8610
|
catch (error) {
|