sdlc-agent-4-enterprise-server 1.55.8 → 1.55.9
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/Dockerfile +9 -9
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/modules/pega/ingest-error-classifier.d.ts +37 -0
- package/dist/modules/pega/ingest-error-classifier.js +77 -0
- package/dist/modules/pega/ingest-error-classifier.js.map +1 -0
- package/dist/server/HttpServer.js +11 -0
- package/dist/server/HttpServer.js.map +1 -1
- package/dist/server/routes/pega-ingest-rule.js +105 -62
- package/dist/server/routes/pega-ingest-rule.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +15 -0
- package/src/modules/pega/ingest-error-classifier.ts +128 -0
- package/src/server/HttpServer.ts +11 -0
- package/src/server/routes/pega-ingest-rule.ts +69 -17
package/Dockerfile
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# syntax=docker/dockerfile:1
|
|
2
2
|
# =============================================================================
|
|
3
|
-
# SA4E-335 Backend Dockerfile — Node.js 22 + Hono Code Intelligence MCP Server
|
|
3
|
+
# SA4E-335 Backend Dockerfile — Node.js 22.23.4 + Hono Code Intelligence MCP Server
|
|
4
4
|
# -----------------------------------------------------------------------------
|
|
5
5
|
# Multi-stage build (7 stages): base, deps, build, mcp-python, production,
|
|
6
6
|
# development, test. `production` is the SOLE CI push target (TDD AD-2).
|
|
7
7
|
# Build-only tooling never reaches the runtime image.
|
|
8
8
|
#
|
|
9
9
|
# Design decisions (TDD §3.2 / Security Review):
|
|
10
|
-
# - AD-1 : base = node:22-slim (Debian/glibc) so native modules such as
|
|
10
|
+
# - AD-1 : base = node:22.23.4-slim (Debian/glibc) so native modules such as
|
|
11
11
|
# onnxruntime-node link against glibc (musl/alpine is incompatible).
|
|
12
12
|
# - SEC-02 (Critical): base image pinned by immutable sha256 DIGEST, not only
|
|
13
13
|
# the mutable tag, to keep the build hermetic/reproducible. The human
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
# =============================================================================
|
|
30
30
|
|
|
31
31
|
# --- Build-time arguments (NO secrets — build args must never carry credentials, BR-17) ---
|
|
32
|
-
ARG NODE_VERSION=22
|
|
33
|
-
# node:22-slim — pin by digest for a reproducible supply chain (SEC-02).
|
|
32
|
+
ARG NODE_VERSION=22.23.4
|
|
33
|
+
# node:22.23.4-slim — pin by digest for a reproducible supply chain (SEC-02).
|
|
34
34
|
# Tag kept for readability; the digest is the authoritative pin.
|
|
35
|
-
ARG NODE_BASE_DIGEST=sha256:
|
|
35
|
+
ARG NODE_BASE_DIGEST=sha256:de170b39439eb0e5a3444ad7d60b015614a381e10fecae2e30ec9e735122f38d
|
|
36
36
|
|
|
37
37
|
# -----------------------------------------------------------------------------
|
|
38
38
|
# base — shared workdir + build toolchain (glibc). Build-time only.
|
|
@@ -173,7 +173,7 @@ RUN python3 -m venv /opt/markdown-exporter-venv \
|
|
|
173
173
|
|
|
174
174
|
# -----------------------------------------------------------------------------
|
|
175
175
|
# production ★ — runtime image. SOLE CI push target. Minimal surface, non-root.
|
|
176
|
-
# No git, no compilers (SEC-05). node:22-slim pinned by digest (SEC-02).
|
|
176
|
+
# No git, no compilers (SEC-05). node:22.23.4-slim pinned by digest (SEC-02).
|
|
177
177
|
# -----------------------------------------------------------------------------
|
|
178
178
|
FROM node:${NODE_VERSION}-slim@${NODE_BASE_DIGEST} AS production
|
|
179
179
|
WORKDIR /app
|
|
@@ -182,7 +182,7 @@ WORKDIR /app
|
|
|
182
182
|
# npm CLI that the base image bundles under /usr/local/lib/node_modules/npm.
|
|
183
183
|
# Those copies (pacote, sigstore, brace-expansion, picomatch, ip-address,
|
|
184
184
|
# undici) are NOT reachable from backend/package-lock.json, and the pinned
|
|
185
|
-
# node:22-slim digest is already the latest — so the fix is to bump the CLI
|
|
185
|
+
# node:22.23.4-slim digest is already the latest — so the fix is to bump the CLI
|
|
186
186
|
# itself (10.9.9 → NPM_VERSION: pacote/sigstore majors land on patched lines)
|
|
187
187
|
# and refresh the two packages upstream has not re-released yet, using
|
|
188
188
|
# same-major PATCH releases only (5.0.9→5.0.12, 6.28.0→6.28.1).
|
|
@@ -206,7 +206,7 @@ RUN npm install -g npm@${NPM_VERSION} --no-audit --no-fund --loglevel=error \
|
|
|
206
206
|
# SEC-04: upgrade Debian packages to fix CVE-2026-103111 (libpcre2-8-0 HIGH)
|
|
207
207
|
# and the perl CVE set found by Trivy on 2026-10-10 (CVE-2026-13221 CRITICAL,
|
|
208
208
|
# CVE-2026-42497 HIGH, CVE-2026-42496, CVE-2026-8376, CVE-2026-48962,
|
|
209
|
-
# CVE-2026-57432, CVE-2026-57433 — perl-base/perl-modules in node:
|
|
209
|
+
# CVE-2026-57432, CVE-2026-57433 — perl-base/perl-modules in node:22.23.4-slim)
|
|
210
210
|
&& apt-get update \
|
|
211
211
|
&& apt-get upgrade -y --no-install-recommends libpcre2-8-0 perl-base perl-modules-5.36 \
|
|
212
212
|
&& apt-get clean \
|
|
@@ -265,7 +265,7 @@ ENV INDEX_CONCURRENCY_LIMIT=8
|
|
|
265
265
|
|
|
266
266
|
EXPOSE 48721
|
|
267
267
|
|
|
268
|
-
# Dependency-free health probe (SEC-12): node:22-slim does not ship `wget`, so a
|
|
268
|
+
# Dependency-free health probe (SEC-12): node:22.23.4-slim does not ship `wget`, so a
|
|
269
269
|
# wget-based probe would silently fail. Use node's built-in http client instead.
|
|
270
270
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
271
271
|
CMD node -e "require('http').get('http://localhost:'+(process.env.PORT||48721)+'/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"
|
package/dist/index.js
CHANGED
|
@@ -105,6 +105,20 @@ async function main() {
|
|
|
105
105
|
};
|
|
106
106
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
107
107
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
108
|
+
// Global crash guards: a single rejected/uncaught error on an async path (e.g.
|
|
109
|
+
// Pega ingest) must NOT silently kill the process. Without these, Node tears the
|
|
110
|
+
// process down and EVERY in-flight HTTP socket fails at once as
|
|
111
|
+
// `UND_ERR_SOCKET "other side closed"` on the client — the exact Pega-ingest symptom.
|
|
112
|
+
// We log with full context so the real throw site is attributable instead of invisible.
|
|
113
|
+
process.on('unhandledRejection', (reason) => {
|
|
114
|
+
const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { reason };
|
|
115
|
+
logger.error(err, '[fatal] Unhandled promise rejection — process kept alive for diagnosis');
|
|
116
|
+
});
|
|
117
|
+
process.on('uncaughtException', (err) => {
|
|
118
|
+
logger.error({ message: err.message, stack: err.stack }, '[fatal] Uncaught exception — process kept alive for diagnosis');
|
|
119
|
+
// Intentionally NOT exiting: an isolated ingest-path throw should not drop all
|
|
120
|
+
// concurrent connections. Fatal startup errors are still caught by main().catch.
|
|
121
|
+
});
|
|
108
122
|
logger.info({ pid: process.pid, port: config.port, version: VERSION }, 'Backend MCP Server ready');
|
|
109
123
|
}
|
|
110
124
|
main().catch((err) => {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,IAAI,MAAM,cAAc,CAAC;AAChC,OAAO,KAAK,EAAE,MAAM,IAAI,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACpF,OAAO,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACpF,OAAO,EAAE,aAAa,EAAE,MAAM,+CAA+C,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,sBAAsB,EAAE,MAAM,sCAAsC,CAAC;AAE9E,MAAM,OAAO,GAAG,OAAO,CAAC;AACxB,MAAM,UAAU,GAAG,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;AAE7C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAE5B,MAAM,MAAM,GAAG,IAAI,CAAC;QAClB,KAAK,EAAE,MAAM,CAAC,QAAQ;QACtB,SAAS,EAAE,EAAE,MAAM,EAAE,WAAW,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE;KAChE,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,EAAE,EAAE,6BAA6B,CAAC,CAAC;IAEnH,6BAA6B;IAC7B,MAAM,SAAS,GAAG,IAAI,SAAS,EAAE,CAAC;IAClC,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC7C,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IAC7C,SAAS,CAAC,gBAAgB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IAE/C,iCAAiC;IACjC,MAAM,YAAY,EAAE,CAAC;IAErB,uEAAuE;IACvE,IAAI,CAAC;QACH,MAAM,mBAAmB,EAAE,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,sDAAsD,CAAC,CAAC;IAChF,CAAC;IAED,iEAAiE;IACjE,6DAA6D;IAC7D,mEAAmE;IACnE,IAAI,CAAC;QACH,MAAM,mBAAmB,EAAE,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,yEAAyE,CAAC,CAAC;IACnG,CAAC;IAED,mFAAmF;IACnF,IAAI,CAAC;QACH,MAAM,aAAa,EAAE,CAAC;IACxB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,gEAAgE,CAAC,CAAC;IAC1F,CAAC;IAED,6BAA6B;IAC7B,MAAM,QAAQ,GAAG,IAAI,cAAc,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE;QAClD,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,aAAa;QACxC,OAAO,EAAE,OAAO;KACjB,EAAE,SAAS,CAAC,CAAC;IAEd,OAAO,CAAC,oBAAoB,EAAE,CAAC;IAE/B,wEAAwE;IACxE,6DAA6D;IAC7D,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,GAAG,EAAE,CAAC,4BAA4B,CAAC,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAElG,2BAA2B;IAC3B,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;IAC/B,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,aAAa,EAAE,EAAE,CAAC,CAAC;IAE9E,MAAM,CAAC,IAAI,CACT,EAAE,YAAY,EAAE,QAAQ,CAAC,aAAa,EAAE,EAAE,YAAY,EAAE,QAAQ,CAAC,aAAa,EAAE,EAAE,EAClF,qBAAqB,CACtB,CAAC;IAEF,sBAAsB;IACtB,oDAAoD;IACpD,MAAM,YAAY,GAAG,MAAM,CAAC,YAAY,CAAC;IACzC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAChD,MAAM,CAAC,IAAI,CAAC,EAAE,YAAY,EAAE,EAAE,8BAA8B,CAAC,CAAC;IAChE,CAAC;IAED,oEAAoE;IACpE,4EAA4E;IAC5E,MAAM,sBAAsB,CAAC,MAAM,CAAC,CAAC;IAErC,MAAM,UAAU,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAC9C,MAAM,gBAAgB,GAAG,OAAO,CAAC,sBAAsB,EAAE,CAAC;IAC1D,MAAM,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;IACtE,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;IAErB,4BAA4B;IAC5B,MAAM,QAAQ,GAAG,KAAK,EAAE,MAAc,EAAE,EAAE;QACxC,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,EAAE,0BAA0B,CAAC,CAAC;QACpD,0EAA0E;QAC1E,4EAA4E;QAC5E,yDAAyD;QACzD,IAAI,CAAC;YACH,MAAM,EAAE,iCAAiC,EAAE,GAAG,MAAM,MAAM,CAAC,gDAAgD,CAAC,CAAC;YAC7G,MAAM,iCAAiC,EAAE,CAAC;QAC5C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,2DAA2D,CAAC,CAAC;QACpF,CAAC;QACD,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;QACpB,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC;IAE/C,+EAA+E;IAC/E,iFAAiF;IACjF,gEAAgE;IAChE,sFAAsF;IACtF,wFAAwF;IACxF,OAAO,CAAC,EAAE,CAAC,oBAAoB,EAAE,CAAC,MAAe,EAAE,EAAE;QACnD,MAAM,GAAG,GAAG,MAAM,YAAY,KAAK,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;QACpG,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,wEAAwE,CAAC,CAAC;IAC9F,CAAC,CAAC,CAAC;IACH,OAAO,CAAC,EAAE,CAAC,mBAAmB,EAAE,CAAC,GAAU,EAAE,EAAE;QAC7C,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,EAAE,+DAA+D,CAAC,CAAC;QAC1H,+EAA+E;QAC/E,iFAAiF;IACnF,CAAC,CAAC,CAAC;IAEH,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,0BAA0B,CAAC,CAAC;AACrG,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,UAAU,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,EAAE,+BAA+B,CAAC,CAAC;IAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify ingest-path errors into a safe, structured envelope so the extension
|
|
3
|
+
* can decide retry-vs-skip and show the user a cause WITHOUT reading server logs.
|
|
4
|
+
*
|
|
5
|
+
* SEC: we expose a stable `code`, a category, a retryable flag, and the error's
|
|
6
|
+
* class name — but NEVER the raw stack, SQL text, or internal paths (those stay
|
|
7
|
+
* in the server log only). The message is a classified, non-leaky summary.
|
|
8
|
+
*/
|
|
9
|
+
/** Stable error codes returned to the extension (contract — do not rename lightly). */
|
|
10
|
+
export type IngestErrorCode = 'VALIDATION_ERROR' | 'MISSING_CHECKSUM' | 'DB_TRANSIENT' | 'DB_PERMANENT' | 'EXTRACTION_ERROR' | 'INTERNAL_ERROR';
|
|
11
|
+
/** Coarse category for UI grouping. */
|
|
12
|
+
export type IngestErrorCategory = 'validation' | 'database' | 'extraction' | 'unknown';
|
|
13
|
+
/**
|
|
14
|
+
* Structured, log-free error detail for the response envelope.
|
|
15
|
+
* `retryable` lets the extension distinguish a transient fault (retry with
|
|
16
|
+
* backoff) from a permanent one (skip the rule, don't hammer the backend).
|
|
17
|
+
*/
|
|
18
|
+
export interface IngestErrorDetails {
|
|
19
|
+
category: IngestErrorCategory;
|
|
20
|
+
retryable: boolean;
|
|
21
|
+
/** Error constructor name (e.g. "MissingChecksumError") — safe, no payload. */
|
|
22
|
+
errorName: string;
|
|
23
|
+
}
|
|
24
|
+
/** Full classified result the route turns into `{ code, message, details }`. */
|
|
25
|
+
export interface ClassifiedIngestError {
|
|
26
|
+
code: IngestErrorCode;
|
|
27
|
+
message: string;
|
|
28
|
+
details: IngestErrorDetails;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Classify an unknown thrown value into a safe envelope error.
|
|
32
|
+
* @param err - The caught error (any shape).
|
|
33
|
+
* @returns A classified, log-free error contract for the response body.
|
|
34
|
+
*/
|
|
35
|
+
export declare function classifyIngestError(err: unknown): ClassifiedIngestError;
|
|
36
|
+
/** Classify a post-ingest extraction-phase failure (schema map / RelativeExtractor). */
|
|
37
|
+
export declare function classifyExtractionError(err: unknown): ClassifiedIngestError;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify ingest-path errors into a safe, structured envelope so the extension
|
|
3
|
+
* can decide retry-vs-skip and show the user a cause WITHOUT reading server logs.
|
|
4
|
+
*
|
|
5
|
+
* SEC: we expose a stable `code`, a category, a retryable flag, and the error's
|
|
6
|
+
* class name — but NEVER the raw stack, SQL text, or internal paths (those stay
|
|
7
|
+
* in the server log only). The message is a classified, non-leaky summary.
|
|
8
|
+
*/
|
|
9
|
+
/** Postgres SQLSTATE classes/codes that are transient (worth a client retry). */
|
|
10
|
+
const TRANSIENT_PG_CODES = new Set([
|
|
11
|
+
'08000', // connection_exception
|
|
12
|
+
'08003', // connection_does_not_exist
|
|
13
|
+
'08006', // connection_failure
|
|
14
|
+
'40001', // serialization_failure
|
|
15
|
+
'40P01', // deadlock_detected
|
|
16
|
+
'53300', // too_many_connections
|
|
17
|
+
'57P01', // admin_shutdown
|
|
18
|
+
'57P03', // cannot_connect_now
|
|
19
|
+
]);
|
|
20
|
+
/** Node socket/network error codes that are transient. */
|
|
21
|
+
const TRANSIENT_NET_CODES = new Set([
|
|
22
|
+
'ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN',
|
|
23
|
+
]);
|
|
24
|
+
/**
|
|
25
|
+
* Classify an unknown thrown value into a safe envelope error.
|
|
26
|
+
* @param err - The caught error (any shape).
|
|
27
|
+
* @returns A classified, log-free error contract for the response body.
|
|
28
|
+
*/
|
|
29
|
+
export function classifyIngestError(err) {
|
|
30
|
+
const name = err instanceof Error ? err.constructor.name : typeof err;
|
|
31
|
+
const code = extractErrorCode(err);
|
|
32
|
+
// 1. Known domain error: missing checksum → caller must upgrade (permanent).
|
|
33
|
+
if (name === 'MissingChecksumError' || code === 'MISSING_CHECKSUM') {
|
|
34
|
+
return build('MISSING_CHECKSUM', 'Rule rejected: missing client-computed checksum. Upgrade the extension.', 'validation', false, name);
|
|
35
|
+
}
|
|
36
|
+
// 2. Database faults — split transient (retry) vs permanent (skip).
|
|
37
|
+
if (code && (TRANSIENT_PG_CODES.has(code) || TRANSIENT_NET_CODES.has(code))) {
|
|
38
|
+
return build('DB_TRANSIENT', `Transient database/network fault (${code}). Safe to retry.`, 'database', true, name);
|
|
39
|
+
}
|
|
40
|
+
if (isLikelyDbError(err, name)) {
|
|
41
|
+
return build('DB_PERMANENT', 'Database rejected the write (non-transient). Do not retry without a fix.', 'database', false, name);
|
|
42
|
+
}
|
|
43
|
+
// 3. Fallback: unclassified internal error (treated as retryable-once by the
|
|
44
|
+
// extension is acceptable, but marked non-retryable here to avoid loops).
|
|
45
|
+
return build('INTERNAL_ERROR', 'Ingest failed due to an internal error. See the error code and category.', 'unknown', false, name);
|
|
46
|
+
}
|
|
47
|
+
/** Classify a post-ingest extraction-phase failure (schema map / RelativeExtractor). */
|
|
48
|
+
export function classifyExtractionError(err) {
|
|
49
|
+
const name = err instanceof Error ? err.constructor.name : typeof err;
|
|
50
|
+
return build('EXTRACTION_ERROR', 'Rule was ingested but dependency extraction failed. Rule stored; relatives unknown.', 'extraction', false, name);
|
|
51
|
+
}
|
|
52
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
53
|
+
function build(code, message, category, retryable, errorName) {
|
|
54
|
+
return { code, message, details: { category, retryable, errorName } };
|
|
55
|
+
}
|
|
56
|
+
/** Read a `.code` property (pg errors + Node syscall errors both expose one). */
|
|
57
|
+
function extractErrorCode(err) {
|
|
58
|
+
if (err && typeof err === 'object' && 'code' in err) {
|
|
59
|
+
const c = err.code;
|
|
60
|
+
if (typeof c === 'string' && c.length > 0)
|
|
61
|
+
return c;
|
|
62
|
+
}
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
/** Heuristic: does this look like a DB error (has pg-style fields or name)? */
|
|
66
|
+
function isLikelyDbError(err, name) {
|
|
67
|
+
if (/postgres|database|query|sql/i.test(name))
|
|
68
|
+
return true;
|
|
69
|
+
if (err && typeof err === 'object') {
|
|
70
|
+
const e = err;
|
|
71
|
+
// node-postgres errors carry severity/routine/schema fields.
|
|
72
|
+
if ('severity' in e || 'routine' in e || 'sqlState' in e)
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=ingest-error-classifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ingest-error-classifier.js","sourceRoot":"","sources":["../../../src/modules/pega/ingest-error-classifier.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAiCH,iFAAiF;AACjF,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,OAAO,EAAE,uBAAuB;IAChC,OAAO,EAAE,4BAA4B;IACrC,OAAO,EAAE,qBAAqB;IAC9B,OAAO,EAAE,wBAAwB;IACjC,OAAO,EAAE,oBAAoB;IAC7B,OAAO,EAAE,uBAAuB;IAChC,OAAO,EAAE,iBAAiB;IAC1B,OAAO,EAAE,qBAAqB;CAC/B,CAAC,CAAC;AAEH,0DAA0D;AAC1D,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC;IAClC,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW;CAC7E,CAAC,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,GAAY;IAC9C,MAAM,IAAI,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC;IACtE,MAAM,IAAI,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAEnC,6EAA6E;IAC7E,IAAI,IAAI,KAAK,sBAAsB,IAAI,IAAI,KAAK,kBAAkB,EAAE,CAAC;QACnE,OAAO,KAAK,CAAC,kBAAkB,EAC7B,yEAAyE,EACzE,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAED,oEAAoE;IACpE,IAAI,IAAI,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,mBAAmB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC5E,OAAO,KAAK,CAAC,cAAc,EACzB,qCAAqC,IAAI,mBAAmB,EAC5D,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5B,CAAC;IACD,IAAI,eAAe,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAC,cAAc,EACzB,0EAA0E,EAC1E,UAAU,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;IAC7B,CAAC;IAED,6EAA6E;IAC7E,6EAA6E;IAC7E,OAAO,KAAK,CAAC,gBAAgB,EAC3B,0EAA0E,EAC1E,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,uBAAuB,CAAC,GAAY;IAClD,MAAM,IAAI,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,GAAG,CAAC;IACtE,OAAO,KAAK,CAAC,kBAAkB,EAC7B,qFAAqF,EACrF,YAAY,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;AAC/B,CAAC;AAED,6EAA6E;AAE7E,SAAS,KAAK,CACZ,IAAqB,EAAE,OAAe,EACtC,QAA6B,EAAE,SAAkB,EAAE,SAAiB;IAEpE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,EAAE,CAAC;AACxE,CAAC;AAED,iFAAiF;AACjF,SAAS,gBAAgB,CAAC,GAAY;IACpC,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC;QACpD,MAAM,CAAC,GAAI,GAA0B,CAAC,IAAI,CAAC;QAC3C,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,CAAC,CAAC;IACtD,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,+EAA+E;AAC/E,SAAS,eAAe,CAAC,GAAY,EAAE,IAAY;IACjD,IAAI,8BAA8B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IAC3D,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACnC,MAAM,CAAC,GAAG,GAA8B,CAAC;QACzC,6DAA6D;QAC7D,IAAI,UAAU,IAAI,CAAC,IAAI,SAAS,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACxE,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
|
|
@@ -37,6 +37,17 @@ export class HttpServer {
|
|
|
37
37
|
fetch: this.app.fetch,
|
|
38
38
|
port: this.port,
|
|
39
39
|
hostname: this.host,
|
|
40
|
+
// Fix 5/UND_ERR_SOCKET: Node's default keepAliveTimeout (~5s) closes an idle
|
|
41
|
+
// socket mid-pipeline when an ingest request is queued behind the DB under
|
|
42
|
+
// load — the client sees "other side closed". Raise the timeouts so a slow
|
|
43
|
+
// request waiting on Postgres is never dropped without a response.
|
|
44
|
+
// serverOptions is forwarded to node:http.createServer (@hono/node-server).
|
|
45
|
+
// Invariant: headersTimeout > keepAliveTimeout, requestTimeout ≥ both.
|
|
46
|
+
serverOptions: {
|
|
47
|
+
keepAliveTimeout: 60_000, // keep idle sockets alive across batched POSTs
|
|
48
|
+
headersTimeout: 65_000, // > keepAliveTimeout (Node requirement)
|
|
49
|
+
requestTimeout: 120_000, // allow a slow DB-bound ingest to finish
|
|
50
|
+
},
|
|
40
51
|
}, async (info) => {
|
|
41
52
|
this._isRunning = true;
|
|
42
53
|
this.logger.info({ port: info.port, host: this.host }, 'Backend server started');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HttpServer.js","sourceRoot":"","sources":["../../src/server/HttpServer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAGH,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAE1C,OAAO,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AACzE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EACL,+BAA+B,GAChC,MAAM,iDAAiD,CAAC;AAEzD,OAAO,EAAE,mBAAmB,EAA0B,MAAM,yBAAyB,CAAC;AACtF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAK5D,MAAM,OAAO,UAAU;IAUD;IATZ,GAAG,CAAO;IACV,MAAM,GAAoC,IAAI,CAAC;IAC/C,MAAM,CAAS;IACf,IAAI,CAAS;IACb,IAAI,CAAS;IACb,UAAU,GAAG,KAAK,CAAC;IAC3B,+EAA+E;IACvE,gBAAgB,CAAoB;IAE5C,YAAoB,OAA0B;QAA1B,YAAO,GAAP,OAAO,CAAmB;QAC5C,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,mBAAmB,CAAC,OAAO,CAAC,CAAC;IAC1C,CAAC;IAED,KAAK,CAAC,KAAK;QACT,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;YAC7B,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC;gBAClB,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK;gBACrB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,IAAI,CAAC,IAAI;
|
|
1
|
+
{"version":3,"file":"HttpServer.js","sourceRoot":"","sources":["../../src/server/HttpServer.ts"],"names":[],"mappings":"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"}
|
|
@@ -9,6 +9,7 @@ import { PegaService } from '../../modules/pega/PegaService.js';
|
|
|
9
9
|
import { IngestRuleRequestSchema } from '../../modules/pega/schemas/ingest-rule.schema.js';
|
|
10
10
|
import { RelativeExtractor } from '../../modules/pega/services/RelativeExtractor.js';
|
|
11
11
|
import { PegaSchemaLoader } from '../../modules/pega/PegaSchemaLoader.js';
|
|
12
|
+
import { classifyIngestError, classifyExtractionError, } from '../../modules/pega/ingest-error-classifier.js';
|
|
12
13
|
/** Build schemaMap from pega-core-schemas.json (cached singleton) */
|
|
13
14
|
let cachedSchemaMap = null;
|
|
14
15
|
function getSchemaMap() {
|
|
@@ -23,70 +24,104 @@ export function createIngestRuleRoute(registry, logger) {
|
|
|
23
24
|
// 10MB body limit per rule JSON
|
|
24
25
|
app.use('*', bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
25
26
|
app.post('/', async (c) => {
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
// Top-level guard: ingestSafely only wraps service.ingestRule(). Steps 4-5
|
|
28
|
+
// (getSchemaMap + RelativeExtractor.extract) run OUTSIDE it — a throw there
|
|
29
|
+
// would escape the handler and, under the Node HTTP server, can surface on the
|
|
30
|
+
// client as a socket close with no response (UND_ERR_SOCKET) instead of a 500.
|
|
31
|
+
// Wrapping the whole handler guarantees EVERY failure returns an error envelope.
|
|
32
|
+
try {
|
|
33
|
+
return await handleIngest(c, registry, logger);
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return c.json({ data: null, error:
|
|
40
|
-
message: 'X-Project-Id header or JWT pid claim is required.' } }, 401);
|
|
35
|
+
catch (err) {
|
|
36
|
+
const classified = classifyIngestError(err);
|
|
37
|
+
logger.error({ err: err instanceof Error ? err.message : String(err),
|
|
38
|
+
stack: err instanceof Error ? err.stack : undefined, code: classified.code }, '[pega-ingest-rule] Unhandled error in route handler');
|
|
39
|
+
const httpStatus = classified.details.retryable ? 503 : 500;
|
|
40
|
+
return c.json({ data: null, error: classified }, httpStatus);
|
|
41
41
|
}
|
|
42
|
-
// 2. Parse and validate request body
|
|
43
|
-
const body = await c.req.json().catch(() => null);
|
|
44
|
-
if (!body) {
|
|
45
|
-
return c.json({
|
|
46
|
-
data: null,
|
|
47
|
-
error: { code: 'VALIDATION_ERROR', message: 'Invalid JSON body' },
|
|
48
|
-
}, 400);
|
|
49
|
-
}
|
|
50
|
-
const parsed = IngestRuleRequestSchema.safeParse(body);
|
|
51
|
-
if (!parsed.success) {
|
|
52
|
-
const msg = parsed.error.errors[0]?.message || 'Invalid request';
|
|
53
|
-
return c.json({
|
|
54
|
-
data: null,
|
|
55
|
-
error: { code: 'VALIDATION_ERROR', message: msg },
|
|
56
|
-
}, 400);
|
|
57
|
-
}
|
|
58
|
-
// SA4E-241 SEC-01: body.projectId (if sent) must match identity → 403.
|
|
59
|
-
if (parsed.data.projectId && parsed.data.projectId !== identityProjectId) {
|
|
60
|
-
return c.json({ data: null, error: { code: 'PROJECT_MISMATCH',
|
|
61
|
-
message: 'body.projectId does not match the authenticated identity.' } }, 403);
|
|
62
|
-
}
|
|
63
|
-
// 3. Ingest rule via PegaService — scope strictly by authenticated identity.
|
|
64
|
-
const { ruleJson, checksum, version } = parsed.data;
|
|
65
|
-
const ingestResult = await ingestSafely(service, identityProjectId, ruleJson, checksum, version, logger);
|
|
66
|
-
if (ingestResult.error) {
|
|
67
|
-
return c.json({
|
|
68
|
-
data: null,
|
|
69
|
-
error: { code: 'INTERNAL_ERROR', message: ingestResult.error },
|
|
70
|
-
}, 500);
|
|
71
|
-
}
|
|
72
|
-
// 4. Extract relatives via RelativeExtractor
|
|
73
|
-
const schemaMap = getSchemaMap();
|
|
74
|
-
const extractor = new RelativeExtractor(schemaMap);
|
|
75
|
-
const relatives = extractor.extract(ruleJson);
|
|
76
|
-
// 5. Return combined response
|
|
77
|
-
const status = ingestResult.ruleId === -1 ? 200 : 201;
|
|
78
|
-
return c.json({
|
|
79
|
-
data: {
|
|
80
|
-
status: ingestResult.status,
|
|
81
|
-
ruleId: ingestResult.ruleId,
|
|
82
|
-
unresolvedDependencies: relatives,
|
|
83
|
-
reason: ingestResult.reason,
|
|
84
|
-
},
|
|
85
|
-
error: null,
|
|
86
|
-
}, status);
|
|
87
42
|
});
|
|
88
43
|
return app;
|
|
89
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Core ingest handler — parse, validate, ingest, extract relatives.
|
|
47
|
+
* Extracted so the route wrapper can catch ANY throw (including the post-ingest
|
|
48
|
+
* schema/extraction steps) and always return an envelope.
|
|
49
|
+
*/
|
|
50
|
+
async function handleIngest(c, registry, logger) {
|
|
51
|
+
// 1. Resolve PegaService from memory module
|
|
52
|
+
const service = resolvePegaService(registry);
|
|
53
|
+
if (!service) {
|
|
54
|
+
return c.json({
|
|
55
|
+
data: null,
|
|
56
|
+
error: { code: 'NOT_READY', message: 'Memory module not ready' },
|
|
57
|
+
}, 503);
|
|
58
|
+
}
|
|
59
|
+
// SA4E-241 SEC-01: projectId derives from the authenticated identity, never
|
|
60
|
+
// from the body. This is a WRITE path — fail-closed (401) with no identity;
|
|
61
|
+
// body.projectId may only cross-check (403 on mismatch).
|
|
62
|
+
const identityProjectId = c.get('projectContext')?.projectId ?? '';
|
|
63
|
+
if (!identityProjectId) {
|
|
64
|
+
return c.json({ data: null, error: { code: 'MISSING_PROJECT_IDENTITY',
|
|
65
|
+
message: 'X-Project-Id header or JWT pid claim is required.' } }, 401);
|
|
66
|
+
}
|
|
67
|
+
// 2. Parse and validate request body
|
|
68
|
+
const body = await c.req.json().catch(() => null);
|
|
69
|
+
if (!body) {
|
|
70
|
+
return c.json({
|
|
71
|
+
data: null,
|
|
72
|
+
error: { code: 'VALIDATION_ERROR', message: 'Invalid JSON body' },
|
|
73
|
+
}, 400);
|
|
74
|
+
}
|
|
75
|
+
const parsed = IngestRuleRequestSchema.safeParse(body);
|
|
76
|
+
if (!parsed.success) {
|
|
77
|
+
const msg = parsed.error.errors[0]?.message || 'Invalid request';
|
|
78
|
+
return c.json({
|
|
79
|
+
data: null,
|
|
80
|
+
error: { code: 'VALIDATION_ERROR', message: msg },
|
|
81
|
+
}, 400);
|
|
82
|
+
}
|
|
83
|
+
// SA4E-241 SEC-01: body.projectId (if sent) must match identity → 403.
|
|
84
|
+
if (parsed.data.projectId && parsed.data.projectId !== identityProjectId) {
|
|
85
|
+
return c.json({ data: null, error: { code: 'PROJECT_MISMATCH',
|
|
86
|
+
message: 'body.projectId does not match the authenticated identity.' } }, 403);
|
|
87
|
+
}
|
|
88
|
+
// 3. Ingest rule via PegaService — scope strictly by authenticated identity.
|
|
89
|
+
const { ruleJson, checksum, version } = parsed.data;
|
|
90
|
+
const ingestResult = await ingestSafely(service, identityProjectId, ruleJson, checksum, version, logger);
|
|
91
|
+
if (ingestResult.classified) {
|
|
92
|
+
// Classified, log-free envelope: the extension reads code + details.category
|
|
93
|
+
// + details.retryable to decide retry-vs-skip and show the user a cause.
|
|
94
|
+
// Transient faults → 503 (retry-after semantics); permanent → 500.
|
|
95
|
+
const httpStatus = ingestResult.classified.details.retryable ? 503 : 500;
|
|
96
|
+
return c.json({ data: null, error: ingestResult.classified }, httpStatus);
|
|
97
|
+
}
|
|
98
|
+
// 4. Extract relatives via RelativeExtractor. This runs AFTER a successful
|
|
99
|
+
// ingest, so a failure here must NOT masquerade as an ingest failure — the
|
|
100
|
+
// rule IS stored. Return a distinct EXTRACTION_ERROR so the extension knows
|
|
101
|
+
// the rule landed but relatives are unknown (do not re-ingest blindly).
|
|
102
|
+
let relatives;
|
|
103
|
+
try {
|
|
104
|
+
const schemaMap = getSchemaMap();
|
|
105
|
+
const extractor = new RelativeExtractor(schemaMap);
|
|
106
|
+
relatives = extractor.extract(ruleJson);
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
logger.error({ err: err instanceof Error ? err.message : String(err),
|
|
110
|
+
stack: err instanceof Error ? err.stack : undefined, ruleId: ingestResult.ruleId }, '[pega-ingest-rule] Relative extraction failed (rule already stored)');
|
|
111
|
+
return c.json({ data: null, error: classifyExtractionError(err) }, 500);
|
|
112
|
+
}
|
|
113
|
+
// 5. Return combined response
|
|
114
|
+
const status = ingestResult.ruleId === -1 ? 200 : 201;
|
|
115
|
+
return c.json({
|
|
116
|
+
data: {
|
|
117
|
+
status: ingestResult.status,
|
|
118
|
+
ruleId: ingestResult.ruleId,
|
|
119
|
+
unresolvedDependencies: relatives,
|
|
120
|
+
reason: ingestResult.reason,
|
|
121
|
+
},
|
|
122
|
+
error: null,
|
|
123
|
+
}, status);
|
|
124
|
+
}
|
|
90
125
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
91
126
|
/** Lazily resolve PegaService from memory module */
|
|
92
127
|
function resolvePegaService(registry) {
|
|
@@ -95,15 +130,23 @@ function resolvePegaService(registry) {
|
|
|
95
130
|
return null;
|
|
96
131
|
return new PegaService(memModule.getEngine());
|
|
97
132
|
}
|
|
98
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* Wrap ingestRule in try/catch. On failure, return a CLASSIFIED error (safe,
|
|
135
|
+
* log-free) so the route can hand the extension a structured cause — the raw
|
|
136
|
+
* message/stack stays in the server log only (SEC: no internal detail leak).
|
|
137
|
+
*/
|
|
99
138
|
async function ingestSafely(service, projectId, ruleJson, checksum, version, logger) {
|
|
100
139
|
try {
|
|
101
140
|
const result = await service.ingestRule({ projectId, ruleJson, checksum, version });
|
|
102
141
|
return { status: result.status, ruleId: result.ruleId, reason: result.reason };
|
|
103
142
|
}
|
|
104
143
|
catch (err) {
|
|
105
|
-
|
|
106
|
-
|
|
144
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
145
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
146
|
+
const classified = classifyIngestError(err);
|
|
147
|
+
// Full detail in the log (for ops); only the classified envelope on the wire.
|
|
148
|
+
logger.error({ err: message, stack, code: classified.code, category: classified.details.category }, '[pega-ingest-rule] Ingestion failed');
|
|
149
|
+
return { status: 'error', classified };
|
|
107
150
|
}
|
|
108
151
|
}
|
|
109
152
|
//# sourceMappingURL=pega-ingest-rule.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pega-ingest-rule.js","sourceRoot":"","sources":["../../../src/server/routes/pega-ingest-rule.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"pega-ingest-rule.js","sourceRoot":"","sources":["../../../src/server/routes/pega-ingest-rule.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sdlc-agent-4-enterprise-server",
|
|
3
|
-
"version": "1.55.
|
|
3
|
+
"version": "1.55.9",
|
|
4
4
|
"description": "Code Intelligence MCP Server — multi-agent SDLC pipeline with semantic memory, code graph, and draw.io integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"url": "git+https://github.com/dnguyenminh/SDLC-Agents-4-Enteprise.git"
|
|
48
48
|
},
|
|
49
49
|
"engines": {
|
|
50
|
-
"node": "22.
|
|
50
|
+
"node": "22.23.4"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@hono/node-server": "^1.12.0",
|
package/src/index.ts
CHANGED
|
@@ -125,6 +125,21 @@ async function main() {
|
|
|
125
125
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
126
126
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
127
127
|
|
|
128
|
+
// Global crash guards: a single rejected/uncaught error on an async path (e.g.
|
|
129
|
+
// Pega ingest) must NOT silently kill the process. Without these, Node tears the
|
|
130
|
+
// process down and EVERY in-flight HTTP socket fails at once as
|
|
131
|
+
// `UND_ERR_SOCKET "other side closed"` on the client — the exact Pega-ingest symptom.
|
|
132
|
+
// We log with full context so the real throw site is attributable instead of invisible.
|
|
133
|
+
process.on('unhandledRejection', (reason: unknown) => {
|
|
134
|
+
const err = reason instanceof Error ? { message: reason.message, stack: reason.stack } : { reason };
|
|
135
|
+
logger.error(err, '[fatal] Unhandled promise rejection — process kept alive for diagnosis');
|
|
136
|
+
});
|
|
137
|
+
process.on('uncaughtException', (err: Error) => {
|
|
138
|
+
logger.error({ message: err.message, stack: err.stack }, '[fatal] Uncaught exception — process kept alive for diagnosis');
|
|
139
|
+
// Intentionally NOT exiting: an isolated ingest-path throw should not drop all
|
|
140
|
+
// concurrent connections. Fatal startup errors are still caught by main().catch.
|
|
141
|
+
});
|
|
142
|
+
|
|
128
143
|
logger.info({ pid: process.pid, port: config.port, version: VERSION }, 'Backend MCP Server ready');
|
|
129
144
|
}
|
|
130
145
|
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Classify ingest-path errors into a safe, structured envelope so the extension
|
|
3
|
+
* can decide retry-vs-skip and show the user a cause WITHOUT reading server logs.
|
|
4
|
+
*
|
|
5
|
+
* SEC: we expose a stable `code`, a category, a retryable flag, and the error's
|
|
6
|
+
* class name — but NEVER the raw stack, SQL text, or internal paths (those stay
|
|
7
|
+
* in the server log only). The message is a classified, non-leaky summary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** Stable error codes returned to the extension (contract — do not rename lightly). */
|
|
11
|
+
export type IngestErrorCode =
|
|
12
|
+
| 'VALIDATION_ERROR'
|
|
13
|
+
| 'MISSING_CHECKSUM'
|
|
14
|
+
| 'DB_TRANSIENT'
|
|
15
|
+
| 'DB_PERMANENT'
|
|
16
|
+
| 'EXTRACTION_ERROR'
|
|
17
|
+
| 'INTERNAL_ERROR';
|
|
18
|
+
|
|
19
|
+
/** Coarse category for UI grouping. */
|
|
20
|
+
export type IngestErrorCategory = 'validation' | 'database' | 'extraction' | 'unknown';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Structured, log-free error detail for the response envelope.
|
|
24
|
+
* `retryable` lets the extension distinguish a transient fault (retry with
|
|
25
|
+
* backoff) from a permanent one (skip the rule, don't hammer the backend).
|
|
26
|
+
*/
|
|
27
|
+
export interface IngestErrorDetails {
|
|
28
|
+
category: IngestErrorCategory;
|
|
29
|
+
retryable: boolean;
|
|
30
|
+
/** Error constructor name (e.g. "MissingChecksumError") — safe, no payload. */
|
|
31
|
+
errorName: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Full classified result the route turns into `{ code, message, details }`. */
|
|
35
|
+
export interface ClassifiedIngestError {
|
|
36
|
+
code: IngestErrorCode;
|
|
37
|
+
message: string;
|
|
38
|
+
details: IngestErrorDetails;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Postgres SQLSTATE classes/codes that are transient (worth a client retry). */
|
|
42
|
+
const TRANSIENT_PG_CODES = new Set([
|
|
43
|
+
'08000', // connection_exception
|
|
44
|
+
'08003', // connection_does_not_exist
|
|
45
|
+
'08006', // connection_failure
|
|
46
|
+
'40001', // serialization_failure
|
|
47
|
+
'40P01', // deadlock_detected
|
|
48
|
+
'53300', // too_many_connections
|
|
49
|
+
'57P01', // admin_shutdown
|
|
50
|
+
'57P03', // cannot_connect_now
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
/** Node socket/network error codes that are transient. */
|
|
54
|
+
const TRANSIENT_NET_CODES = new Set([
|
|
55
|
+
'ECONNRESET', 'ETIMEDOUT', 'ECONNREFUSED', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN',
|
|
56
|
+
]);
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Classify an unknown thrown value into a safe envelope error.
|
|
60
|
+
* @param err - The caught error (any shape).
|
|
61
|
+
* @returns A classified, log-free error contract for the response body.
|
|
62
|
+
*/
|
|
63
|
+
export function classifyIngestError(err: unknown): ClassifiedIngestError {
|
|
64
|
+
const name = err instanceof Error ? err.constructor.name : typeof err;
|
|
65
|
+
const code = extractErrorCode(err);
|
|
66
|
+
|
|
67
|
+
// 1. Known domain error: missing checksum → caller must upgrade (permanent).
|
|
68
|
+
if (name === 'MissingChecksumError' || code === 'MISSING_CHECKSUM') {
|
|
69
|
+
return build('MISSING_CHECKSUM',
|
|
70
|
+
'Rule rejected: missing client-computed checksum. Upgrade the extension.',
|
|
71
|
+
'validation', false, name);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Database faults — split transient (retry) vs permanent (skip).
|
|
75
|
+
if (code && (TRANSIENT_PG_CODES.has(code) || TRANSIENT_NET_CODES.has(code))) {
|
|
76
|
+
return build('DB_TRANSIENT',
|
|
77
|
+
`Transient database/network fault (${code}). Safe to retry.`,
|
|
78
|
+
'database', true, name);
|
|
79
|
+
}
|
|
80
|
+
if (isLikelyDbError(err, name)) {
|
|
81
|
+
return build('DB_PERMANENT',
|
|
82
|
+
'Database rejected the write (non-transient). Do not retry without a fix.',
|
|
83
|
+
'database', false, name);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 3. Fallback: unclassified internal error (treated as retryable-once by the
|
|
87
|
+
// extension is acceptable, but marked non-retryable here to avoid loops).
|
|
88
|
+
return build('INTERNAL_ERROR',
|
|
89
|
+
'Ingest failed due to an internal error. See the error code and category.',
|
|
90
|
+
'unknown', false, name);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Classify a post-ingest extraction-phase failure (schema map / RelativeExtractor). */
|
|
94
|
+
export function classifyExtractionError(err: unknown): ClassifiedIngestError {
|
|
95
|
+
const name = err instanceof Error ? err.constructor.name : typeof err;
|
|
96
|
+
return build('EXTRACTION_ERROR',
|
|
97
|
+
'Rule was ingested but dependency extraction failed. Rule stored; relatives unknown.',
|
|
98
|
+
'extraction', false, name);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function build(
|
|
104
|
+
code: IngestErrorCode, message: string,
|
|
105
|
+
category: IngestErrorCategory, retryable: boolean, errorName: string,
|
|
106
|
+
): ClassifiedIngestError {
|
|
107
|
+
return { code, message, details: { category, retryable, errorName } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Read a `.code` property (pg errors + Node syscall errors both expose one). */
|
|
111
|
+
function extractErrorCode(err: unknown): string | undefined {
|
|
112
|
+
if (err && typeof err === 'object' && 'code' in err) {
|
|
113
|
+
const c = (err as { code?: unknown }).code;
|
|
114
|
+
if (typeof c === 'string' && c.length > 0) return c;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Heuristic: does this look like a DB error (has pg-style fields or name)? */
|
|
120
|
+
function isLikelyDbError(err: unknown, name: string): boolean {
|
|
121
|
+
if (/postgres|database|query|sql/i.test(name)) return true;
|
|
122
|
+
if (err && typeof err === 'object') {
|
|
123
|
+
const e = err as Record<string, unknown>;
|
|
124
|
+
// node-postgres errors carry severity/routine/schema fields.
|
|
125
|
+
if ('severity' in e || 'routine' in e || 'sqlState' in e) return true;
|
|
126
|
+
}
|
|
127
|
+
return false;
|
|
128
|
+
}
|
package/src/server/HttpServer.ts
CHANGED
|
@@ -47,6 +47,17 @@ export class HttpServer {
|
|
|
47
47
|
fetch: this.app.fetch,
|
|
48
48
|
port: this.port,
|
|
49
49
|
hostname: this.host,
|
|
50
|
+
// Fix 5/UND_ERR_SOCKET: Node's default keepAliveTimeout (~5s) closes an idle
|
|
51
|
+
// socket mid-pipeline when an ingest request is queued behind the DB under
|
|
52
|
+
// load — the client sees "other side closed". Raise the timeouts so a slow
|
|
53
|
+
// request waiting on Postgres is never dropped without a response.
|
|
54
|
+
// serverOptions is forwarded to node:http.createServer (@hono/node-server).
|
|
55
|
+
// Invariant: headersTimeout > keepAliveTimeout, requestTimeout ≥ both.
|
|
56
|
+
serverOptions: {
|
|
57
|
+
keepAliveTimeout: 60_000, // keep idle sockets alive across batched POSTs
|
|
58
|
+
headersTimeout: 65_000, // > keepAliveTimeout (Node requirement)
|
|
59
|
+
requestTimeout: 120_000, // allow a slow DB-bound ingest to finish
|
|
60
|
+
},
|
|
50
61
|
}, async (info) => {
|
|
51
62
|
this._isRunning = true;
|
|
52
63
|
this.logger.info({ port: info.port, host: this.host }, 'Backend server started');
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Hono } from 'hono';
|
|
8
|
+
import type { Context } from 'hono';
|
|
8
9
|
import { bodyLimit } from 'hono/body-limit';
|
|
9
10
|
import type { Logger } from 'pino';
|
|
10
11
|
import type { ModuleRegistry } from '../../modules/ModuleRegistry.js';
|
|
@@ -13,6 +14,9 @@ import { IngestRuleRequestSchema } from '../../modules/pega/schemas/ingest-rule.
|
|
|
13
14
|
import { RelativeExtractor } from '../../modules/pega/services/RelativeExtractor.js';
|
|
14
15
|
import { PegaSchemaLoader } from '../../modules/pega/PegaSchemaLoader.js';
|
|
15
16
|
import type { PegaRuleKbSchema } from '../../modules/pega/strategies/KbDrivenPegaParserStrategy.js';
|
|
17
|
+
import {
|
|
18
|
+
classifyIngestError, classifyExtractionError, type ClassifiedIngestError,
|
|
19
|
+
} from '../../modules/pega/ingest-error-classifier.js';
|
|
16
20
|
|
|
17
21
|
/** Build schemaMap from pega-core-schemas.json (cached singleton) */
|
|
18
22
|
let cachedSchemaMap: Map<string, PegaRuleKbSchema> | null = null;
|
|
@@ -40,6 +44,36 @@ export function createIngestRuleRoute(registry: ModuleRegistry, logger: Logger):
|
|
|
40
44
|
app.use('*', bodyLimit({ maxSize: 10 * 1024 * 1024 }));
|
|
41
45
|
|
|
42
46
|
app.post('/', async (c) => {
|
|
47
|
+
// Top-level guard: ingestSafely only wraps service.ingestRule(). Steps 4-5
|
|
48
|
+
// (getSchemaMap + RelativeExtractor.extract) run OUTSIDE it — a throw there
|
|
49
|
+
// would escape the handler and, under the Node HTTP server, can surface on the
|
|
50
|
+
// client as a socket close with no response (UND_ERR_SOCKET) instead of a 500.
|
|
51
|
+
// Wrapping the whole handler guarantees EVERY failure returns an error envelope.
|
|
52
|
+
try {
|
|
53
|
+
return await handleIngest(c, registry, logger);
|
|
54
|
+
} catch (err) {
|
|
55
|
+
const classified = classifyIngestError(err);
|
|
56
|
+
logger.error({ err: err instanceof Error ? err.message : String(err),
|
|
57
|
+
stack: err instanceof Error ? err.stack : undefined, code: classified.code },
|
|
58
|
+
'[pega-ingest-rule] Unhandled error in route handler');
|
|
59
|
+
const httpStatus = classified.details.retryable ? 503 : 500;
|
|
60
|
+
return c.json({ data: null, error: classified }, httpStatus);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return app;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Core ingest handler — parse, validate, ingest, extract relatives.
|
|
69
|
+
* Extracted so the route wrapper can catch ANY throw (including the post-ingest
|
|
70
|
+
* schema/extraction steps) and always return an envelope.
|
|
71
|
+
*/
|
|
72
|
+
async function handleIngest(
|
|
73
|
+
c: Context<PegaEnv>,
|
|
74
|
+
registry: ModuleRegistry,
|
|
75
|
+
logger: Logger,
|
|
76
|
+
) {
|
|
43
77
|
// 1. Resolve PegaService from memory module
|
|
44
78
|
const service = resolvePegaService(registry);
|
|
45
79
|
if (!service) {
|
|
@@ -85,17 +119,29 @@ export function createIngestRuleRoute(registry: ModuleRegistry, logger: Logger):
|
|
|
85
119
|
// 3. Ingest rule via PegaService — scope strictly by authenticated identity.
|
|
86
120
|
const { ruleJson, checksum, version } = parsed.data;
|
|
87
121
|
const ingestResult = await ingestSafely(service, identityProjectId, ruleJson, checksum, version, logger);
|
|
88
|
-
if (ingestResult.
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
122
|
+
if (ingestResult.classified) {
|
|
123
|
+
// Classified, log-free envelope: the extension reads code + details.category
|
|
124
|
+
// + details.retryable to decide retry-vs-skip and show the user a cause.
|
|
125
|
+
// Transient faults → 503 (retry-after semantics); permanent → 500.
|
|
126
|
+
const httpStatus = ingestResult.classified.details.retryable ? 503 : 500;
|
|
127
|
+
return c.json({ data: null, error: ingestResult.classified }, httpStatus);
|
|
93
128
|
}
|
|
94
129
|
|
|
95
|
-
// 4. Extract relatives via RelativeExtractor
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
130
|
+
// 4. Extract relatives via RelativeExtractor. This runs AFTER a successful
|
|
131
|
+
// ingest, so a failure here must NOT masquerade as an ingest failure — the
|
|
132
|
+
// rule IS stored. Return a distinct EXTRACTION_ERROR so the extension knows
|
|
133
|
+
// the rule landed but relatives are unknown (do not re-ingest blindly).
|
|
134
|
+
let relatives;
|
|
135
|
+
try {
|
|
136
|
+
const schemaMap = getSchemaMap();
|
|
137
|
+
const extractor = new RelativeExtractor(schemaMap);
|
|
138
|
+
relatives = extractor.extract(ruleJson as Record<string, unknown>);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
logger.error({ err: err instanceof Error ? err.message : String(err),
|
|
141
|
+
stack: err instanceof Error ? err.stack : undefined, ruleId: ingestResult.ruleId },
|
|
142
|
+
'[pega-ingest-rule] Relative extraction failed (rule already stored)');
|
|
143
|
+
return c.json({ data: null, error: classifyExtractionError(err) }, 500);
|
|
144
|
+
}
|
|
99
145
|
|
|
100
146
|
// 5. Return combined response
|
|
101
147
|
const status = ingestResult.ruleId === -1 ? 200 : 201;
|
|
@@ -108,9 +154,6 @@ export function createIngestRuleRoute(registry: ModuleRegistry, logger: Logger):
|
|
|
108
154
|
},
|
|
109
155
|
error: null,
|
|
110
156
|
}, status);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return app;
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
@@ -122,7 +165,11 @@ function resolvePegaService(registry: ModuleRegistry): PegaService | null {
|
|
|
122
165
|
return new PegaService(memModule.getEngine());
|
|
123
166
|
}
|
|
124
167
|
|
|
125
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* Wrap ingestRule in try/catch. On failure, return a CLASSIFIED error (safe,
|
|
170
|
+
* log-free) so the route can hand the extension a structured cause — the raw
|
|
171
|
+
* message/stack stays in the server log only (SEC: no internal detail leak).
|
|
172
|
+
*/
|
|
126
173
|
async function ingestSafely(
|
|
127
174
|
service: PegaService,
|
|
128
175
|
projectId: string,
|
|
@@ -130,12 +177,17 @@ async function ingestSafely(
|
|
|
130
177
|
checksum: string | undefined,
|
|
131
178
|
version: string | undefined,
|
|
132
179
|
logger: Logger,
|
|
133
|
-
): Promise<{ status: string; ruleId?: number; reason?: string;
|
|
180
|
+
): Promise<{ status: string; ruleId?: number; reason?: string; classified?: ClassifiedIngestError }> {
|
|
134
181
|
try {
|
|
135
182
|
const result = await service.ingestRule({ projectId, ruleJson, checksum, version });
|
|
136
183
|
return { status: result.status, ruleId: result.ruleId, reason: result.reason };
|
|
137
|
-
} catch (err:
|
|
138
|
-
|
|
139
|
-
|
|
184
|
+
} catch (err: unknown) {
|
|
185
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
186
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
187
|
+
const classified = classifyIngestError(err);
|
|
188
|
+
// Full detail in the log (for ops); only the classified envelope on the wire.
|
|
189
|
+
logger.error({ err: message, stack, code: classified.code, category: classified.details.category },
|
|
190
|
+
'[pega-ingest-rule] Ingestion failed');
|
|
191
|
+
return { status: 'error', classified };
|
|
140
192
|
}
|
|
141
193
|
}
|