@ciq-dev/neoiq-foundation-node 1.0.1-beta.1 → 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,
@@ -126,11 +353,14 @@ function createLogger(options) {
126
353
  const traceId = spanContext?.traceId || ctx?.traceId;
127
354
  const spanId = spanContext?.spanId || ctx?.spanId;
128
355
  const correlationId = ctx?.correlationId;
129
- return {
356
+ const contextData = ctx?.contextData;
357
+ const result = {
130
358
  trace_id: traceId,
131
359
  span_id: spanId,
132
360
  correlation_id: correlationId
133
361
  };
362
+ if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
363
+ return result;
134
364
  },
135
365
  formatters: { level: (label) => ({ level: label }) },
136
366
  transport: prettyPrint ? {
@@ -191,12 +421,26 @@ function getGlobalLogger() {
191
421
  //#region src/features/tracing.ts
192
422
  let sdk = null;
193
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
+ }
194
437
  /** Initialize OpenTelemetry tracing */
195
438
  function setupTracing(options) {
196
439
  if (isInitialized$1) {
197
440
  console.warn("[neoiq-foundation] Tracing already initialized");
198
441
  return;
199
442
  }
443
+ warnPreloadedModules();
200
444
  const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
201
445
  const resource = resourceFromAttributes({
202
446
  [ATTR_SERVICE_NAME]: serviceName,
@@ -353,7 +597,6 @@ function createObservabilityPlugin(options) {
353
597
  const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
354
598
  const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
355
599
  const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
356
- const tracer = tracingEnabled ? trace$1.getTracer("neoiq-foundation-node") : null;
357
600
  let requestCounter;
358
601
  let requestDuration;
359
602
  let requestErrors;
@@ -370,22 +613,12 @@ function createObservabilityPlugin(options) {
370
613
  }
371
614
  const correlationId = request.headers["x-request-id"] || randomUUID();
372
615
  reply.header("x-request-id", correlationId);
373
- let span;
374
616
  let traceId = "";
375
617
  let spanId = "";
376
- if (tracer) {
377
- const parentContext = propagation$1.extract(context$1.active(), request.headers);
378
- span = tracer.startSpan(`${request.method} ${request.routeOptions?.url || request.url}`, {
379
- kind: 1,
380
- attributes: {
381
- "http.method": request.method,
382
- "http.url": request.url,
383
- "http.route": request.routeOptions?.url || request.url,
384
- "http.user_agent": request.headers["user-agent"] || "",
385
- "http.correlation_id": correlationId
386
- }
387
- }, parentContext);
388
- 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();
389
622
  traceId = spanContext.traceId;
390
623
  spanId = spanContext.spanId;
391
624
  }
@@ -395,7 +628,6 @@ function createObservabilityPlugin(options) {
395
628
  spanId,
396
629
  startTime: Date.now()
397
630
  };
398
- request.__span = span;
399
631
  request.__requestContext = requestContext;
400
632
  runInContext(requestContext, () => {
401
633
  const logData = {
@@ -421,7 +653,7 @@ function createObservabilityPlugin(options) {
421
653
  runInContext(ctx, () => {
422
654
  logger.debug({
423
655
  correlation_id: ctx.correlationId,
424
- body: truncateBody(request.body, maxBodySize)
656
+ body: sanitizeBody(truncateBody(request.body, maxBodySize))
425
657
  }, "Request body");
426
658
  done();
427
659
  });
@@ -436,14 +668,13 @@ function createObservabilityPlugin(options) {
436
668
  logger.debug({
437
669
  correlation_id: ctx.correlationId,
438
670
  statusCode: reply.statusCode,
439
- body: truncateBody(payload, maxBodySize)
671
+ body: sanitizeBody(truncateBody(payload, maxBodySize))
440
672
  }, "Response body");
441
673
  done(null, payload);
442
674
  });
443
675
  });
444
676
  fastify.addHook("onResponse", (request, reply, done) => {
445
677
  const ctx = request.__requestContext;
446
- const span = request.__span;
447
678
  if (!ctx) {
448
679
  done();
449
680
  return;
@@ -467,18 +698,15 @@ function createObservabilityPlugin(options) {
467
698
  requestDuration.record(durationMs, labels);
468
699
  if (reply.statusCode >= 400) requestErrors.add(1, labels);
469
700
  }
470
- if (span) {
471
- span.setStatus({ code: reply.statusCode < 400 ? SpanStatusCode$1.OK : SpanStatusCode$1.ERROR });
472
- span.setAttribute("http.status_code", reply.statusCode);
473
- span.setAttribute("http.response_time_ms", durationMs);
474
- span.end();
701
+ if (tracingEnabled) {
702
+ const activeSpan = trace$1.getActiveSpan();
703
+ if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
475
704
  }
476
705
  done();
477
706
  });
478
707
  });
479
708
  fastify.addHook("onError", (request, _reply, error, done) => {
480
709
  const ctx = request.__requestContext;
481
- const span = request.__span;
482
710
  if (!ctx) {
483
711
  done();
484
712
  return;
@@ -490,12 +718,15 @@ function createObservabilityPlugin(options) {
490
718
  url: request.url,
491
719
  error: error.message
492
720
  }, "Request failed");
493
- if (span) {
494
- span.setStatus({
495
- code: SpanStatusCode$1.ERROR,
496
- message: error.message
497
- });
498
- 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
+ }
499
730
  }
500
731
  done();
501
732
  });
@@ -625,17 +856,54 @@ function createHttpClient(options) {
625
856
  breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
626
857
  breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
627
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;
628
890
  }
629
891
  return client;
630
892
  }
631
893
 
632
894
  //#endregion
633
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
+ }
634
902
  /** Create a fully configured observability foundation */
635
903
  function createFoundation(input) {
636
904
  const startTime = Date.now();
637
905
  const config = parseConfig(input);
638
- 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;
639
907
  const features = {
640
908
  tracing: featuresConfig.tracing ?? true,
641
909
  metrics: featuresConfig.metrics ?? true,
@@ -651,7 +919,8 @@ function createFoundation(input) {
651
919
  environment,
652
920
  level: loggingConfig.level ?? "info",
653
921
  prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
654
- contextManager
922
+ contextManager,
923
+ additionalRedactPaths: redactionConfig.additionalPaths
655
924
  });
656
925
  setGlobalLogger(logger);
657
926
  } catch (err) {
@@ -723,17 +992,137 @@ function createFoundation(input) {
723
992
  environment,
724
993
  features
725
994
  }, "Foundation initialized");
726
- const foundation = {
727
- 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 = {
728
1043
  logger,
729
- context: contextManager,
730
1044
  tracer: tracerInstance,
731
1045
  meter: meterInstance,
732
- features,
733
1046
  getTracer: (name) => getTracer(name || serviceName),
734
1047
  getMeter: (name, version) => getMeter(name, version),
735
1048
  getTraceContext,
736
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,
737
1126
  fastifyPlugin: createObservabilityPlugin({
738
1127
  serviceName,
739
1128
  logger,
@@ -742,91 +1131,47 @@ function createFoundation(input) {
742
1131
  metricsEnabled: features.metrics && !metricsError,
743
1132
  requestLogging: requestLoggingConfig
744
1133
  }),
745
- createHttpClient: (options) => createHttpClient({
746
- ...options,
747
- foundation
748
- }),
749
- shutdown: async () => {
750
- logger.info({}, "Shutting down foundation...");
751
- const promises = [];
752
- if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
753
- if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
754
- await Promise.all(promises);
755
- 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();
756
1159
  },
757
1160
  isReady: () => {
758
- if (features.tracing && !tracingError && !isTracingEnabled()) return false;
759
- if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
760
- return true;
1161
+ warnDeprecation("isReady", "lifecycle.isReady");
1162
+ return lifecycle.isReady();
761
1163
  },
762
1164
  health: () => {
763
- const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
764
- const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
765
- const loggingUp = !loggingError;
766
- const allUp = tracingUp && metricsUp && loggingUp;
767
- const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
768
- return {
769
- status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
770
- timestamp: new Date().toISOString(),
771
- service: serviceName,
772
- version: serviceVersion,
773
- uptime: Math.floor((Date.now() - startTime) / 1e3),
774
- components: {
775
- tracing: {
776
- enabled: features.tracing,
777
- status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
778
- message: tracingError
779
- },
780
- metrics: {
781
- enabled: features.metrics,
782
- status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
783
- message: metricsError
784
- },
785
- logging: {
786
- enabled: features.logging,
787
- status: loggingError ? "down" : "up",
788
- message: loggingError
789
- }
790
- }
791
- };
1165
+ warnDeprecation("health", "lifecycle.health");
1166
+ return lifecycle.health();
792
1167
  },
793
- trace: async (name, fn) => {
794
- const tracer = tracerInstance || getTracer(serviceName);
795
- return new Promise((resolve, reject) => {
796
- tracer.startActiveSpan(name, async (span) => {
797
- try {
798
- const result = await fn();
799
- span.setStatus({ code: SpanStatusCode.OK });
800
- span.end();
801
- resolve(result);
802
- } catch (err) {
803
- const error = err;
804
- span.setStatus({
805
- code: SpanStatusCode.ERROR,
806
- message: error.message
807
- });
808
- span.recordException(error);
809
- span.end();
810
- logger.error({
811
- span: name,
812
- error: error.message
813
- }, "Span failed");
814
- reject(error);
815
- }
816
- });
817
- });
1168
+ trace: (name, fn) => {
1169
+ warnDeprecation("trace", "observability.trace");
1170
+ return observability.trace(name, fn);
818
1171
  },
819
- safeRun: async (fn, fallback) => {
820
- try {
821
- return await fn();
822
- } catch (err) {
823
- const error = err;
824
- logger.error({
825
- error: error.message,
826
- stack: error.stack
827
- }, "safeRun caught error");
828
- return fallback;
829
- }
1172
+ safeRun: (fn, fallback) => {
1173
+ warnDeprecation("safeRun", "lifecycle.safeRun");
1174
+ return lifecycle.safeRun(fn, fallback);
830
1175
  }
831
1176
  };
832
1177
  return foundation;
@@ -835,5 +1180,254 @@ function createFoundation(input) {
835
1180
  const setupObservability = createFoundation;
836
1181
 
837
1182
  //#endregion
838
- 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 };
839
1433
  //# sourceMappingURL=index.mjs.map