sdlc-agent-4-enterprise-server 1.55.8 → 1.55.10

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 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:43ac6c60b8f89723f746e8a92ce91abd5017e627ce1ddfe4238355d3a30b772c
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,10 +182,11 @@ 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
- # same-major PATCH releases only (5.0.9→5.0.12, 6.28.0→6.28.1).
188
+ # same-major releases only (5.0.9→5.0.12; undici 6.28.0→6.29.1 clears
189
+ # CVE-2026-109383 + CVE-2026-87018, both fixed in undici 6.29.1).
189
190
  # npm/npx stay installed: the sandbox module's local-mode fallback (UC-06)
190
191
  # shells out to them at runtime, so removing the CLI would be a regression.
191
192
  # Build stages keep the base npm for `npm ci` — only the runtime CLI moves.
@@ -194,7 +195,7 @@ WORKDIR /app
194
195
  ARG NPM_VERSION=11.21.0
195
196
  RUN npm install -g npm@${NPM_VERSION} --no-audit --no-fund --loglevel=error \
196
197
  && NM=/usr/local/lib/node_modules/npm/node_modules \
197
- && for entry in "brace-expansion 5.0.12" "undici 6.28.1"; do \
198
+ && for entry in "brace-expansion 5.0.12" "undici 6.29.1"; do \
198
199
  name=$(echo "$entry" | cut -d ' ' -f1); ver=$(echo "$entry" | cut -d ' ' -f2); \
199
200
  tgz=$(npm pack "${name}@${ver}" --pack-destination /tmp --silent); \
200
201
  rm -rf "${NM}/${name}"; mkdir -p "${NM}/${name}"; \
@@ -206,7 +207,7 @@ RUN npm install -g npm@${NPM_VERSION} --no-audit --no-fund --loglevel=error \
206
207
  # SEC-04: upgrade Debian packages to fix CVE-2026-103111 (libpcre2-8-0 HIGH)
207
208
  # and the perl CVE set found by Trivy on 2026-10-10 (CVE-2026-13221 CRITICAL,
208
209
  # 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:XX-slim)
210
+ # CVE-2026-57432, CVE-2026-57433 — perl-base/perl-modules in node:22.23.4-slim)
210
211
  && apt-get update \
211
212
  && apt-get upgrade -y --no-install-recommends libpcre2-8-0 perl-base perl-modules-5.36 \
212
213
  && apt-get clean \
@@ -265,7 +266,7 @@ ENV INDEX_CONCURRENCY_LIMIT=8
265
266
 
266
267
  EXPOSE 48721
267
268
 
268
- # Dependency-free health probe (SEC-12): node:22-slim does not ship `wget`, so a
269
+ # Dependency-free health probe (SEC-12): node:22.23.4-slim does not ship `wget`, so a
269
270
  # wget-based probe would silently fail. Use node's built-in http client instead.
270
271
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
271
272
  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/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <img src="https://img.shields.io/badge/version-1.55.8-blue?style=for-the-badge" alt="Version">
9
+ <img src="https://img.shields.io/badge/version-1.55.10-blue?style=for-the-badge" alt="Version">
10
10
  <img src="https://img.shields.io/badge/tools-66+-teal?style=for-the-badge" alt="Tools">
11
11
  <img src="https://img.shields.io/badge/node-%3E%3D18-green?style=for-the-badge" alt="Node">
12
12
  </p>
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":"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,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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -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":"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"}
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
- // 1. Resolve PegaService from memory module
27
- const service = resolvePegaService(registry);
28
- if (!service) {
29
- return c.json({
30
- data: null,
31
- error: { code: 'NOT_READY', message: 'Memory module not ready' },
32
- }, 503);
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
- // SA4E-241 SEC-01: projectId derives from the authenticated identity, never
35
- // from the body. This is a WRITE path — fail-closed (401) with no identity;
36
- // body.projectId may only cross-check (403 on mismatch).
37
- const identityProjectId = c.get('projectContext')?.projectId ?? '';
38
- if (!identityProjectId) {
39
- return c.json({ data: null, error: { code: 'MISSING_PROJECT_IDENTITY',
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
- /** Wrap ingestRule in try/catch to prevent unhandled exceptions */
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
- logger.error({ err: err.message, stack: err.stack }, '[pega-ingest-rule] Ingestion failed');
106
- return { status: 'error', error: 'Ingestion failed. Check server logs for details.' };
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":"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"}
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.8",
3
+ "version": "1.55.10",
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.x"
50
+ "node": "22.23.4"
51
51
  },
52
52
  "dependencies": {
53
53
  "@hono/node-server": "^1.12.0",
@@ -96,5 +96,8 @@
96
96
  "bugs": {
97
97
  "url": "https://github.com/dnguyenminh/SDLC-Agents-4-Enteprise/issues"
98
98
  },
99
- "homepage": "https://github.com/dnguyenminh/SDLC-Agents-4-Enteprise#readme"
99
+ "homepage": "https://github.com/dnguyenminh/SDLC-Agents-4-Enteprise#readme",
100
+ "overrides": {
101
+ "undici": "6.29.1"
102
+ }
100
103
  }
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
+ }
@@ -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.error) {
89
- return c.json({
90
- data: null,
91
- error: { code: 'INTERNAL_ERROR', message: ingestResult.error },
92
- }, 500);
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
- const schemaMap = getSchemaMap();
97
- const extractor = new RelativeExtractor(schemaMap);
98
- const relatives = extractor.extract(ruleJson as Record<string, unknown>);
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
- /** Wrap ingestRule in try/catch to prevent unhandled exceptions */
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; error?: 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: any) {
138
- logger.error({ err: err.message, stack: err.stack }, '[pega-ingest-rule] Ingestion failed');
139
- return { status: 'error', error: 'Ingestion failed. Check server logs for details.' };
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
  }