@ciq-dev/neoiq-foundation-node 1.0.1-beta.1 → 1.0.1-beta.3

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,12 +1,9 @@
1
- import { z } from "zod";
2
- import { AsyncLocalStorage } from "async_hooks";
3
- import pino from "pino";
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 { NodeSDK } from "@opentelemetry/sdk-node";
6
- import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
7
- import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
1
+ import { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, context, getActiveSpan, getDefaultOtelEndpoint, getTraceContext, getTracer, isTracingEnabled, parseConfig, propagation, setupTracing, shutdownTracing, trace } from "./tracing-Cv-Y3fZx.mjs";
8
2
  import { resourceFromAttributes } from "@opentelemetry/resources";
9
3
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
4
+ import { SpanStatusCode as SpanStatusCode$1, context as context$1, metrics, propagation as propagation$1, trace as trace$1 } from "@opentelemetry/api";
5
+ import { AsyncLocalStorage } from "async_hooks";
6
+ import pino from "pino";
10
7
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
11
8
  import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
12
9
  import fp from "fastify-plugin";
@@ -14,106 +11,251 @@ import { randomUUID } from "crypto";
14
11
  import axios from "axios";
15
12
  import axiosRetry from "axios-retry";
16
13
  import CircuitBreaker from "opossum";
14
+ import { createHash } from "node:crypto";
15
+ import { Readable } from "node:stream";
17
16
 
