@ciq-dev/neoiq-foundation-node 1.0.1-beta.0 → 1.0.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
+ import { createRequire } from "module";
1
2
  import { z } from "zod";
2
3
  import { AsyncLocalStorage } from "async_hooks";
3
- import pino from "pino";
4
4
  import { SpanStatusCode, SpanStatusCode as SpanStatusCode$1, context, context as context$1, metrics, propagation, propagation as propagation$1, trace, trace as trace$1 } from "@opentelemetry/api";
5
+ import pino from "pino";
5
6
  import { NodeSDK } from "@opentelemetry/sdk-node";
6
7
  import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
7
8
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
@@ -14,7 +15,12 @@ import { randomUUID } from "crypto";
14
15
  import axios from "axios";
15
16
  import axiosRetry from "axios-retry";
16
17
  import CircuitBreaker from "opossum";
18
+ import { Readable } from "node:stream";
19
+
20
+ //#region rolldown:runtime
21
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
17
22
 
23
+ //#endregion
18
24
  //#region src/config.ts
19
25
  const AutoInstrumentationConfigSchema = z.object({
20
26
  http: z.boolean().default(true),
@@ -57,6 +63,11 @@ const RequestLoggingConfigSchema = z.object({
57
63
  maxBodySize: z.number().default(10 * 1024),
58
64
  redactHeaders: z.array(z.string()).optional()
59
65
  }).partial();
66
+ const RedactionConfigSchema = z.object({ additionalPaths: z.array(z.string()).default([]) }).partial();
67
+ const ShutdownConfigSchema = z.object({
68
+ flushOnCrash: z.boolean().default(false),
69
+ flushTimeoutMs: z.number().min(100).default(5e3)
70
+ }).partial();
60
71
  const FoundationConfigSchema = z.object({
61
72
  serviceName: z.string().min(1, "serviceName is required"),
62
73
  serviceVersion: z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
@@ -69,7 +80,9 @@ const FoundationConfigSchema = z.object({
69
80
  features: FeaturesConfigSchema.default({}),
70
81
  otel: OtelConfigSchema.default({}),
71
82
  logging: LoggingConfigSchema.default({}),
72
- requestLogging: RequestLoggingConfigSchema.default({})
83
+ requestLogging: RequestLoggingConfigSchema.default({}),
84
+ redaction: RedactionConfigSchema.default({}),
85
+ shutdown: ShutdownConfigSchema.default({})
73
86
  });
74
87
  /** Parse and validate configuration */
75
88
  function parseConfig(input) {
@@ -87,33 +100,247 @@ function getDefaultOtelEndpoint() {
87
100
 
88
101
  //#endregion
89
102
  //#region src/features/context.ts
103
+ const BAGGAGE_CORRELATION_KEY = "correlation.id";
104
+ function setBaggageCorrelationId(correlationId) {
105
+ const currentBaggage = propagation$1.getBaggage(context$1.active());
106
+ const baggage = (currentBaggage ?? propagation$1.createBaggage()).setEntry(BAGGAGE_CORRELATION_KEY, { value: correlationId });
107
+ return propagation$1.setBaggage(context$1.active(), baggage);
108
+ }
109
+ function getBaggageCorrelationId() {
110
+ const baggage = propagation$1.getBaggage(context$1.active());
111
+ return baggage?.getEntry(BAGGAGE_CORRELATION_KEY)?.value;
112
+ }
90
113
  /** Create a new context manager instance */
91
114
  function createContextManager() {
92
115
  const als = new AsyncLocalStorage();
93
116
  return {
94
- getContext: () => als.getStore(),
117
+ getContext() {
118
+ const alsCtx = als.getStore();
119
+ if (!alsCtx) return void 0;
120
+ const baggageCorrelationId = getBaggageCorrelationId();
121
+ if (baggageCorrelationId && baggageCorrelationId !== alsCtx.correlationId) return {
122
+ ...alsCtx,
123
+ correlationId: baggageCorrelationId
124
+ };
125
+ return alsCtx;
126
+ },
95
127
  run(context$2, fn) {
128
+ if (context$2.correlationId) {
129
+ const otelCtx = setBaggageCorrelationId(context$2.correlationId);
130
+ return context$1.with(otelCtx, () => als.run(context$2, fn));
131
+ }
96
132
  return als.run(context$2, fn);
97
133
  },
98
134
  get(key) {
135
+ if (key === "correlationId") return getBaggageCorrelationId() ?? als.getStore()?.correlationId;
99
136
  return als.getStore()?.[key];
100
137
  },
101
138
  update(updates) {
102
139
  const current = als.getStore();
103
140
  if (!current) return void 0;
141
+ if (updates.correlationId) setBaggageCorrelationId(updates.correlationId);
104
142
  Object.assign(current, updates);
105
143
  return current;
144
+ },
145
+ setContextValue(key, value) {
146
+ const current = als.getStore();
147
+ if (!current) return;
148
+ if (!current.contextData) current.contextData = {};
149
+ current.contextData[key] = value;
150
+ },
151
+ getContextValue(key) {
152
+ return als.getStore()?.contextData?.[key];
153
+ },
154
+ getContextData() {
155
+ return als.getStore()?.contextData ?? {};
156
+ },
157
+ setContextData(data) {
158
+ const current = als.getStore();
159
+ if (!current) return;
160
+ current.contextData = {
161
+ ...current.contextData ?? {},
162
+ ...data
163
+ };
164
+ },
165
+ clearContextData() {
166
+ const current = als.getStore();
167
+ if (!current) return;
168
+ current.contextData = {};
106
169
  }
107
170
  };
108
171
  }
109
172
 
173
+ //#endregion
174
+ //#region src/features/redaction.ts
175
+ /**
176
+ * Log Redaction & PII Sanitization
177
+ *
178
+ * Two-layer approach:
179
+ * - Layer A: Pino native `redact` (fast-redact) for key-based redaction on every log call.
180
+ * Compiled at init, near-zero runtime cost.
181
+ * - Layer B: Deep-traverse sanitizer for value-pattern detection (JWTs, AWS keys, etc.).
182
+ * Only used for request/response body logging — NOT on every log call.
183
+ */
184
+ const PLACEHOLDER = "[REDACTED]";
185
+ /**
186
+ * Key names that Pino's fast-redact will censor automatically.
187
+ * Supports wildcards: '*.password' matches nested keys one level deep.
188
+ */
189
+ const REDACT_PATHS = [
190
+ "password",
191
+ "passwd",
192
+ "pass",
193
+ "pwd",
194
+ "secret",
195
+ "secretKey",
196
+ "secret_key",
197
+ "token",
198
+ "accessToken",
199
+ "access_token",
200
+ "refreshToken",
201
+ "refresh_token",
202
+ "idToken",
203
+ "id_token",
204
+ "apiKey",
205
+ "api_key",
206
+ "apiSecret",
207
+ "api_secret",
208
+ "authorization",
209
+ "auth",
210
+ "credentials",
211
+ "privateKey",
212
+ "private_key",
213
+ "cookie",
214
+ "setCookie",
215
+ "set_cookie",
216
+ "creditCard",
217
+ "credit_card",
218
+ "cardNumber",
219
+ "card_number",
220
+ "ccNumber",
221
+ "cc_number",
222
+ "cvv",
223
+ "cvc",
224
+ "securityCode",
225
+ "security_code",
226
+ "accountNumber",
227
+ "account_number",
228
+ "ssn",
229
+ "socialSecurity",
230
+ "social_security",
231
+ "dateOfBirth",
232
+ "date_of_birth",
233
+ "dob",
234
+ "*.password",
235
+ "*.secret",
236
+ "*.token",
237
+ "*.apiKey",
238
+ "*.api_key",
239
+ "*.authorization",
240
+ "*.cookie",
241
+ "*.credentials",
242
+ "*.creditCard",
243
+ "*.cardNumber",
244
+ "*.cvv",
245
+ "*.ssn",
246
+ "*.privateKey",
247
+ "*.private_key"
248
+ ];
249
+ /**
250
+ * Value patterns that indicate a secret regardless of the key name.
251
+ * Used only for body sanitization (Layer B).
252
+ */
253
+ const VALUE_PATTERNS = [
254
+ {
255
+ label: "jwt",
256
+ pattern: /^eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+$/
257
+ },
258
+ {
259
+ label: "aws_access_key",
260
+ pattern: /(?:^|[^A-Za-z0-9])AKIA[0-9A-Z]{16}(?:$|[^A-Za-z0-9])/
261
+ },
262
+ {
263
+ label: "stripe_key",
264
+ pattern: /^[sr]k_(live|test)_[A-Za-z0-9]{10,}$/
265
+ },
266
+ {
267
+ label: "openai_key",
268
+ pattern: /^sk-[A-Za-z0-9_-]{20,}$/
269
+ },
270
+ {
271
+ label: "github_token",
272
+ pattern: /^gh[ps]_[A-Za-z0-9]{36,}$/
273
+ },
274
+ {
275
+ label: "pem_private_key",
276
+ pattern: /-----BEGIN\s+(RSA\s+|EC\s+)?PRIVATE\s+KEY-----/
277
+ },
278
+ {
279
+ label: "connection_string",
280
+ pattern: /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:]+:[^@]+@/
281
+ },
282
+ {
283
+ label: "bearer_token",
284
+ pattern: /^Bearer\s+\S{10,}$/i
285
+ }
286
+ ];
287
+ const SENSITIVE_KEYS = new Set(REDACT_PATHS.filter((p) => !p.includes("*")).map((k) => k.toLowerCase()));
288
+ const MAX_DEPTH = 10;
289
+ const MAX_KEYS = 200;
290
+ function mask(value) {
291
+ if (typeof value !== "string" || value.length <= 8) return PLACEHOLDER;
292
+ return `${value.slice(0, 4)}${"*".repeat(Math.min(value.length - 8, 20))}${value.slice(-4)}`;
293
+ }
294
+ function isSensitiveValue(value) {
295
+ return VALUE_PATTERNS.some((p) => p.pattern.test(value));
296
+ }
297
+ function sanitizeValue(value, depth) {
298
+ if (depth > MAX_DEPTH) return value;
299
+ if (typeof value === "string" && isSensitiveValue(value)) return mask(value);
300
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, depth + 1));
301
+ if (value !== null && typeof value === "object") return sanitizeObject(value, depth + 1);
302
+ return value;
303
+ }
304
+ function sanitizeObject(obj, depth) {
305
+ if (depth > MAX_DEPTH) return obj;
306
+ const keys = Object.keys(obj);
307
+ if (keys.length > MAX_KEYS) return obj;
308
+ const result = {};
309
+ for (const key of keys) {
310
+ const value = obj[key];
311
+ if (SENSITIVE_KEYS.has(key.toLowerCase())) result[key] = typeof value === "string" ? mask(value) : PLACEHOLDER;
312
+ else result[key] = sanitizeValue(value, depth);
313
+ }
314
+ return result;
315
+ }
316
+ /**
317
+ * Deep-traverse sanitizer for request/response bodies.
318
+ * Checks both key names (deny-list) and value patterns (JWT, AWS keys, etc.).
319
+ * NOT intended for every log call — use Pino native `redact` for that.
320
+ */
321
+ function sanitizeBody(body) {
322
+ if (body === null || body === void 0) return body;
323
+ if (typeof body === "string") return isSensitiveValue(body) ? mask(body) : body;
324
+ if (typeof body !== "object") return body;
325
+ if (Array.isArray(body)) return body.map((item) => sanitizeValue(item, 0));
326
+ return sanitizeObject(body, 0);
327
+ }
328
+ /** Build the Pino `redact` config object for fast-redact integration */
329
+ function buildPinoRedactConfig(additionalPaths = []) {
330
+ return {
331
+ paths: [...REDACT_PATHS, ...additionalPaths],
332
+ censor: PLACEHOLDER
333
+ };
334
+ }
335
+
110
336
  //#endregion
111
337
  //#region src/features/logging.ts
112
338
  /** Create a structured logger with automatic trace context injection */
113
339
  function createLogger(options) {
114
- const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager } = options;
340
+ const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager, additionalRedactPaths = [] } = options;
115
341
  const pinoLogger = pino({
116
342
  level,
343
+ redact: buildPinoRedactConfig(additionalRedactPaths),
117
344
  base: {
118
345
  service: serviceName,
119
346
  version: serviceVersion,
@@ -123,11 +350,17 @@ function createLogger(options) {
123
350
  const span = trace$1.getActiveSpan();
124
351
  const spanContext = span?.spanContext();
125
352
  const ctx = contextManager?.getContext();
126
- return {
127
- traceId: spanContext?.traceId || ctx?.traceId,
128
- spanId: spanContext?.spanId || ctx?.spanId,
129
- correlationId: ctx?.correlationId
353
+ const traceId = spanContext?.traceId || ctx?.traceId;
354
+ const spanId = spanContext?.spanId || ctx?.spanId;
355
+ const correlationId = ctx?.correlationId;
356
+ const contextData = ctx?.contextData;
357
+ const result = {
358
+ trace_id: traceId,
359
+ span_id: spanId,
360
+ correlation_id: correlationId
130
361
  };
362
+ if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
363
+ return result;
131
364
  },
132
365
  formatters: { level: (label) => ({ level: label }) },
133
366
  transport: prettyPrint ? {
@@ -161,7 +394,10 @@ function createFallbackLogger(serviceName = "unknown") {
161
394
  ...obj,
162
395
  msg
163
396
  };
164
- console[level === "debug" ? "log" : level](JSON.stringify(logObj));
397
+ const payload = JSON.stringify(logObj);
398
+ if (level === "warn") console.warn(payload);
399
+ else if (level === "error") console.error(payload);
400
+ else console.log(payload);
165
401
  };
166
402
  const fallback = {
167
403
  debug: (obj, msg) => log("debug", obj, msg),
@@ -185,12 +421,26 @@ function getGlobalLogger() {
185
421
  //#region src/features/tracing.ts
186
422
  let sdk = null;
187
423
  let isInitialized$1 = false;
424
+ const MONITORED_MODULES = [
425
+ "pg",
426
+ "mongodb",
427
+ "ioredis",
428
+ "mysql2",
429
+ "express",
430
+ "@grpc/grpc-js"
431
+ ];
432
+ function warnPreloadedModules() {
433
+ for (const mod of MONITORED_MODULES) try {
434
+ if (__require.resolve(mod) in (__require.cache || {})) console.warn(`[neoiq-foundation] "${mod}" was imported before tracing init. Auto-instrumentation may not work for this module. Use node -r @ciq-dev/neoiq-foundation-node/bootstrap or call createFoundation() first.`);
435
+ } catch {}
436
+ }
188
437
  /** Initialize OpenTelemetry tracing */
189
438
  function setupTracing(options) {
190
439
  if (isInitialized$1) {
191
440
  console.warn("[neoiq-foundation] Tracing already initialized");
192
441
  return;
193
442
  }
443
+ warnPreloadedModules();
194
444
  const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
195
445
  const resource = resourceFromAttributes({
196
446
  [ATTR_SERVICE_NAME]: serviceName,
@@ -347,7 +597,6 @@ function createObservabilityPlugin(options) {
347
597
  const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
348
598
  const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
349
599
  const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
350
- const tracer = tracingEnabled ? trace$1.getTracer("neoiq-foundation-node") : null;
351
600
  let requestCounter;
352
601
  let requestDuration;
353
602
  let requestErrors;
@@ -364,22 +613,12 @@ function createObservabilityPlugin(options) {
364
613
  }
365
614
  const correlationId = request.headers["x-request-id"] || randomUUID();
366
615
  reply.header("x-request-id", correlationId);
367
- let span;
368
616
  let traceId = "";
369
617
  let spanId = "";
370
- if (tracer) {
371
- const parentContext = propagation$1.extract(context$1.active(), request.headers);
372
- span = tracer.startSpan(`${request.method} ${request.routeOptions?.url || request.url}`, {
373
- kind: 1,
374
- attributes: {
375
- "http.method": request.method,
376
- "http.url": request.url,
377
- "http.route": request.routeOptions?.url || request.url,
378
- "http.user_agent": request.headers["user-agent"] || "",
379
- "http.correlation_id": correlationId
380
- }
381
- }, parentContext);
382
- const spanContext = span.spanContext();
618
+ const activeSpan = tracingEnabled ? trace$1.getActiveSpan() : void 0;
619
+ if (activeSpan) {
620
+ activeSpan.setAttribute("http.correlation_id", correlationId);
621
+ const spanContext = activeSpan.spanContext();
383
622
  traceId = spanContext.traceId;
384
623
  spanId = spanContext.spanId;
385
624
  }
@@ -389,12 +628,12 @@ function createObservabilityPlugin(options) {
389
628
  spanId,
390
629
  startTime: Date.now()
391
630
  };
392
- request.__span = span;
393
631
  request.__requestContext = requestContext;
394
632
  runInContext(requestContext, () => {
395
633
  const logData = {
396
- correlationId,
397
- traceId: traceId || void 0,
634
+ correlation_id: correlationId,
635
+ trace_id: traceId || void 0,
636
+ span_id: spanId || void 0,
398
637
  method: request.method,
399
638
  url: request.url,
400
639
  ip: request.ip,
@@ -413,8 +652,8 @@ function createObservabilityPlugin(options) {
413
652
  }
414
653
  runInContext(ctx, () => {
415
654
  logger.debug({
416
- correlationId: ctx.correlationId,
417
- body: truncateBody(request.body, maxBodySize)
655
+ correlation_id: ctx.correlationId,
656
+ body: sanitizeBody(truncateBody(request.body, maxBodySize))
418
657
  }, "Request body");
419
658
  done();
420
659
  });
@@ -427,16 +666,15 @@ function createObservabilityPlugin(options) {
427
666
  }
428
667
  runInContext(ctx, () => {
429
668
  logger.debug({
430
- correlationId: ctx.correlationId,
669
+ correlation_id: ctx.correlationId,
431
670
  statusCode: reply.statusCode,
432
- body: truncateBody(payload, maxBodySize)
671
+ body: sanitizeBody(truncateBody(payload, maxBodySize))
433
672
  }, "Response body");
434
673
  done(null, payload);
435
674
  });
436
675
  });
437
676
  fastify.addHook("onResponse", (request, reply, done) => {
438
677
  const ctx = request.__requestContext;
439
- const span = request.__span;
440
678
  if (!ctx) {
441
679
  done();
442
680
  return;
@@ -450,7 +688,7 @@ function createObservabilityPlugin(options) {
450
688
  };
451
689
  runInContext(ctx, () => {
452
690
  logger.info({
453
- correlationId: ctx.correlationId,
691
+ correlation_id: ctx.correlationId,
454
692
  method: request.method,
455
693
  statusCode: reply.statusCode,
456
694
  durationMs
@@ -460,35 +698,35 @@ function createObservabilityPlugin(options) {
460
698
  requestDuration.record(durationMs, labels);
461
699
  if (reply.statusCode >= 400) requestErrors.add(1, labels);
462
700
  }
463
- if (span) {
464
- span.setStatus({ code: reply.statusCode < 400 ? SpanStatusCode$1.OK : SpanStatusCode$1.ERROR });
465
- span.setAttribute("http.status_code", reply.statusCode);
466
- span.setAttribute("http.response_time_ms", durationMs);
467
- span.end();
701
+ if (tracingEnabled) {
702
+ const activeSpan = trace$1.getActiveSpan();
703
+ if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
468
704
  }
469
705
  done();
470
706
  });
471
707
  });
472
708
  fastify.addHook("onError", (request, _reply, error, done) => {
473
709
  const ctx = request.__requestContext;
474
- const span = request.__span;
475
710
  if (!ctx) {
476
711
  done();
477
712
  return;
478
713
  }
479
714
  runInContext(ctx, () => {
480
715
  logger.error({
481
- correlationId: ctx.correlationId,
716
+ correlation_id: ctx.correlationId,
482
717
  method: request.method,
483
718
  url: request.url,
484
719
  error: error.message
485
720
  }, "Request failed");
486
- if (span) {
487
- span.setStatus({
488
- code: SpanStatusCode$1.ERROR,
489
- message: error.message
490
- });
491
- span.recordException(error);
721
+ if (tracingEnabled) {
722
+ const activeSpan = trace$1.getActiveSpan();
723
+ if (activeSpan) {
724
+ activeSpan.setStatus({
725
+ code: SpanStatusCode$1.ERROR,
726
+ message: error.message
727
+ });
728
+ activeSpan.recordException(error);
729
+ }
492
730
  }
493
731
  done();
494
732
  });
@@ -618,17 +856,54 @@ function createHttpClient(options) {
618
856
  breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
619
857
  breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
620
858
  breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
859
+ const originalRequest = client.request.bind(client);
860
+ client.request = (config) => breaker.fire(config);
861
+ client.get = (url, config) => breaker.fire({
862
+ ...config,
863
+ method: "GET",
864
+ url
865
+ });
866
+ client.post = (url, data, config) => breaker.fire({
867
+ ...config,
868
+ method: "POST",
869
+ url,
870
+ data
871
+ });
872
+ client.put = (url, data, config) => breaker.fire({
873
+ ...config,
874
+ method: "PUT",
875
+ url,
876
+ data
877
+ });
878
+ client.delete = (url, config) => breaker.fire({
879
+ ...config,
880
+ method: "DELETE",
881
+ url
882
+ });
883
+ client.patch = (url, data, config) => breaker.fire({
884
+ ...config,
885
+ method: "PATCH",
886
+ url,
887
+ data
888
+ });
889
+ client.__originalRequest = originalRequest;
621
890
  }
622
891
  return client;
623
892
  }
624
893
 
625
894
  //#endregion
626
895
  //#region src/foundation.ts
896
+ const deprecationWarnings = new Set();
897
+ function warnDeprecation(oldPath, newPath) {
898
+ if (deprecationWarnings.has(oldPath)) return;
899
+ deprecationWarnings.add(oldPath);
900
+ console.warn(`[neoiq-foundation] DEPRECATED: foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`);
901
+ }
627
902
  /** Create a fully configured observability foundation */
628
903
  function createFoundation(input) {
629
904
  const startTime = Date.now();
630
905
  const config = parseConfig(input);
631
- const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig } = config;
906
+ const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig, redaction: redactionConfig, shutdown: shutdownConfig } = config;
632
907
  const features = {
633
908
  tracing: featuresConfig.tracing ?? true,
634
909
  metrics: featuresConfig.metrics ?? true,
@@ -644,7 +919,8 @@ function createFoundation(input) {
644
919
  environment,
645
920
  level: loggingConfig.level ?? "info",
646
921
  prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
647
- contextManager
922
+ contextManager,
923
+ additionalRedactPaths: redactionConfig.additionalPaths
648
924
  });
649
925
  setGlobalLogger(logger);
650
926
  } catch (err) {
@@ -716,17 +992,137 @@ function createFoundation(input) {
716
992
  environment,
717
993
  features
718
994
  }, "Foundation initialized");
719
- const foundation = {
720
- config,
995
+ if (shutdownConfig.flushOnCrash) {
996
+ const flushTimeoutMs = shutdownConfig.flushTimeoutMs ?? 5e3;
997
+ const crashFlush = (origin, err) => {
998
+ const error = err instanceof Error ? err : new Error(String(err));
999
+ logger.error({
1000
+ error: error.message,
1001
+ stack: error.stack,
1002
+ origin
1003
+ }, "Process crash detected, flushing telemetry");
1004
+ const flushPromises = [];
1005
+ if (features.tracing && isTracingEnabled()) flushPromises.push(shutdownTracing());
1006
+ if (features.metrics && isMetricsEnabled()) flushPromises.push(shutdownMetrics());
1007
+ const timeout = new Promise((resolve) => setTimeout(resolve, flushTimeoutMs));
1008
+ Promise.race([Promise.allSettled(flushPromises), timeout]).finally(() => {
1009
+ process.exit(1);
1010
+ });
1011
+ };
1012
+ process.on("uncaughtException", (err) => crashFlush("uncaughtException", err));
1013
+ process.on("unhandledRejection", (reason) => crashFlush("unhandledRejection", reason));
1014
+ logger.info({ flushTimeoutMs }, "Crash-flush handlers registered");
1015
+ }
1016
+ const traceInSpan = async (name, fn) => {
1017
+ const tracer = tracerInstance || getTracer(serviceName);
1018
+ return new Promise((resolve, reject) => {
1019
+ tracer.startActiveSpan(name, async (span) => {
1020
+ try {
1021
+ const result = await fn();
1022
+ span.setStatus({ code: SpanStatusCode.OK });
1023
+ span.end();
1024
+ resolve(result);
1025
+ } catch (err) {
1026
+ const error = err;
1027
+ span.setStatus({
1028
+ code: SpanStatusCode.ERROR,
1029
+ message: error.message
1030
+ });
1031
+ span.recordException(error);
1032
+ span.end();
1033
+ logger.error({
1034
+ span: name,
1035
+ error: error.message
1036
+ }, "Span failed");
1037
+ reject(error);
1038
+ }
1039
+ });
1040
+ });
1041
+ };
1042
+ const observability = {
721
1043
  logger,
722
- context: contextManager,
723
1044
  tracer: tracerInstance,
724
1045
  meter: meterInstance,
725
- features,
726
1046
  getTracer: (name) => getTracer(name || serviceName),
727
1047
  getMeter: (name, version) => getMeter(name, version),
728
1048
  getTraceContext,
729
1049
  getActiveSpan,
1050
+ trace: traceInSpan
1051
+ };
1052
+ const httpModule = { createClient: (options) => createHttpClient({
1053
+ ...options,
1054
+ foundation
1055
+ }) };
1056
+ const buildHealthStatus = () => {
1057
+ const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
1058
+ const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
1059
+ const loggingUp = !loggingError;
1060
+ const allUp = tracingUp && metricsUp && loggingUp;
1061
+ const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
1062
+ return {
1063
+ status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
1064
+ timestamp: new Date().toISOString(),
1065
+ service: serviceName,
1066
+ version: serviceVersion,
1067
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
1068
+ components: {
1069
+ tracing: {
1070
+ enabled: features.tracing,
1071
+ status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
1072
+ message: tracingError
1073
+ },
1074
+ metrics: {
1075
+ enabled: features.metrics,
1076
+ status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
1077
+ message: metricsError
1078
+ },
1079
+ logging: {
1080
+ enabled: features.logging,
1081
+ status: loggingError ? "down" : "up",
1082
+ message: loggingError
1083
+ }
1084
+ }
1085
+ };
1086
+ };
1087
+ const shutdownFn = async () => {
1088
+ logger.info({}, "Shutting down foundation...");
1089
+ const promises = [];
1090
+ if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
1091
+ if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
1092
+ await Promise.all(promises);
1093
+ logger.info({}, "Foundation shutdown complete");
1094
+ };
1095
+ const isReadyFn = () => {
1096
+ if (features.tracing && !tracingError && !isTracingEnabled()) return false;
1097
+ if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
1098
+ return true;
1099
+ };
1100
+ const safeRunFn = async (fn, fallback) => {
1101
+ try {
1102
+ return await fn();
1103
+ } catch (err) {
1104
+ const error = err;
1105
+ logger.error({
1106
+ error: error.message,
1107
+ stack: error.stack
1108
+ }, "safeRun caught error");
1109
+ return fallback;
1110
+ }
1111
+ };
1112
+ const lifecycle = {
1113
+ health: buildHealthStatus,
1114
+ isReady: isReadyFn,
1115
+ shutdown: shutdownFn,
1116
+ safeRun: safeRunFn
1117
+ };
1118
+ const foundation = {
1119
+ config,
1120
+ features,
1121
+ observability,
1122
+ http: httpModule,
1123
+ lifecycle,
1124
+ logger,
1125
+ context: contextManager,
730
1126
  fastifyPlugin: createObservabilityPlugin({
731
1127
  serviceName,
732
1128
  logger,
@@ -735,91 +1131,47 @@ function createFoundation(input) {
735
1131
  metricsEnabled: features.metrics && !metricsError,
736
1132
  requestLogging: requestLoggingConfig
737
1133
  }),
738
- createHttpClient: (options) => createHttpClient({
739
- ...options,
740
- foundation
741
- }),
742
- shutdown: async () => {
743
- logger.info({}, "Shutting down foundation...");
744
- const promises = [];
745
- if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
746
- if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
747
- await Promise.all(promises);
748
- logger.info({}, "Foundation shutdown complete");
1134
+ tracer: tracerInstance,
1135
+ meter: meterInstance,
1136
+ getTracer: (name) => {
1137
+ warnDeprecation("getTracer", "observability.getTracer");
1138
+ return observability.getTracer(name);
1139
+ },
1140
+ getMeter: (name, version) => {
1141
+ warnDeprecation("getMeter", "observability.getMeter");
1142
+ return observability.getMeter(name, version);
1143
+ },
1144
+ getTraceContext: () => {
1145
+ warnDeprecation("getTraceContext", "observability.getTraceContext");
1146
+ return observability.getTraceContext();
1147
+ },
1148
+ getActiveSpan: () => {
1149
+ warnDeprecation("getActiveSpan", "observability.getActiveSpan");
1150
+ return observability.getActiveSpan();
1151
+ },
1152
+ createHttpClient: (options) => {
1153
+ warnDeprecation("createHttpClient", "http.createClient");
1154
+ return httpModule.createClient(options);
1155
+ },
1156
+ shutdown: () => {
1157
+ warnDeprecation("shutdown", "lifecycle.shutdown");
1158
+ return lifecycle.shutdown();
749
1159
  },
750
1160
  isReady: () => {
751
- if (features.tracing && !tracingError && !isTracingEnabled()) return false;
752
- if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
753
- return true;
1161
+ warnDeprecation("isReady", "lifecycle.isReady");
1162
+ return lifecycle.isReady();
754
1163
  },
755
1164
  health: () => {
756
- const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
757
- const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
758
- const loggingUp = !loggingError;
759
- const allUp = tracingUp && metricsUp && loggingUp;
760
- const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
761
- return {
762
- status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
763
- timestamp: new Date().toISOString(),
764
- service: serviceName,
765
- version: serviceVersion,
766
- uptime: Math.floor((Date.now() - startTime) / 1e3),
767
- components: {
768
- tracing: {
769
- enabled: features.tracing,
770
- status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
771
- message: tracingError
772
- },
773
- metrics: {
774
- enabled: features.metrics,
775
- status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
776
- message: metricsError
777
- },
778
- logging: {
779
- enabled: features.logging,
780
- status: loggingError ? "down" : "up",
781
- message: loggingError
782
- }
783
- }
784
- };
1165
+ warnDeprecation("health", "lifecycle.health");
1166
+ return lifecycle.health();
785
1167
  },
786
- trace: async (name, fn) => {
787
- const tracer = tracerInstance || getTracer(serviceName);
788
- return new Promise((resolve, reject) => {
789
- tracer.startActiveSpan(name, async (span) => {
790
- try {
791
- const result = await fn();
792
- span.setStatus({ code: SpanStatusCode.OK });
793
- span.end();
794
- resolve(result);
795
- } catch (err) {
796
- const error = err;
797
- span.setStatus({
798
- code: SpanStatusCode.ERROR,
799
- message: error.message
800
- });
801
- span.recordException(error);
802
- span.end();
803
- logger.error({
804
- span: name,
805
- error: error.message
806
- }, "Span failed");
807
- reject(error);
808
- }
809
- });
810
- });
1168
+ trace: (name, fn) => {
1169
+ warnDeprecation("trace", "observability.trace");
1170
+ return observability.trace(name, fn);
811
1171
  },
812
- safeRun: async (fn, fallback) => {
813
- try {
814
- return await fn();
815
- } catch (err) {
816
- const error = err;
817
- logger.error({
818
- error: error.message,
819
- stack: error.stack
820
- }, "safeRun caught error");
821
- return fallback;
822
- }
1172
+ safeRun: (fn, fallback) => {
1173
+ warnDeprecation("safeRun", "lifecycle.safeRun");
1174
+ return lifecycle.safeRun(fn, fallback);
823
1175
  }
824
1176
  };
825
1177
  return foundation;
@@ -828,5 +1180,254 @@ function createFoundation(input) {
828
1180
  const setupObservability = createFoundation;
829
1181
 
830
1182
  //#endregion
831
- export { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RequestLoggingConfigSchema, SpanStatusCode, context, createContextManager, createFallbackLogger, createFoundation, createHttpClient, createLogger, createObservabilityPlugin, getActiveSpan, getDefaultOtelEndpoint, getGlobalLogger, getMeter, getTraceContext, getTracer, isMetricsEnabled, isTracingEnabled, metrics, parseConfig, propagation, setGlobalLogger, setupMetrics, setupObservability, setupTracing, shutdownMetrics, shutdownTracing, trace };
1183
+ //#region src/integrations/object-store/aws-s3.ts
1184
+ async function loadAwsS3() {
1185
+ try {
1186
+ const clientMod = await import("@aws-sdk/client-s3");
1187
+ const presignerMod = await import("@aws-sdk/s3-request-presigner");
1188
+ return {
1189
+ S3Client: clientMod.S3Client,
1190
+ PutObjectCommand: clientMod.PutObjectCommand,
1191
+ GetObjectCommand: clientMod.GetObjectCommand,
1192
+ HeadObjectCommand: clientMod.HeadObjectCommand,
1193
+ DeleteObjectCommand: clientMod.DeleteObjectCommand,
1194
+ ListObjectsV2Command: clientMod.ListObjectsV2Command,
1195
+ getSignedUrl: presignerMod.getSignedUrl
1196
+ };
1197
+ } catch (err) {
1198
+ const e = err;
1199
+ throw new Error(`AWS SDK not available. Install optional peer deps: @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner. Original error: ${e.message}`);
1200
+ }
1201
+ }
1202
+ function normalizeBody(body) {
1203
+ return body;
1204
+ }
1205
+ /**
1206
+ * AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
1207
+ *
1208
+ * This implementation uses dynamic imports so `neoiq-foundation-node` can be used without AWS SDK.
1209
+ */
1210
+ var AwsS3ObjectStore = class {
1211
+ provider = "aws-s3";
1212
+ clientPromise;
1213
+ awsPromise = loadAwsS3();
1214
+ constructor(options = {}) {
1215
+ if (options.client) {
1216
+ this.clientPromise = Promise.resolve(options.client);
1217
+ return;
1218
+ }
1219
+ this.clientPromise = (async () => {
1220
+ const aws = await this.awsPromise;
1221
+ return new aws.S3Client(options.clientOptions ?? {});
1222
+ })();
1223
+ }
1224
+ async client() {
1225
+ return this.clientPromise;
1226
+ }
1227
+ async putObject(ref, body, options = {}) {
1228
+ const aws = await this.awsPromise;
1229
+ const s3 = await this.client();
1230
+ const res = await s3.send(new aws.PutObjectCommand({
1231
+ Bucket: ref.bucket,
1232
+ Key: ref.key,
1233
+ Body: normalizeBody(body),
1234
+ ContentType: options.contentType,
1235
+ CacheControl: options.cacheControl,
1236
+ Metadata: options.metadata
1237
+ }));
1238
+ return {
1239
+ etag: res?.ETag,
1240
+ versionId: res?.VersionId
1241
+ };
1242
+ }
1243
+ async getObject(ref) {
1244
+ const aws = await this.awsPromise;
1245
+ const s3 = await this.client();
1246
+ const res = await s3.send(new aws.GetObjectCommand({
1247
+ Bucket: ref.bucket,
1248
+ Key: ref.key
1249
+ }));
1250
+ if (!res?.Body) {
1251
+ const err = new Error(`S3 GetObject returned empty body: ${ref.bucket}/${ref.key}`);
1252
+ err.code = "EMPTY_BODY";
1253
+ throw err;
1254
+ }
1255
+ return {
1256
+ body: res.Body,
1257
+ contentType: res.ContentType,
1258
+ contentLength: res.ContentLength,
1259
+ etag: res.ETag,
1260
+ versionId: res.VersionId,
1261
+ metadata: res.Metadata,
1262
+ lastModified: res.LastModified
1263
+ };
1264
+ }
1265
+ async headObject(ref) {
1266
+ const aws = await this.awsPromise;
1267
+ const s3 = await this.client();
1268
+ try {
1269
+ const res = await s3.send(new aws.HeadObjectCommand({
1270
+ Bucket: ref.bucket,
1271
+ Key: ref.key
1272
+ }));
1273
+ return {
1274
+ exists: true,
1275
+ contentType: res.ContentType,
1276
+ contentLength: res.ContentLength,
1277
+ etag: res.ETag,
1278
+ versionId: res.VersionId,
1279
+ metadata: res.Metadata,
1280
+ lastModified: res.LastModified
1281
+ };
1282
+ } catch (err) {
1283
+ const e = err;
1284
+ const name = String(e?.name || "");
1285
+ const httpStatus = e?.$metadata?.httpStatusCode;
1286
+ if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
1287
+ throw err;
1288
+ }
1289
+ }
1290
+ async deleteObject(ref) {
1291
+ const aws = await this.awsPromise;
1292
+ const s3 = await this.client();
1293
+ await s3.send(new aws.DeleteObjectCommand({
1294
+ Bucket: ref.bucket,
1295
+ Key: ref.key
1296
+ }));
1297
+ }
1298
+ async listObjects(options) {
1299
+ const aws = await this.awsPromise;
1300
+ const s3 = await this.client();
1301
+ const res = await s3.send(new aws.ListObjectsV2Command({
1302
+ Bucket: options.bucket,
1303
+ Prefix: options.prefix,
1304
+ ContinuationToken: options.continuationToken,
1305
+ MaxKeys: options.maxKeys
1306
+ }));
1307
+ return {
1308
+ objects: (res?.Contents ?? []).map((o) => ({
1309
+ key: o.Key,
1310
+ size: o.Size,
1311
+ etag: o.ETag,
1312
+ lastModified: o.LastModified
1313
+ })),
1314
+ nextContinuationToken: res?.NextContinuationToken
1315
+ };
1316
+ }
1317
+ async presignGetObject(ref, options) {
1318
+ const aws = await this.awsPromise;
1319
+ const s3 = await this.client();
1320
+ return aws.getSignedUrl(s3, new aws.GetObjectCommand({
1321
+ Bucket: ref.bucket,
1322
+ Key: ref.key,
1323
+ ResponseContentType: options.responseContentType
1324
+ }), { expiresIn: options.expiresInSeconds });
1325
+ }
1326
+ async presignPutObject(ref, options) {
1327
+ const aws = await this.awsPromise;
1328
+ const s3 = await this.client();
1329
+ return aws.getSignedUrl(s3, new aws.PutObjectCommand({
1330
+ Bucket: ref.bucket,
1331
+ Key: ref.key,
1332
+ ContentType: options.contentType
1333
+ }), { expiresIn: options.expiresInSeconds });
1334
+ }
1335
+ };
1336
+
1337
+ //#endregion
1338
+ //#region src/integrations/object-store/in-memory.ts
1339
+ function toBuffer(body) {
1340
+ if (typeof body === "string") return Buffer.from(body);
1341
+ if (Buffer.isBuffer(body)) return body;
1342
+ if (body instanceof Uint8Array) return Buffer.from(body);
1343
+ return new Promise((resolve, reject) => {
1344
+ const chunks = [];
1345
+ body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1346
+ body.on("end", () => resolve(Buffer.concat(chunks)));
1347
+ body.on("error", reject);
1348
+ });
1349
+ }
1350
+ function simpleEtag(buf) {
1351
+ return `mem-${buf.length}-${buf.subarray(0, 8).toString("hex")}`;
1352
+ }
1353
+ /**
1354
+ * InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
1355
+ * NOTE: presign* methods are not meaningful here and will throw.
1356
+ */
1357
+ var InMemoryObjectStore = class {
1358
+ provider = "in-memory";
1359
+ buckets = new Map();
1360
+ bucketMap(bucket) {
1361
+ let m = this.buckets.get(bucket);
1362
+ if (!m) {
1363
+ m = new Map();
1364
+ this.buckets.set(bucket, m);
1365
+ }
1366
+ return m;
1367
+ }
1368
+ async putObject(ref, body, options = {}) {
1369
+ const buf = await toBuffer(body);
1370
+ const obj = {
1371
+ body: buf,
1372
+ contentType: options.contentType,
1373
+ cacheControl: options.cacheControl,
1374
+ metadata: options.metadata,
1375
+ etag: simpleEtag(buf),
1376
+ lastModified: new Date()
1377
+ };
1378
+ this.bucketMap(ref.bucket).set(ref.key, obj);
1379
+ return { etag: obj.etag };
1380
+ }
1381
+ async getObject(ref) {
1382
+ const obj = this.bucketMap(ref.bucket).get(ref.key);
1383
+ if (!obj) {
1384
+ const err = new Error(`Object not found: ${ref.bucket}/${ref.key}`);
1385
+ err.code = "OBJECT_NOT_FOUND";
1386
+ throw err;
1387
+ }
1388
+ return {
1389
+ body: Readable.from(obj.body),
1390
+ contentType: obj.contentType,
1391
+ contentLength: obj.body.length,
1392
+ etag: obj.etag,
1393
+ metadata: obj.metadata,
1394
+ lastModified: obj.lastModified
1395
+ };
1396
+ }
1397
+ async headObject(ref) {
1398
+ const obj = this.bucketMap(ref.bucket).get(ref.key);
1399
+ if (!obj) return { exists: false };
1400
+ return {
1401
+ exists: true,
1402
+ contentType: obj.contentType,
1403
+ contentLength: obj.body.length,
1404
+ etag: obj.etag,
1405
+ metadata: obj.metadata,
1406
+ lastModified: obj.lastModified
1407
+ };
1408
+ }
1409
+ async deleteObject(ref) {
1410
+ this.bucketMap(ref.bucket).delete(ref.key);
1411
+ }
1412
+ async listObjects(options) {
1413
+ const { bucket, prefix = "" } = options;
1414
+ const map = this.bucketMap(bucket);
1415
+ const objects = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
1416
+ key,
1417
+ size: obj.body.length,
1418
+ etag: obj.etag,
1419
+ lastModified: obj.lastModified
1420
+ })).sort((a, b) => a.key.localeCompare(b.key));
1421
+ return { objects };
1422
+ }
1423
+ async presignGetObject(_ref, _options) {
1424
+ throw new Error("InMemoryObjectStore does not support presigned URLs");
1425
+ }
1426
+ async presignPutObject(_ref, _options) {
1427
+ throw new Error("InMemoryObjectStore does not support presigned URLs");
1428
+ }
1429
+ };
1430
+
1431
+ //#endregion
1432
+ export { AutoInstrumentationConfigSchema, AwsS3ObjectStore, FeaturesConfigSchema, FoundationConfigSchema, InMemoryObjectStore, LoggingConfigSchema, OtelConfigSchema, REDACT_PATHS, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, buildPinoRedactConfig, context, createContextManager, createFallbackLogger, createFoundation, createHttpClient, createLogger, createObservabilityPlugin, getActiveSpan, getDefaultOtelEndpoint, getGlobalLogger, getMeter, getTraceContext, getTracer, isMetricsEnabled, isTracingEnabled, metrics, parseConfig, propagation, sanitizeBody, setGlobalLogger, setupMetrics, setupObservability, setupTracing, shutdownMetrics, shutdownTracing, trace };
832
1433
  //# sourceMappingURL=index.mjs.map