18
- //#region src/config.ts
19
- const AutoInstrumentationConfigSchema = z.object({
20
- http: z.boolean().default(true),
21
- fastify: z.boolean().default(true),
22
- express: z.boolean().default(true),
23
- mongodb: z.boolean().default(true),
24
- pg: z.boolean().default(true),
25
- mysql: z.boolean().default(true),
26
- redis: z.boolean().default(true),
27
- ioredis: z.boolean().default(true),
28
- grpc: z.boolean().default(true),
29
- fs: z.boolean().default(false),
30
- dns: z.boolean().default(false)
31
- }).partial();
32
- const FeaturesConfigSchema = z.object({
33
- tracing: z.boolean().default(true),
34
- metrics: z.boolean().default(true),
35
- logging: z.boolean().default(true),
36
- autoInstrumentation: AutoInstrumentationConfigSchema.default({})
37
- }).partial();
38
- const DEFAULT_OTEL_ENDPOINT = "http://otel-stack-deployment-collector.observability.svc.cluster.local:4317";
39
- const OtelConfigSchema = z.object({
40
- endpoint: z.string().default(DEFAULT_OTEL_ENDPOINT),
41
- metricsIntervalMs: z.number().min(1e3).default(5e3),
42
- traceSampleRate: z.number().min(0).max(1).default(1)
43
- }).partial();
44
- const LoggingConfigSchema = z.object({
45
- level: z.enum([
46
- "debug",
47
- "info",
48
- "warn",
49
- "error"
50
- ]).default("info"),
51
- prettyPrint: z.boolean().optional()
52
- }).partial();
53
- const RequestLoggingConfigSchema = z.object({
54
- logHeaders: z.boolean().default(true),
55
- logBody: z.boolean().default(false),
56
- logResponseBody: z.boolean().default(false),
57
- maxBodySize: z.number().default(10 * 1024),
58
- redactHeaders: z.array(z.string()).optional()
59
- }).partial();
60
- const FoundationConfigSchema = z.object({
61
- serviceName: z.string().min(1, "serviceName is required"),
62
- serviceVersion: z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
63
- environment: z.enum([
64
- "development",
65
- "staging",
66
- "qa",
67
- "production"
68
- ]).default(process.env.NODE_ENV || "development"),
69
- features: FeaturesConfigSchema.default({}),
70
- otel: OtelConfigSchema.default({}),
71
- logging: LoggingConfigSchema.default({}),
72
- requestLogging: RequestLoggingConfigSchema.default({})
73
- });
74
- /** Parse and validate configuration */
75
- function parseConfig(input) {
76
- const result = FoundationConfigSchema.safeParse(input);
77
- if (!result.success) {
78
- const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
79
- throw new Error(`Invalid foundation configuration:\n${errors}`);
80
- }
81
- return result.data;
17
+ //#region src/features/context.ts
18
+ const BAGGAGE_CORRELATION_KEY = "correlation.id";
19
+ function setBaggageCorrelationId(correlationId) {
20
+ const currentBaggage = propagation$1.getBaggage(context$1.active());
21
+ const baggage = (currentBaggage ?? propagation$1.createBaggage()).setEntry(BAGGAGE_CORRELATION_KEY, { value: correlationId });
22
+ return propagation$1.setBaggage(context$1.active(), baggage);
82
23
  }
83
- /** Get default OTEL endpoint */
84
- function getDefaultOtelEndpoint() {
85
- return process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTEL_ENDPOINT;
24
+ function getBaggageCorrelationId() {
25
+ const baggage = propagation$1.getBaggage(context$1.active());
26
+ return baggage?.getEntry(BAGGAGE_CORRELATION_KEY)?.value;
86
27
  }
87
-
88
- //#endregion
89
- //#region src/features/context.ts
90
28
  /** Create a new context manager instance */
91
29
  function createContextManager() {
92
30
  const als = new AsyncLocalStorage();
93
31
  return {
94
- getContext: () => als.getStore(),
32
+ getContext() {
33
+ const alsCtx = als.getStore();
34
+ if (!alsCtx) return void 0;
35
+ const baggageCorrelationId = getBaggageCorrelationId();
36
+ if (baggageCorrelationId && baggageCorrelationId !== alsCtx.correlationId) return {
37
+ ...alsCtx,
38
+ correlationId: baggageCorrelationId
39
+ };
40
+ return alsCtx;
41
+ },
95
42
  run(context$2, fn) {
43
+ if (context$2.correlationId) {
44
+ const otelCtx = setBaggageCorrelationId(context$2.correlationId);
45
+ return context$1.with(otelCtx, () => als.run(context$2, fn));
46
+ }
96
47
  return als.run(context$2, fn);
97
48
  },
98
49
  get(key) {
50
+ if (key === "correlationId") return getBaggageCorrelationId() ?? als.getStore()?.correlationId;
99
51
  return als.getStore()?.[key];
100
52
  },
101
53
  update(updates) {
102
54
  const current = als.getStore();
103
55
  if (!current) return void 0;
56
+ if (updates.correlationId) setBaggageCorrelationId(updates.correlationId);
104
57
  Object.assign(current, updates);
105
58
  return current;
59
+ },
60
+ setContextValue(key, value) {
61
+ const current = als.getStore();
62
+ if (!current) return;
63
+ if (!current.contextData) current.contextData = {};
64
+ current.contextData[key] = value;
65
+ },
66
+ getContextValue(key) {
67
+ return als.getStore()?.contextData?.[key];
68
+ },
69
+ getContextData() {
70
+ return als.getStore()?.contextData ?? {};
71
+ },
72
+ setContextData(data) {
73
+ const current = als.getStore();
74
+ if (!current) return;
75
+ current.contextData = {
76
+ ...current.contextData ?? {},
77
+ ...data
78
+ };
79
+ },
80
+ clearContextData() {
81
+ const current = als.getStore();
82
+ if (!current) return;
83
+ current.contextData = {};
106
84
  }
107
85
  };
108
86
  }
109
87
 
88
+ //#endregion
89
+ //#region src/features/redaction.ts
90
+ /**
91
+ * Log Redaction & PII Sanitization
92
+ *
93
+ * Two-layer approach:
94
+ * - Layer A: Pino native `redact` (fast-redact) for key-based redaction on every log call.
95
+ * Compiled at init, near-zero runtime cost.
96
+ * - Layer B: Deep-traverse sanitizer for value-pattern detection (JWTs, AWS keys, etc.).
97
+ * Only used for request/response body logging — NOT on every log call.
98
+ */
99
+ const PLACEHOLDER = "[REDACTED]";
100
+ /**
101
+ * Key names that Pino's fast-redact will censor automatically.
102
+ * Supports wildcards: '*.password' matches nested keys one level deep.
103
+ */
104
+ const REDACT_PATHS = [
105
+ "password",
106
+ "passwd",
107
+ "pass",
108
+ "pwd",
109
+ "secret",
110
+ "secretKey",
111
+ "secret_key",
112
+ "token",
113
+ "accessToken",
114
+ "access_token",
115
+ "refreshToken",
116
+ "refresh_token",
117
+ "idToken",
118
+ "id_token",
119
+ "apiKey",
120
+ "api_key",
121
+ "apiSecret",
122
+ "api_secret",
123
+ "authorization",
124
+ "auth",
125
+ "credentials",
126
+ "privateKey",
127
+ "private_key",
128
+ "cookie",
129
+ "setCookie",
130
+ "set_cookie",
131
+ "creditCard",
132
+ "credit_card",
133
+ "cardNumber",
134
+ "card_number",
135
+ "ccNumber",
136
+ "cc_number",
137
+ "cvv",
138
+ "cvc",
139
+ "securityCode",
140
+ "security_code",
141
+ "accountNumber",
142
+ "account_number",
143
+ "ssn",
144
+ "socialSecurity",
145
+ "social_security",
146
+ "dateOfBirth",
147
+ "date_of_birth",
148
+ "dob",
149
+ "*.password",
150
+ "*.secret",
151
+ "*.token",
152
+ "*.apiKey",
153
+ "*.api_key",
154
+ "*.authorization",
155
+ "*.cookie",
156
+ "*.credentials",
157
+ "*.creditCard",
158
+ "*.cardNumber",
159
+ "*.cvv",
160
+ "*.ssn",
161
+ "*.privateKey",
162
+ "*.private_key"
163
+ ];
164
+ /**
165
+ * Value patterns that indicate a secret regardless of the key name.
166
+ * Used only for body sanitization (Layer B).
167
+ */
168
+ const VALUE_PATTERNS = [
169
+ {
170
+ label: "jwt",
171
+ pattern: /^eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+$/
172
+ },
173
+ {
174
+ label: "aws_access_key",
175
+ pattern: /(?:^|[^A-Za-z0-9])AKIA[0-9A-Z]{16}(?:$|[^A-Za-z0-9])/
176
+ },
177
+ {
178
+ label: "stripe_key",
179
+ pattern: /^[sr]k_(live|test)_[A-Za-z0-9]{10,}$/
180
+ },
181
+ {
182
+ label: "openai_key",
183
+ pattern: /^sk-[A-Za-z0-9_-]{20,}$/
184
+ },
185
+ {
186
+ label: "github_token",
187
+ pattern: /^gh[ps]_[A-Za-z0-9]{36,}$/
188
+ },
189
+ {
190
+ label: "pem_private_key",
191
+ pattern: /-----BEGIN\s+(RSA\s+|EC\s+)?PRIVATE\s+KEY-----/
192
+ },
193
+ {
194
+ label: "connection_string",
195
+ pattern: /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:]+:[^@]+@/
196
+ },
197
+ {
198
+ label: "bearer_token",
199
+ pattern: /^Bearer\s+\S{10,}$/i
200
+ }
201
+ ];
202
+ const SENSITIVE_KEYS = new Set(REDACT_PATHS.filter((p) => !p.includes("*")).map((k) => k.toLowerCase()));
203
+ const MAX_DEPTH = 10;
204
+ const MAX_KEYS = 200;
205
+ function mask(value) {
206
+ if (typeof value !== "string" || value.length <= 8) return PLACEHOLDER;
207
+ return `${value.slice(0, 4)}${"*".repeat(Math.min(value.length - 8, 20))}${value.slice(-4)}`;
208
+ }
209
+ function isSensitiveValue(value) {
210
+ return VALUE_PATTERNS.some((p) => p.pattern.test(value));
211
+ }
212
+ function sanitizeValue(value, depth) {
213
+ if (depth > MAX_DEPTH) return value;
214
+ if (typeof value === "string" && isSensitiveValue(value)) return mask(value);
215
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, depth + 1));
216
+ if (value !== null && typeof value === "object") return sanitizeObject(value, depth + 1);
217
+ return value;
218
+ }
219
+ function sanitizeObject(obj, depth) {
220
+ if (depth > MAX_DEPTH) return obj;
221
+ const keys = Object.keys(obj);
222
+ if (keys.length > MAX_KEYS) return obj;
223
+ const result = {};
224
+ for (const key of keys) {
225
+ const value = obj[key];
226
+ if (SENSITIVE_KEYS.has(key.toLowerCase())) result[key] = typeof value === "string" ? mask(value) : PLACEHOLDER;
227
+ else result[key] = sanitizeValue(value, depth);
228
+ }
229
+ return result;
230
+ }
231
+ /**
232
+ * Deep-traverse sanitizer for request/response bodies.
233
+ * Checks both key names (deny-list) and value patterns (JWT, AWS keys, etc.).
234
+ * NOT intended for every log call — use Pino native `redact` for that.
235
+ */
236
+ function sanitizeBody(body) {
237
+ if (body === null || body === void 0) return body;
238
+ if (typeof body === "string") return isSensitiveValue(body) ? mask(body) : body;
239
+ if (typeof body !== "object") return body;
240
+ if (Array.isArray(body)) return body.map((item) => sanitizeValue(item, 0));
241
+ return sanitizeObject(body, 0);
242
+ }
243
+ /** Build the Pino `redact` config object for fast-redact integration */
244
+ function buildPinoRedactConfig(additionalPaths = []) {
245
+ return {
246
+ paths: [...REDACT_PATHS, ...additionalPaths],
247
+ censor: PLACEHOLDER
248
+ };
249
+ }
250
+
110
251
  //#endregion
111
252
  //#region src/features/logging.ts
112
253
  /** Create a structured logger with automatic trace context injection */
113
254
  function createLogger(options) {
114
- const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager } = options;
255
+ const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager, additionalRedactPaths = [] } = options;
115
256
  const pinoLogger = pino({
116
257
  level,
258
+ redact: buildPinoRedactConfig(additionalRedactPaths),
117
259
  base: {
118
260
  service: serviceName,
119
261
  version: serviceVersion,
@@ -126,11 +268,14 @@ function createLogger(options) {
126
268
  const traceId = spanContext?.traceId || ctx?.traceId;
127
269
  const spanId = spanContext?.spanId || ctx?.spanId;
128
270
  const correlationId = ctx?.correlationId;
129
- return {
271
+ const contextData = ctx?.contextData;
272
+ const result = {
130
273
  trace_id: traceId,
131
274
  span_id: spanId,
132
275
  correlation_id: correlationId
133
276
  };
277
+ if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
278
+ return result;
134
279
  },
135
280
  formatters: { level: (label) => ({ level: label }) },
136
281
  transport: prettyPrint ? {
@@ -155,12 +300,13 @@ function wrapPinoLogger(pinoLogger) {
155
300
  };
156
301
  }
157
302
  /** Fallback logger when Pino is not available */
158
- function createFallbackLogger(serviceName = "unknown") {
303
+ function createFallbackLogger(serviceName = "unknown", baseBindings = {}) {
159
304
  const log = (level, obj, msg) => {
160
305
  const logObj = {
161
306
  timestamp: new Date().toISOString(),
162
307
  level,
163
308
  service: serviceName,
309
+ ...baseBindings,
164
310
  ...obj,
165
311
  msg
166
312
  };
@@ -174,7 +320,10 @@ function createFallbackLogger(serviceName = "unknown") {
174
320
  info: (obj, msg) => log("info", obj, msg),
175
321
  warn: (obj, msg) => log("warn", obj, msg),
176
322
  error: (obj, msg) => log("error", obj, msg),
177
- child: () => fallback,
323
+ child: (bindings) => createFallbackLogger(serviceName, {
324
+ ...baseBindings,
325
+ ...bindings
326
+ }),
178
327
  pino: null
179
328
  };
180
329
  return fallback;
@@ -187,84 +336,6 @@ function getGlobalLogger() {
187
336
  return globalLogger || createFallbackLogger();
188
337
  }
189
338
 
190
- //#endregion
191
- //#region src/features/tracing.ts
192
- let sdk = null;
193
- let isInitialized$1 = false;
194
- /** Initialize OpenTelemetry tracing */
195
- function setupTracing(options) {
196
- if (isInitialized$1) {
197
- console.warn("[neoiq-foundation] Tracing already initialized");
198
- return;
199
- }
200
- const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
201
- const resource = resourceFromAttributes({
202
- [ATTR_SERVICE_NAME]: serviceName,
203
- [ATTR_SERVICE_VERSION]: serviceVersion,
204
- "deployment.environment": environment
205
- });
206
- const traceExporter = new OTLPTraceExporter({ url: endpoint });
207
- const instrumentationConfig = buildInstrumentationConfig(autoInstrumentation);
208
- sdk = new NodeSDK({
209
- resource,
210
- traceExporter,
211
- instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)]
212
- });
213
- sdk.start();
214
- isInitialized$1 = true;
215
- }
216
- function buildInstrumentationConfig(config) {
217
- const mapping = {
218
- http: "@opentelemetry/instrumentation-http",
219
- fastify: "@opentelemetry/instrumentation-fastify",
220
- express: "@opentelemetry/instrumentation-express",
221
- mongodb: "@opentelemetry/instrumentation-mongodb",
222
- pg: "@opentelemetry/instrumentation-pg",
223
- mysql: "@opentelemetry/instrumentation-mysql",
224
- redis: "@opentelemetry/instrumentation-redis",
225
- ioredis: "@opentelemetry/instrumentation-ioredis",
226
- grpc: "@opentelemetry/instrumentation-grpc",
227
- fs: "@opentelemetry/instrumentation-fs",
228
- dns: "@opentelemetry/instrumentation-dns"
229
- };
230
- const result = {};
231
- for (const [key, instrumentationName] of Object.entries(mapping)) {
232
- const userSetting = config[key];
233
- const defaultValue = key !== "fs" && key !== "dns";
234
- result[instrumentationName] = { enabled: userSetting ?? defaultValue };
235
- }
236
- return result;
237
- }
238
- /** Shutdown tracing gracefully */
239
- async function shutdownTracing() {
240
- if (!sdk) return;
241
- try {
242
- await sdk.shutdown();
243
- isInitialized$1 = false;
244
- sdk = null;
245
- } catch (error) {
246
- console.error("[neoiq-foundation] Error shutting down tracing:", error);
247
- }
248
- }
249
- function getTracer(name) {
250
- return trace.getTracer(name);
251
- }
252
- function getActiveSpan() {
253
- return trace.getActiveSpan();
254
- }
255
- function getTraceContext() {
256
- const span = trace.getActiveSpan();
257
- if (!span) return {};
258
- const ctx = span.spanContext();
259
- return {
260
- traceId: ctx.traceId,
261
- spanId: ctx.spanId
262
- };
263
- }
264
- function isTracingEnabled() {
265
- return isInitialized$1;
266
- }
267
-
268
339
  //#endregion
269
340
  //#region src/features/metrics.ts
270
341
  let meterProvider = null;
@@ -302,6 +373,7 @@ async function shutdownMetrics() {
302
373
  meterProvider = null;
303
374
  } catch (error) {
304
375
  console.error("[neoiq-foundation] Error shutting down metrics:", error);
376
+ throw error;
305
377
  }
306
378
  }
307
379
  function getMeter(name, version = "1.0.0") {
@@ -353,7 +425,6 @@ function createObservabilityPlugin(options) {
353
425
  const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
354
426
  const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
355
427
  const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
356
- const tracer = tracingEnabled ? trace$1.getTracer("neoiq-foundation-node") : null;
357
428
  let requestCounter;
358
429
  let requestDuration;
359
430
  let requestErrors;
@@ -370,22 +441,12 @@ function createObservabilityPlugin(options) {
370
441
  }
371
442
  const correlationId = request.headers["x-request-id"] || randomUUID();
372
443
  reply.header("x-request-id", correlationId);
373
- let span;
374
444
  let traceId = "";
375
445
  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();
446
+ const activeSpan = tracingEnabled ? trace$1.getActiveSpan() : void 0;
447
+ if (activeSpan) {
448
+ activeSpan.setAttribute("http.correlation_id", correlationId);
449
+ const spanContext = activeSpan.spanContext();
389
450
  traceId = spanContext.traceId;
390
451
  spanId = spanContext.spanId;
391
452
  }
@@ -395,7 +456,6 @@ function createObservabilityPlugin(options) {
395
456
  spanId,
396
457
  startTime: Date.now()
397
458
  };
398
- request.__span = span;
399
459
  request.__requestContext = requestContext;
400
460
  runInContext(requestContext, () => {
401
461
  const logData = {
@@ -421,7 +481,7 @@ function createObservabilityPlugin(options) {
421
481
  runInContext(ctx, () => {
422
482
  logger.debug({
423
483
  correlation_id: ctx.correlationId,
424
- body: truncateBody(request.body, maxBodySize)
484
+ body: sanitizeBody(truncateBody(request.body, maxBodySize))
425
485
  }, "Request body");
426
486
  done();
427
487
  });
@@ -436,14 +496,13 @@ function createObservabilityPlugin(options) {
436
496
  logger.debug({
437
497
  correlation_id: ctx.correlationId,
438
498
  statusCode: reply.statusCode,
439
- body: truncateBody(payload, maxBodySize)
499
+ body: sanitizeBody(truncateBody(payload, maxBodySize))
440
500
  }, "Response body");
441
501
  done(null, payload);
442
502
  });
443
503
  });
444
504
  fastify.addHook("onResponse", (request, reply, done) => {
445
505
  const ctx = request.__requestContext;
446
- const span = request.__span;
447
506
  if (!ctx) {
448
507
  done();
449
508
  return;
@@ -467,18 +526,15 @@ function createObservabilityPlugin(options) {
467
526
  requestDuration.record(durationMs, labels);
468
527
  if (reply.statusCode >= 400) requestErrors.add(1, labels);
469
528
  }
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();
529
+ if (tracingEnabled) {
530
+ const activeSpan = trace$1.getActiveSpan();
531
+ if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
475
532
  }
476
533
  done();
477
534
  });
478
535
  });
479
536
  fastify.addHook("onError", (request, _reply, error, done) => {
480
537
  const ctx = request.__requestContext;
481
- const span = request.__span;
482
538
  if (!ctx) {
483
539
  done();
484
540
  return;
@@ -490,12 +546,15 @@ function createObservabilityPlugin(options) {
490
546
  url: request.url,
491
547
  error: error.message
492
548
  }, "Request failed");
493
- if (span) {
494
- span.setStatus({
495
- code: SpanStatusCode$1.ERROR,
496
- message: error.message
497
- });
498
- span.recordException(error);
549
+ if (tracingEnabled) {
550
+ const activeSpan = trace$1.getActiveSpan();
551
+ if (activeSpan) {
552
+ activeSpan.setStatus({
553
+ code: SpanStatusCode$1.ERROR,
554
+ message: error.message
555
+ });
556
+ activeSpan.recordException(error);
557
+ }
499
558
  }
500
559
  done();
501
560
  });
@@ -592,31 +651,34 @@ function createHttpClient(options) {
592
651
  }
593
652
  return Promise.reject(error);
594
653
  });
595
- const retryConfig = {
596
- retries: retry.retries ?? 3,
597
- retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
598
- retryCondition: (error) => {
599
- const retryStatusCodes = retry.retryStatusCodes ?? [
600
- 408,
601
- 429,
602
- 500,
603
- 502,
604
- 503,
605
- 504
606
- ];
607
- return !error.response || retryStatusCodes.includes(error.response?.status || 0);
608
- },
609
- onRetry: (retryCount, error, requestConfig) => {
610
- logger.warn({
611
- retryCount,
612
- url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
613
- error: error.message
614
- }, "Retrying request");
615
- }
616
- };
617
- axiosRetry(client, retryConfig);
618
- if (cbOptions.enabled !== false) {
619
- const breaker = new CircuitBreaker(async (config) => client.request(config), {
654
+ if (retry.enabled !== false) {
655
+ const retryConfig = {
656
+ retries: retry.retries ?? 3,
657
+ retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
658
+ retryCondition: (error) => {
659
+ const retryStatusCodes = retry.retryStatusCodes ?? [
660
+ 408,
661
+ 429,
662
+ 500,
663
+ 502,
664
+ 503,
665
+ 504
666
+ ];
667
+ return !error.response || retryStatusCodes.includes(error.response?.status || 0);
668
+ },
669
+ onRetry: (retryCount, error, requestConfig) => {
670
+ logger.warn({
671
+ retryCount,
672
+ url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
673
+ error: error.message
674
+ }, "Retrying request");
675
+ }
676
+ };
677
+ axiosRetry(client, retryConfig);
678
+ }
679
+ if (cbOptions.enabled === true) {
680
+ const originalRequest = client.request.bind(client);
681
+ const breaker = new CircuitBreaker(async (config) => originalRequest(config), {
620
682
  timeout,
621
683
  resetTimeout: cbOptions.resetTimeout ?? 3e4,
622
684
  errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
@@ -625,17 +687,58 @@ function createHttpClient(options) {
625
687
  breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
626
688
  breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
627
689
  breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
690
+ client.request = (config) => breaker.fire(config);
691
+ client.get = (url, config) => breaker.fire({
692
+ ...config,
693
+ method: "GET",
694
+ url
695
+ });
696
+ client.post = (url, data, config) => breaker.fire({
697
+ ...config,
698
+ method: "POST",
699
+ url,
700
+ data
701
+ });
702
+ client.put = (url, data, config) => breaker.fire({
703
+ ...config,
704
+ method: "PUT",
705
+ url,
706
+ data
707
+ });
708
+ client.delete = (url, config) => breaker.fire({
709
+ ...config,
710
+ method: "DELETE",
711
+ url
712
+ });
713
+ client.patch = (url, data, config) => breaker.fire({
714
+ ...config,
715
+ method: "PATCH",
716
+ url,
717
+ data
718
+ });
719
+ client.__originalRequest = originalRequest;
628
720
  }
629
721
  return client;
630
722
  }
631
723
 
632
724
  //#endregion
633
725
  //#region src/foundation.ts
726
+ const deprecationWarnings = new Set();
727
+ function warnDeprecation(oldPath, newPath, logger) {
728
+ if (deprecationWarnings.has(oldPath)) return;
729
+ deprecationWarnings.add(oldPath);
730
+ const msg = `foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`;
731
+ if (logger) logger.warn({
732
+ deprecated: oldPath,
733
+ replacement: newPath
734
+ }, msg);
735
+ else console.warn(`[neoiq-foundation] DEPRECATED: ${msg}`);
736
+ }
634
737
  /** Create a fully configured observability foundation */
635
738
  function createFoundation(input) {
636
739
  const startTime = Date.now();
637
740
  const config = parseConfig(input);
638
- const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig } = config;
741
+ const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig, redaction: redactionConfig, shutdown: shutdownConfig } = config;
639
742
  const features = {
640
743
  tracing: featuresConfig.tracing ?? true,
641
744
  metrics: featuresConfig.metrics ?? true,
@@ -651,7 +754,8 @@ function createFoundation(input) {
651
754
  environment,
652
755
  level: loggingConfig.level ?? "info",
653
756
  prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
654
- contextManager
757
+ contextManager,
758
+ additionalRedactPaths: redactionConfig.additionalPaths
655
759
  });
656
760
  setGlobalLogger(logger);
657
761
  } catch (err) {
@@ -723,17 +827,139 @@ function createFoundation(input) {
723
827
  environment,
724
828
  features
725
829
  }, "Foundation initialized");
726
- const foundation = {
727
- config,
830
+ if (shutdownConfig.flushOnCrash) {
831
+ const flushTimeoutMs = shutdownConfig.flushTimeoutMs ?? 5e3;
832
+ const crashFlush = (origin, err) => {
833
+ const error = err instanceof Error ? err : new Error(String(err));
834
+ logger.error({
835
+ error: error.message,
836
+ stack: error.stack,
837
+ origin
838
+ }, "Process crash detected, flushing telemetry");
839
+ const flushPromises = [];
840
+ if (features.tracing && isTracingEnabled()) flushPromises.push(shutdownTracing());
841
+ if (features.metrics && isMetricsEnabled()) flushPromises.push(shutdownMetrics());
842
+ const timeout = new Promise((resolve) => setTimeout(resolve, flushTimeoutMs));
843
+ Promise.race([Promise.allSettled(flushPromises), timeout]).finally(() => {
844
+ process.exit(1);
845
+ });
846
+ };
847
+ process.on("uncaughtException", (err) => crashFlush("uncaughtException", err));
848
+ process.on("unhandledRejection", (reason) => crashFlush("unhandledRejection", reason));
849
+ logger.info({ flushTimeoutMs }, "Crash-flush handlers registered");
850
+ }
851
+ const traceInSpan = async (name, fn) => {
852
+ const tracer = tracerInstance || getTracer(serviceName);
853
+ return new Promise((resolve, reject) => {
854
+ tracer.startActiveSpan(name, async (span) => {
855
+ try {
856
+ const result = await fn();
857
+ span.setStatus({ code: SpanStatusCode.OK });
858
+ span.end();
859
+ resolve(result);
860
+ } catch (err) {
861
+ const error = err;
862
+ span.setStatus({
863
+ code: SpanStatusCode.ERROR,
864
+ message: error.message
865
+ });
866
+ span.recordException(error);
867
+ span.end();
868
+ logger.error({
869
+ span: name,
870
+ error: error.message
871
+ }, "Span failed");
872
+ reject(error);
873
+ }
874
+ });
875
+ });
876
+ };
877
+ const observability = {
728
878
  logger,
729
- context: contextManager,
730
879
  tracer: tracerInstance,
731
880
  meter: meterInstance,
732
- features,
733
881
  getTracer: (name) => getTracer(name || serviceName),
734
882
  getMeter: (name, version) => getMeter(name, version),
735
883
  getTraceContext,
736
884
  getActiveSpan,
885
+ trace: traceInSpan
886
+ };
887
+ const httpModule = { createClient: (options) => createHttpClient({
888
+ ...options,
889
+ foundation
890
+ }) };
891
+ const buildHealthStatus = () => {
892
+ const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
893
+ const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
894
+ const loggingUp = !loggingError;
895
+ const allUp = tracingUp && metricsUp && loggingUp;
896
+ const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && !loggingUp;
897
+ let status = "healthy";
898
+ if (!allUp) status = allDown ? "unhealthy" : "degraded";
899
+ return {
900
+ status,
901
+ timestamp: new Date().toISOString(),
902
+ service: serviceName,
903
+ version: serviceVersion,
904
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
905
+ components: {
906
+ tracing: {
907
+ enabled: features.tracing,
908
+ status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
909
+ message: tracingError
910
+ },
911
+ metrics: {
912
+ enabled: features.metrics,
913
+ status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
914
+ message: metricsError
915
+ },
916
+ logging: {
917
+ enabled: features.logging,
918
+ status: loggingError ? "down" : "up",
919
+ message: loggingError
920
+ }
921
+ }
922
+ };
923
+ };
924
+ const shutdownFn = async () => {
925
+ logger.info({}, "Shutting down foundation...");
926
+ const promises = [];
927
+ if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
928
+ if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
929
+ await Promise.all(promises);
930
+ logger.info({}, "Foundation shutdown complete");
931
+ };
932
+ const isReadyFn = () => {
933
+ if (features.tracing && !tracingError && !isTracingEnabled()) return false;
934
+ if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
935
+ return true;
936
+ };
937
+ const safeRunFn = async (fn, fallback) => {
938
+ try {
939
+ return await fn();
940
+ } catch (err) {
941
+ const error = err;
942
+ logger.error({
943
+ error: error.message,
944
+ stack: error.stack
945
+ }, "safeRun caught error");
946
+ return fallback;
947
+ }
948
+ };
949
+ const lifecycle = {
950
+ health: buildHealthStatus,
951
+ isReady: isReadyFn,
952
+ shutdown: shutdownFn,
953
+ safeRun: safeRunFn
954
+ };
955
+ const foundation = {
956
+ config,
957
+ features,
958
+ observability,
959
+ http: httpModule,
960
+ lifecycle,
961
+ logger,
962
+ context: contextManager,
737
963
  fastifyPlugin: createObservabilityPlugin({
738
964
  serviceName,
739
965
  logger,
@@ -742,91 +968,47 @@ function createFoundation(input) {
742
968
  metricsEnabled: features.metrics && !metricsError,
743
969
  requestLogging: requestLoggingConfig
744
970
  }),
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");
971
+ tracer: tracerInstance,
972
+ meter: meterInstance,
973
+ getTracer: (name) => {
974
+ warnDeprecation("getTracer", "observability.getTracer", logger);
975
+ return observability.getTracer(name);
976
+ },
977
+ getMeter: (name, version) => {
978
+ warnDeprecation("getMeter", "observability.getMeter", logger);
979
+ return observability.getMeter(name, version);
980
+ },
981
+ getTraceContext: () => {
982
+ warnDeprecation("getTraceContext", "observability.getTraceContext", logger);
983
+ return observability.getTraceContext();
984
+ },
985
+ getActiveSpan: () => {
986
+ warnDeprecation("getActiveSpan", "observability.getActiveSpan", logger);
987
+ return observability.getActiveSpan();
988
+ },
989
+ createHttpClient: (options) => {
990
+ warnDeprecation("createHttpClient", "http.createClient", logger);
991
+ return httpModule.createClient(options);
992
+ },
993
+ shutdown: () => {
994
+ warnDeprecation("shutdown", "lifecycle.shutdown", logger);
995
+ return lifecycle.shutdown();
756
996
  },
757
997
  isReady: () => {
758
- if (features.tracing && !tracingError && !isTracingEnabled()) return false;
759
- if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
760
- return true;
998
+ warnDeprecation("isReady", "lifecycle.isReady", logger);
999
+ return lifecycle.isReady();
761
1000
  },
762
1001
  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
- };
1002
+ warnDeprecation("health", "lifecycle.health", logger);
1003
+ return lifecycle.health();
792
1004
  },
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
- });
1005
+ trace: (name, fn) => {
1006
+ warnDeprecation("trace", "observability.trace", logger);
1007
+ return observability.trace(name, fn);
818
1008
  },
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
- }
1009
+ safeRun: (fn, fallback) => {
1010
+ warnDeprecation("safeRun", "lifecycle.safeRun", logger);
1011
+ return lifecycle.safeRun(fn, fallback);
830
1012
  }
831
1013
  };
832
1014
  return foundation;
@@ -835,5 +1017,289 @@ function createFoundation(input) {
835
1017
  const setupObservability = createFoundation;
836
1018
 
837
1019
  //#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 };
1020
+ //#region src/integrations/object-store/aws-s3.ts
1021
+ function isAwsError(err) {
1022
+ return typeof err === "object" && err !== null;
1023
+ }
1024
+ async function loadAwsS3() {
1025
+ try {
1026
+ const clientMod = await import("@aws-sdk/client-s3");
1027
+ const presignerMod = await import("@aws-sdk/s3-request-presigner");
1028
+ return {
1029
+ S3Client: clientMod.S3Client,
1030
+ PutObjectCommand: clientMod.PutObjectCommand,
1031
+ GetObjectCommand: clientMod.GetObjectCommand,
1032
+ HeadObjectCommand: clientMod.HeadObjectCommand,
1033
+ DeleteObjectCommand: clientMod.DeleteObjectCommand,
1034
+ ListObjectsV2Command: clientMod.ListObjectsV2Command,
1035
+ getSignedUrl: presignerMod.getSignedUrl
1036
+ };
1037
+ } catch (err) {
1038
+ const e = err instanceof Error ? err : new Error(String(err));
1039
+ 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}`);
1040
+ }
1041
+ }
1042
+ /**
1043
+ * AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
1044
+ *
1045
+ * This implementation uses dynamic imports so `neoiq-foundation-node` can be used without AWS SDK.
1046
+ */
1047
+ var AwsS3ObjectStore = class {
1048
+ provider = "aws-s3";
1049
+ clientPromise;
1050
+ awsPromise = loadAwsS3();
1051
+ constructor(options = {}) {
1052
+ if (options.client) {
1053
+ this.clientPromise = Promise.resolve(options.client);
1054
+ return;
1055
+ }
1056
+ this.clientPromise = (async () => {
1057
+ const aws = await this.awsPromise;
1058
+ return new aws.S3Client(options.clientOptions ?? {});
1059
+ })();
1060
+ }
1061
+ async client() {
1062
+ return this.clientPromise;
1063
+ }
1064
+ async putObject(ref, body, options = {}) {
1065
+ const aws = await this.awsPromise;
1066
+ const s3 = await this.client();
1067
+ const res = await s3.send(new aws.PutObjectCommand({
1068
+ Bucket: ref.bucket,
1069
+ Key: ref.key,
1070
+ Body: body,
1071
+ ContentType: options.contentType,
1072
+ CacheControl: options.cacheControl,
1073
+ Metadata: options.metadata
1074
+ }));
1075
+ return {
1076
+ etag: res?.ETag,
1077
+ versionId: res?.VersionId
1078
+ };
1079
+ }
1080
+ async getObject(ref) {
1081
+ const aws = await this.awsPromise;
1082
+ const s3 = await this.client();
1083
+ const res = await s3.send(new aws.GetObjectCommand({
1084
+ Bucket: ref.bucket,
1085
+ Key: ref.key
1086
+ }));
1087
+ if (!res?.Body) {
1088
+ const err = new Error(`S3 GetObject returned empty body: ${ref.bucket}/${ref.key}`);
1089
+ err.code = "EMPTY_BODY";
1090
+ throw err;
1091
+ }
1092
+ return {
1093
+ body: res.Body,
1094
+ contentType: res.ContentType,
1095
+ contentLength: res.ContentLength,
1096
+ etag: res.ETag,
1097
+ versionId: res.VersionId,
1098
+ metadata: res.Metadata,
1099
+ lastModified: res.LastModified
1100
+ };
1101
+ }
1102
+ async headObject(ref) {
1103
+ const aws = await this.awsPromise;
1104
+ const s3 = await this.client();
1105
+ try {
1106
+ const res = await s3.send(new aws.HeadObjectCommand({
1107
+ Bucket: ref.bucket,
1108
+ Key: ref.key
1109
+ }));
1110
+ return {
1111
+ exists: true,
1112
+ contentType: res.ContentType,
1113
+ contentLength: res.ContentLength,
1114
+ etag: res.ETag,
1115
+ versionId: res.VersionId,
1116
+ metadata: res.Metadata,
1117
+ lastModified: res.LastModified
1118
+ };
1119
+ } catch (err) {
1120
+ if (isAwsError(err)) {
1121
+ const name = String(err.name ?? "");
1122
+ const httpStatus = err.$metadata?.httpStatusCode;
1123
+ if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
1124
+ }
1125
+ throw err;
1126
+ }
1127
+ }
1128
+ async deleteObject(ref) {
1129
+ const aws = await this.awsPromise;
1130
+ const s3 = await this.client();
1131
+ await s3.send(new aws.DeleteObjectCommand({
1132
+ Bucket: ref.bucket,
1133
+ Key: ref.key
1134
+ }));
1135
+ }
1136
+ async listObjects(options) {
1137
+ const aws = await this.awsPromise;
1138
+ const s3 = await this.client();
1139
+ const res = await s3.send(new aws.ListObjectsV2Command({
1140
+ Bucket: options.bucket,
1141
+ Prefix: options.prefix,
1142
+ ContinuationToken: options.continuationToken,
1143
+ MaxKeys: options.maxKeys
1144
+ }));
1145
+ const contents = res?.Contents ?? [];
1146
+ return {
1147
+ objects: contents.filter((o) => typeof o.Key === "string").map((o) => ({
1148
+ key: o.Key,
1149
+ size: o.Size,
1150
+ etag: o.ETag,
1151
+ lastModified: o.LastModified
1152
+ })),
1153
+ nextContinuationToken: res?.NextContinuationToken
1154
+ };
1155
+ }
1156
+ async presignGetObject(ref, options) {
1157
+ const aws = await this.awsPromise;
1158
+ const s3 = await this.client();
1159
+ return aws.getSignedUrl(s3, new aws.GetObjectCommand({
1160
+ Bucket: ref.bucket,
1161
+ Key: ref.key,
1162
+ ResponseContentType: options.responseContentType
1163
+ }), { expiresIn: options.expiresInSeconds });
1164
+ }
1165
+ async presignPutObject(ref, options) {
1166
+ const aws = await this.awsPromise;
1167
+ const s3 = await this.client();
1168
+ return aws.getSignedUrl(s3, new aws.PutObjectCommand({
1169
+ Bucket: ref.bucket,
1170
+ Key: ref.key,
1171
+ ContentType: options.contentType
1172
+ }), { expiresIn: options.expiresInSeconds });
1173
+ }
1174
+ };
1175
+
1176
+ //#endregion
1177
+ //#region src/integrations/object-store/in-memory.ts
1178
+ const DEFAULT_MAX_OBJECTS = 1e4;
1179
+ const STREAM_TIMEOUT_MS = 3e4;
1180
+ function toBuffer(body) {
1181
+ if (typeof body === "string") return Buffer.from(body);
1182
+ if (Buffer.isBuffer(body)) return body;
1183
+ if (body instanceof Uint8Array) return Buffer.from(body);
1184
+ return new Promise((resolve, reject) => {
1185
+ const chunks = [];
1186
+ const timer = setTimeout(() => {
1187
+ body.destroy(new Error("InMemoryObjectStore: stream read timed out"));
1188
+ reject(new Error("InMemoryObjectStore: stream read timed out"));
1189
+ }, STREAM_TIMEOUT_MS);
1190
+ body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
1191
+ body.on("end", () => {
1192
+ clearTimeout(timer);
1193
+ resolve(Buffer.concat(chunks));
1194
+ });
1195
+ body.on("error", (err) => {
1196
+ clearTimeout(timer);
1197
+ reject(err);
1198
+ });
1199
+ });
1200
+ }
1201
+ function computeEtag(buf) {
1202
+ return `"${createHash("md5").update(buf).digest("hex")}"`;
1203
+ }
1204
+ /**
1205
+ * InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
1206
+ * NOTE: presign* methods are not meaningful here and will throw.
1207
+ */
1208
+ var InMemoryObjectStore = class {
1209
+ provider = "in-memory";
1210
+ buckets = new Map();
1211
+ maxObjects;
1212
+ objectCount = 0;
1213
+ constructor(options = {}) {
1214
+ this.maxObjects = options.maxObjects ?? DEFAULT_MAX_OBJECTS;
1215
+ }
1216
+ bucketMap(bucket) {
1217
+ let m = this.buckets.get(bucket);
1218
+ if (!m) {
1219
+ m = new Map();
1220
+ this.buckets.set(bucket, m);
1221
+ }
1222
+ return m;
1223
+ }
1224
+ async putObject(ref, body, options = {}) {
1225
+ const map = this.bucketMap(ref.bucket);
1226
+ const isNew = !map.has(ref.key);
1227
+ if (isNew && this.objectCount >= this.maxObjects) throw new Error(`InMemoryObjectStore: max object limit reached (${this.maxObjects})`);
1228
+ const buf = await toBuffer(body);
1229
+ const obj = {
1230
+ body: buf,
1231
+ contentType: options.contentType,
1232
+ cacheControl: options.cacheControl,
1233
+ metadata: options.metadata,
1234
+ etag: computeEtag(buf),
1235
+ lastModified: new Date()
1236
+ };
1237
+ map.set(ref.key, obj);
1238
+ if (isNew) this.objectCount++;
1239
+ return { etag: obj.etag };
1240
+ }
1241
+ async getObject(ref) {
1242
+ const obj = this.bucketMap(ref.bucket).get(ref.key);
1243
+ if (!obj) {
1244
+ const err = new Error(`Object not found: ${ref.bucket}/${ref.key}`);
1245
+ err.code = "OBJECT_NOT_FOUND";
1246
+ throw err;
1247
+ }
1248
+ return {
1249
+ body: Readable.from(obj.body),
1250
+ contentType: obj.contentType,
1251
+ contentLength: obj.body.length,
1252
+ etag: obj.etag,
1253
+ metadata: obj.metadata,
1254
+ lastModified: obj.lastModified
1255
+ };
1256
+ }
1257
+ async headObject(ref) {
1258
+ const obj = this.bucketMap(ref.bucket).get(ref.key);
1259
+ if (!obj) return { exists: false };
1260
+ return {
1261
+ exists: true,
1262
+ contentType: obj.contentType,
1263
+ contentLength: obj.body.length,
1264
+ etag: obj.etag,
1265
+ metadata: obj.metadata,
1266
+ lastModified: obj.lastModified
1267
+ };
1268
+ }
1269
+ async deleteObject(ref) {
1270
+ const map = this.bucketMap(ref.bucket);
1271
+ if (map.delete(ref.key)) this.objectCount--;
1272
+ }
1273
+ async listObjects(options) {
1274
+ const { bucket, prefix = "", continuationToken, maxKeys = 1e3 } = options;
1275
+ const map = this.bucketMap(bucket);
1276
+ const all = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
1277
+ key,
1278
+ size: obj.body.length,
1279
+ etag: obj.etag,
1280
+ lastModified: obj.lastModified
1281
+ })).sort((a, b) => a.key.localeCompare(b.key));
1282
+ let startIndex = 0;
1283
+ if (continuationToken) {
1284
+ const idx = all.findIndex((o) => o.key === continuationToken);
1285
+ if (idx === -1) throw new Error(`InMemoryObjectStore: invalid continuationToken "${continuationToken}"`);
1286
+ startIndex = idx + 1;
1287
+ }
1288
+ const page = all.slice(startIndex, startIndex + maxKeys);
1289
+ const hasMore = startIndex + maxKeys < all.length;
1290
+ return {
1291
+ objects: page,
1292
+ nextContinuationToken: hasMore ? page[page.length - 1]?.key : void 0
1293
+ };
1294
+ }
1295
+ async presignGetObject(_ref, _options) {
1296
+ throw new Error("InMemoryObjectStore does not support presigned URLs");
1297
+ }
1298
+ async presignPutObject(_ref, _options) {
1299
+ throw new Error("InMemoryObjectStore does not support presigned URLs");
1300
+ }
1301
+ };
1302
+
1303
+ //#endregion
1304
+ 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
1305
  //# sourceMappingURL=index.mjs.map