pinqloq 1.1.0

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.cjs ADDED
@@ -0,0 +1,716 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ CORRELATION_ID_HEADER_NAME: () => CORRELATION_ID_HEADER_NAME,
24
+ DEVICE_IDENTIFIER_HEADER_NAME: () => DEVICE_IDENTIFIER_HEADER_NAME,
25
+ PinqloqLogFailureReason: () => PinqloqLogFailureReason,
26
+ PinqloqLogLevel: () => PinqloqLogLevel,
27
+ PinqloqLogSourceType: () => PinqloqLogSourceType,
28
+ PinqloqRedactionPlan: () => PinqloqRedactionPlan,
29
+ createPinqloq: () => createPinqloq
30
+ });
31
+ module.exports = __toCommonJS(index_exports);
32
+
33
+ // src/buffering/callback.ts
34
+ function raiseSent(onSent, entry) {
35
+ if (!onSent) return;
36
+ try {
37
+ onSent(entry);
38
+ } catch (error) {
39
+ console.warn("Pinqloq: the onSent callback threw an exception; swallowed.", error);
40
+ }
41
+ }
42
+ function raiseFailed(onFailed, entry, error) {
43
+ if (!onFailed) return;
44
+ try {
45
+ onFailed(entry, error);
46
+ } catch (callbackError) {
47
+ console.warn("Pinqloq: the onFailed callback threw an exception; swallowed.", callbackError);
48
+ }
49
+ }
50
+
51
+ // src/logging/types.ts
52
+ var PinqloqLogLevel = /* @__PURE__ */ ((PinqloqLogLevel2) => {
53
+ PinqloqLogLevel2[PinqloqLogLevel2["Debug"] = 1] = "Debug";
54
+ PinqloqLogLevel2[PinqloqLogLevel2["Information"] = 2] = "Information";
55
+ PinqloqLogLevel2[PinqloqLogLevel2["Warning"] = 3] = "Warning";
56
+ PinqloqLogLevel2[PinqloqLogLevel2["Error"] = 4] = "Error";
57
+ PinqloqLogLevel2[PinqloqLogLevel2["Fatal"] = 5] = "Fatal";
58
+ return PinqloqLogLevel2;
59
+ })(PinqloqLogLevel || {});
60
+ var PinqloqLogSourceType = /* @__PURE__ */ ((PinqloqLogSourceType2) => {
61
+ PinqloqLogSourceType2[PinqloqLogSourceType2["Device"] = 1] = "Device";
62
+ PinqloqLogSourceType2[PinqloqLogSourceType2["Backend"] = 2] = "Backend";
63
+ return PinqloqLogSourceType2;
64
+ })(PinqloqLogSourceType || {});
65
+ var PinqloqLogFailureReason = /* @__PURE__ */ ((PinqloqLogFailureReason2) => {
66
+ PinqloqLogFailureReason2["Unauthorized"] = "Unauthorized";
67
+ PinqloqLogFailureReason2["Forbidden"] = "Forbidden";
68
+ PinqloqLogFailureReason2["MissingCollection"] = "MissingCollection";
69
+ PinqloqLogFailureReason2["QueueFull"] = "QueueFull";
70
+ PinqloqLogFailureReason2["HttpError"] = "HttpError";
71
+ PinqloqLogFailureReason2["Timeout"] = "Timeout";
72
+ PinqloqLogFailureReason2["Network"] = "Network";
73
+ PinqloqLogFailureReason2["Unknown"] = "Unknown";
74
+ return PinqloqLogFailureReason2;
75
+ })(PinqloqLogFailureReason || {});
76
+
77
+ // src/buffering/buffer.ts
78
+ var PinqloqLogBuffer = class {
79
+ constructor(capacity) {
80
+ this.capacity = capacity;
81
+ }
82
+ capacity;
83
+ queue = [];
84
+ droppedCount = 0;
85
+ completed = false;
86
+ get size() {
87
+ return this.queue.length;
88
+ }
89
+ enqueue(entry, onSent, onFailed) {
90
+ if (this.completed) throw new Error("Pinqloq: the log queue is closed.");
91
+ if (!entry) return false;
92
+ entry.date ??= /* @__PURE__ */ new Date();
93
+ if (this.queue.length >= this.capacity) {
94
+ this.droppedCount++;
95
+ if (this.droppedCount === 1 || this.droppedCount % 1e3 === 0) {
96
+ console.warn(`Pinqloq: log queue is full; ${this.droppedCount} logs dropped so far.`);
97
+ }
98
+ raiseFailed(onFailed, entry, {
99
+ reason: "QueueFull" /* QueueFull */,
100
+ message: "Log queue is full; the log was dropped. Increase queueCapacity or lower flushIntervalMs."
101
+ });
102
+ return false;
103
+ }
104
+ this.queue.push({ entry, onSent, onFailed });
105
+ return true;
106
+ }
107
+ enqueueMany(entries, onSent, onFailed) {
108
+ if (!entries) return 0;
109
+ let queuedCount = 0;
110
+ for (const entry of entries) {
111
+ if (this.enqueue(entry, onSent, onFailed)) queuedCount++;
112
+ }
113
+ return queuedCount;
114
+ }
115
+ complete() {
116
+ this.completed = true;
117
+ }
118
+ drain(max) {
119
+ return this.queue.splice(0, max);
120
+ }
121
+ };
122
+
123
+ // src/buffering/dispatcher.ts
124
+ var PinqloqLogDispatcher = class {
125
+ constructor(buffer, apiClient, options) {
126
+ this.buffer = buffer;
127
+ this.apiClient = apiClient;
128
+ this.options = options;
129
+ }
130
+ buffer;
131
+ apiClient;
132
+ options;
133
+ timer;
134
+ pendingSend;
135
+ stopping = false;
136
+ start() {
137
+ this.notifyEnqueued();
138
+ }
139
+ notifyEnqueued() {
140
+ if (this.stopping || this.pendingSend || this.buffer.size === 0) return;
141
+ if (this.buffer.size >= this.options.batchSize) {
142
+ void this.flush();
143
+ return;
144
+ }
145
+ if (this.timer) return;
146
+ this.timer = setTimeout(() => void this.flush(), this.options.flushIntervalMs);
147
+ this.timer.unref?.();
148
+ }
149
+ flush() {
150
+ if (this.pendingSend) return this.pendingSend;
151
+ this.clearTimer();
152
+ if (this.buffer.size === 0) return Promise.resolve();
153
+ this.pendingSend = this.sendBatches().finally(() => {
154
+ this.pendingSend = void 0;
155
+ this.notifyEnqueued();
156
+ });
157
+ return this.pendingSend;
158
+ }
159
+ async sendBatches() {
160
+ do {
161
+ const batch = this.buffer.drain(this.options.batchSize);
162
+ try {
163
+ await this.apiClient.sendBatch(batch);
164
+ } catch (error) {
165
+ console.warn(`Pinqloq: batch of ${batch.length} logs could not be sent; dropped.`, error);
166
+ }
167
+ } while (this.buffer.size > 0 && (this.stopping || this.buffer.size >= this.options.batchSize));
168
+ }
169
+ clearTimer() {
170
+ if (this.timer) clearTimeout(this.timer);
171
+ this.timer = void 0;
172
+ }
173
+ async shutdown() {
174
+ this.stopping = true;
175
+ this.buffer.complete();
176
+ this.clearTimer();
177
+ await this.flush();
178
+ }
179
+ };
180
+
181
+ // src/express/middleware.ts
182
+ var import_node_crypto = require("crypto");
183
+
184
+ // src/redaction/plan.ts
185
+ var ALWAYS_REDACTED_NAMES = new Set(
186
+ [
187
+ "authorization",
188
+ "proxy-authorization",
189
+ "cookie",
190
+ "set-cookie",
191
+ "x-api-key",
192
+ "x-secret-key",
193
+ "x-auth-token",
194
+ "x-access-token",
195
+ "x-csrf-token",
196
+ "x-xsrf-token",
197
+ "secret_key",
198
+ "password",
199
+ "newpassword",
200
+ "oldpassword",
201
+ "currentpassword",
202
+ "passwordconfirmation",
203
+ "confirmpassword",
204
+ "secret",
205
+ "secretkey",
206
+ "clientsecret",
207
+ "apikey",
208
+ "accesstoken",
209
+ "refreshtoken",
210
+ "idtoken",
211
+ "token",
212
+ "otp",
213
+ "otpcode",
214
+ "verificationcode",
215
+ "pin",
216
+ "privatekey",
217
+ "cardnumber",
218
+ "cvv",
219
+ "cvc",
220
+ "securitycode",
221
+ "iban",
222
+ "ssn"
223
+ ].map((name) => name.toLowerCase())
224
+ );
225
+ function isLetterOrDigit(char) {
226
+ return char !== void 0 && /[A-Za-z0-9]/.test(char);
227
+ }
228
+ function isUpper(char) {
229
+ return char !== void 0 && char !== char.toLowerCase() && char === char.toUpperCase();
230
+ }
231
+ function isBoundedName(body, index, length) {
232
+ const endIndex = index + length;
233
+ const isStartBounded = index === 0 || !isLetterOrDigit(body[index - 1]) || isUpper(body[index]) && !isUpper(body[index - 1]);
234
+ const isEndBounded = endIndex === body.length || !isLetterOrDigit(body[endIndex]) || isUpper(body[endIndex]);
235
+ return isStartBounded && isEndBounded;
236
+ }
237
+ function containsWholeName(body, names) {
238
+ const lowerBody = body.toLowerCase();
239
+ for (const name of names) {
240
+ if (name.length === 0) continue;
241
+ let searchFrom = 0;
242
+ while (searchFrom <= lowerBody.length - name.length) {
243
+ const index = lowerBody.indexOf(name, searchFrom);
244
+ if (index < 0) break;
245
+ if (isBoundedName(body, index, name.length)) return true;
246
+ searchFrom = index + 1;
247
+ }
248
+ }
249
+ return false;
250
+ }
251
+ var PinqloqRedactionPlan = class _PinqloqRedactionPlan {
252
+ constructor(redactAll, declaredNames) {
253
+ this.redactAll = redactAll;
254
+ this.declaredNames = new Set([...declaredNames].map((name) => name.toLowerCase()));
255
+ }
256
+ redactAll;
257
+ static NONE = new _PinqloqRedactionPlan(false, []);
258
+ static ALL = new _PinqloqRedactionPlan(true, []);
259
+ declaredNames;
260
+ get hasDeclaredRedactions() {
261
+ return this.redactAll || this.declaredNames.size > 0;
262
+ }
263
+ shouldRedact(propertyOrHeaderName) {
264
+ const lower = propertyOrHeaderName.toLowerCase();
265
+ return this.redactAll || ALWAYS_REDACTED_NAMES.has(lower) || this.declaredNames.has(lower);
266
+ }
267
+ containsDeclaredName(body) {
268
+ return containsWholeName(body, this.declaredNames);
269
+ }
270
+ static containsAlwaysRedactedName(body) {
271
+ return containsWholeName(body, ALWAYS_REDACTED_NAMES);
272
+ }
273
+ };
274
+
275
+ // src/internal/throttledWarn.ts
276
+ var nextAllowedAt = /* @__PURE__ */ new Map();
277
+ function warnThrottled(key, intervalMs, message, ...args) {
278
+ const now = Date.now();
279
+ const next = nextAllowedAt.get(key) ?? 0;
280
+ if (now < next) return;
281
+ nextAllowedAt.set(key, now + intervalMs);
282
+ console.warn(message, ...args);
283
+ }
284
+
285
+ // src/redaction/redaction.ts
286
+ var REDACTED_VALUE = "*****REDACTED*****";
287
+ var MALFORMED_BODY_WARNING_THROTTLE_MS = 6e4;
288
+ var UNPARSEABLE_SENSITIVE_BODY_VALUE = "*****REDACTED: body carries a credential field and is not parseable JSON (non-JSON content type, or longer than the capture limit)*****";
289
+ function redactProperties(value, plan) {
290
+ if (Array.isArray(value)) return value.map((item) => redactProperties(item, plan));
291
+ if (value !== null && typeof value === "object") {
292
+ const result = {};
293
+ for (const [key, item] of Object.entries(value)) {
294
+ result[key] = plan.shouldRedact(key) ? REDACTED_VALUE : redactProperties(item, plan);
295
+ }
296
+ return result;
297
+ }
298
+ return value;
299
+ }
300
+ function redactFully(value) {
301
+ if (Array.isArray(value)) return value.map(redactFully);
302
+ if (value !== null && typeof value === "object") {
303
+ const result = {};
304
+ for (const [key, item] of Object.entries(value)) result[key] = redactFully(item);
305
+ return result;
306
+ }
307
+ return REDACTED_VALUE;
308
+ }
309
+ function redactJsonProperties(body, plan, isSensitive) {
310
+ if (!body.trim()) return body;
311
+ try {
312
+ return JSON.stringify(redactProperties(JSON.parse(body), plan));
313
+ } catch (error) {
314
+ warnThrottled(
315
+ "redactJsonProperties",
316
+ MALFORMED_BODY_WARNING_THROTTLE_MS,
317
+ "Pinqloq: a captured body could not be parsed as JSON; falling back to whole-body handling.",
318
+ error
319
+ );
320
+ return isSensitive ? UNPARSEABLE_SENSITIVE_BODY_VALUE : body;
321
+ }
322
+ }
323
+ function redactJsonFully(body) {
324
+ if (!body.trim()) return body;
325
+ try {
326
+ return JSON.stringify(redactFully(JSON.parse(body)));
327
+ } catch (error) {
328
+ warnThrottled(
329
+ "redactJsonFully",
330
+ MALFORMED_BODY_WARNING_THROTTLE_MS,
331
+ "Pinqloq: a captured body under a redactAll plan could not be parsed as JSON; masking it wholesale.",
332
+ error
333
+ );
334
+ return REDACTED_VALUE;
335
+ }
336
+ }
337
+ function applyBodyRedaction(body, plan) {
338
+ if (plan.redactAll) return redactJsonFully(body);
339
+ const mentionsCredential = PinqloqRedactionPlan.containsAlwaysRedactedName(body) || plan.containsDeclaredName(body);
340
+ if (!plan.hasDeclaredRedactions && !mentionsCredential) return body;
341
+ return redactJsonProperties(body, plan, mentionsCredential);
342
+ }
343
+ function serializeHeaders(headers, plan) {
344
+ const result = {};
345
+ for (const [key, value] of Object.entries(headers)) {
346
+ if (value === void 0) continue;
347
+ const stringValue = Array.isArray(value) ? value.join(", ") : String(value);
348
+ result[key] = plan.shouldRedact(key) ? REDACTED_VALUE : stringValue;
349
+ }
350
+ return JSON.stringify(result);
351
+ }
352
+
353
+ // src/express/pathMatch.ts
354
+ function matchesAnyPathPrefix(path, prefixes) {
355
+ if (!prefixes || prefixes.length === 0) return false;
356
+ const lowerPath = path.toLowerCase();
357
+ return prefixes.some((prefix) => matchesSegmentPrefix(lowerPath, prefix.toLowerCase()));
358
+ }
359
+ function matchesSegmentPrefix(lowerPath, prefix) {
360
+ const normalized = prefix.startsWith("/") ? prefix : `/${prefix}`;
361
+ const trimmed = normalized.length > 1 && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
362
+ if (!lowerPath.startsWith(trimmed)) return false;
363
+ return lowerPath.length === trimmed.length || lowerPath[trimmed.length] === "/";
364
+ }
365
+
366
+ // src/express/captureResponseBody.ts
367
+ function captureResponseBody(res, maxBytes) {
368
+ const originalWrite = res.write.bind(res);
369
+ const originalEnd = res.end.bind(res);
370
+ const chunks = [];
371
+ let capturedBytes = 0;
372
+ function capture(chunk) {
373
+ if (chunk === void 0 || chunk === null || capturedBytes >= maxBytes) return;
374
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
375
+ const remaining = maxBytes - capturedBytes;
376
+ const slice = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
377
+ chunks.push(slice);
378
+ capturedBytes += slice.length;
379
+ }
380
+ res.write = function(chunk, ...args) {
381
+ capture(chunk);
382
+ return originalWrite(chunk, ...args);
383
+ };
384
+ res.end = function(chunk, ...args) {
385
+ if (chunk !== void 0 && typeof chunk !== "function") capture(chunk);
386
+ return originalEnd(chunk, ...args);
387
+ };
388
+ return {
389
+ getBody: () => Buffer.concat(chunks).toString("utf8"),
390
+ restore: () => {
391
+ res.write = originalWrite;
392
+ res.end = originalEnd;
393
+ }
394
+ };
395
+ }
396
+
397
+ // src/express/middleware.ts
398
+ var MAX_BODY_CHARACTERS = 32 * 1024;
399
+ var MAX_BODY_BYTES = 4 * MAX_BODY_CHARACTERS;
400
+ var SELECTOR_WARNING_THROTTLE_MS = 6e4;
401
+ var DEVICE_IDENTIFIER_HEADER_NAME = "device-identifier";
402
+ var CORRELATION_ID_HEADER_NAME = "correlation-id";
403
+ var DEVICE_IDENTIFIER_REQUIRED_MESSAGE = "Pinqloq: the required deviceIdentifier could not be resolved. Send the 'device-identifier' request header, or configure resolveDeviceIdentifier, or set PinqloqOptions.deviceIdentifier.";
404
+ var SERVER_ERROR_STATUS_THRESHOLD = 500;
405
+ var CLIENT_ERROR_STATUS_THRESHOLD = 400;
406
+ function resolveLogLevel(statusCode) {
407
+ if (statusCode >= SERVER_ERROR_STATUS_THRESHOLD) return 4 /* Error */;
408
+ if (statusCode >= CLIENT_ERROR_STATUS_THRESHOLD) return 3 /* Warning */;
409
+ return 2 /* Information */;
410
+ }
411
+ function truncate(value) {
412
+ return value.length <= MAX_BODY_CHARACTERS ? value : value.slice(0, MAX_BODY_CHARACTERS);
413
+ }
414
+ function resolveSelector(selector, req, throttleKey) {
415
+ if (!selector) return "";
416
+ try {
417
+ return selector(req) ?? "";
418
+ } catch (error) {
419
+ warnThrottled(throttleKey, SELECTOR_WARNING_THROTTLE_MS, `Pinqloq: ${throttleKey} threw an exception; ignored.`, error);
420
+ return "";
421
+ }
422
+ }
423
+ function resolveDeviceIdentifier(req, requestOptions, globalOptions) {
424
+ const overridden = resolveSelector(requestOptions.resolveDeviceIdentifier, req, "resolveDeviceIdentifier");
425
+ if (overridden.trim()) return overridden;
426
+ const header = req.headers[DEVICE_IDENTIFIER_HEADER_NAME];
427
+ const headerValue = Array.isArray(header) ? header[0] : header;
428
+ if (headerValue?.trim()) return headerValue;
429
+ return globalOptions.deviceIdentifier ?? "";
430
+ }
431
+ function resolveCorrelationId(req) {
432
+ const header = req.headers[CORRELATION_ID_HEADER_NAME];
433
+ const headerValue = Array.isArray(header) ? header[0] : header;
434
+ return headerValue?.trim() ? headerValue : (0, import_node_crypto.randomUUID)();
435
+ }
436
+ function applyEnrichers(target, enrichers, req, res) {
437
+ if (!enrichers) return;
438
+ for (const [key, selector] of Object.entries(enrichers)) {
439
+ let value;
440
+ try {
441
+ value = selector(req, res);
442
+ } catch (error) {
443
+ warnThrottled(`enricher:${key}`, SELECTOR_WARNING_THROTTLE_MS, `Pinqloq: the '${key}' enricher threw an exception; ignored.`, error);
444
+ continue;
445
+ }
446
+ if (value) target[key] = value;
447
+ }
448
+ }
449
+ function createPinqloqRequestLogging(logger, globalOptions, requestOptions = {}) {
450
+ return function pinqloqRequestLoggingMiddleware(req, res, next) {
451
+ if (matchesAnyPathPrefix(req.path, requestOptions.excludePaths)) {
452
+ next();
453
+ return;
454
+ }
455
+ const deviceIdentifier = resolveDeviceIdentifier(req, requestOptions, globalOptions);
456
+ if (!deviceIdentifier.trim()) {
457
+ res.status(400).send(DEVICE_IDENTIFIER_REQUIRED_MESSAGE);
458
+ return;
459
+ }
460
+ const startedAt = process.hrtime.bigint();
461
+ const redactPlan = requestOptions.redactPaths?.length && matchesAnyPathPrefix(req.path, requestOptions.redactPaths) ? PinqloqRedactionPlan.ALL : new PinqloqRedactionPlan(false, requestOptions.redactFields ?? []);
462
+ const requestHeaders = truncate(serializeHeaders(req.headers, redactPlan));
463
+ const inputJson = truncate(
464
+ applyBodyRedaction(req.body !== void 0 ? JSON.stringify(req.body) : "", redactPlan)
465
+ );
466
+ const correlationId = resolveCorrelationId(req);
467
+ const appVersionName = resolveSelector(requestOptions.resolveAppVersionName, req, "resolveAppVersionName");
468
+ const capture = captureResponseBody(res, MAX_BODY_BYTES);
469
+ res.once("finish", () => {
470
+ capture.restore();
471
+ const elapsedMs = Number(process.hrtime.bigint() - startedAt) / 1e6;
472
+ const statusCode = res.statusCode;
473
+ const method = req.method;
474
+ const path = req.path;
475
+ const outputJson = truncate(applyBodyRedaction(capture.getBody(), redactPlan));
476
+ const responseHeaders = truncate(serializeHeaders(res.getHeaders(), redactPlan));
477
+ const metadata = {};
478
+ metadata.event = `${method} ${path}`.trim();
479
+ applyEnrichers(metadata, requestOptions.metadata, req, res);
480
+ const resolvedEventName = metadata.event;
481
+ delete metadata.event;
482
+ metadata.method = method;
483
+ metadata.statusCode = String(statusCode);
484
+ metadata.durationMs = String(Math.round(elapsedMs));
485
+ metadata.RequestMethod = method;
486
+ metadata.ResponseCode = String(statusCode);
487
+ const detail = {};
488
+ applyEnrichers(detail, requestOptions.detail, req, res);
489
+ detail.InputJson = inputJson;
490
+ detail.OutputJson = outputJson;
491
+ detail.RequestHeaders = requestHeaders;
492
+ detail.ResponseHeaders = responseHeaders;
493
+ const logLevel = resolveLogLevel(statusCode);
494
+ const resolvedAppVersionName = appVersionName || void 0;
495
+ logger.enqueue({
496
+ logLevel,
497
+ event: resolvedEventName,
498
+ deviceIdentifier,
499
+ appVersionName: resolvedAppVersionName,
500
+ logSourceType: 2 /* Backend */,
501
+ correlationId,
502
+ path,
503
+ metadata,
504
+ detail
505
+ });
506
+ });
507
+ next();
508
+ };
509
+ }
510
+
511
+ // src/options.ts
512
+ var INGEST_BASE_ADDRESS = "https://pinqloq-external-api.pinqponq.io";
513
+ var DEFAULTS = {
514
+ bulkPath: "api/client-logs/bulk",
515
+ batchSize: 200,
516
+ flushIntervalMs: 2e3,
517
+ queueCapacity: 1e4,
518
+ httpTimeoutMs: 1e4
519
+ };
520
+ function resolveOptions(options) {
521
+ if (!options.secretKey) {
522
+ throw new Error("Pinqloq: secretKey is required.");
523
+ }
524
+ return {
525
+ secretKey: options.secretKey,
526
+ apiLogsCollectionName: options.apiLogsCollectionName,
527
+ bulkPath: options.bulkPath ?? DEFAULTS.bulkPath,
528
+ batchSize: Math.max(1, options.batchSize ?? DEFAULTS.batchSize),
529
+ flushIntervalMs: Math.max(1, options.flushIntervalMs ?? DEFAULTS.flushIntervalMs),
530
+ queueCapacity: Math.max(1, options.queueCapacity ?? DEFAULTS.queueCapacity),
531
+ httpTimeoutMs: Math.max(1, options.httpTimeoutMs ?? DEFAULTS.httpTimeoutMs),
532
+ appVersionName: options.appVersionName,
533
+ deviceIdentifier: options.deviceIdentifier
534
+ };
535
+ }
536
+
537
+ // src/http/ingestClient.ts
538
+ var SECRET_KEY_HEADER = "X-Secret-Key";
539
+ var HTTP_WARNING_THROTTLE_MS = 6e4;
540
+ var MAX_ERROR_BODY_CHARACTERS = 512;
541
+ var PinqloqIngestApiClient = class {
542
+ constructor(options) {
543
+ this.options = options;
544
+ }
545
+ options;
546
+ nextHttpWarningAt = 0;
547
+ async sendBatch(items) {
548
+ if (items.length === 0) return;
549
+ const groups = /* @__PURE__ */ new Map();
550
+ for (const item of items) {
551
+ const collectionName = this.resolveCollectionName(item.entry) ?? "";
552
+ const group = groups.get(collectionName);
553
+ if (group) group.push(item);
554
+ else groups.set(collectionName, [item]);
555
+ }
556
+ for (const [collectionName, groupItems] of groups) {
557
+ await this.sendGroup(collectionName || void 0, groupItems);
558
+ }
559
+ }
560
+ async sendGroup(collectionName, groupItems) {
561
+ const url = `${INGEST_BASE_ADDRESS}/${this.options.bulkPath.replace(/^\/+/, "")}`;
562
+ const payload = {
563
+ collectionName,
564
+ logs: groupItems.map((item) => this.toWireItem(item.entry))
565
+ };
566
+ const controller = new AbortController();
567
+ const timeout = setTimeout(() => controller.abort(), this.options.httpTimeoutMs);
568
+ try {
569
+ const response = await fetch(url, {
570
+ method: "POST",
571
+ headers: {
572
+ "Content-Type": "application/json",
573
+ [SECRET_KEY_HEADER]: this.options.secretKey
574
+ },
575
+ body: JSON.stringify(payload),
576
+ signal: controller.signal
577
+ });
578
+ if (response.ok) {
579
+ for (const item of groupItems) raiseSent(item.onSent, item.entry);
580
+ return;
581
+ }
582
+ const errorBody = await this.readErrorBody(response);
583
+ const error = this.buildHttpError(response.status, collectionName, errorBody);
584
+ for (const item of groupItems) raiseFailed(item.onFailed, item.entry, error);
585
+ } catch (exception) {
586
+ const aborted = controller.signal.aborted;
587
+ const error = this.buildExceptionError(exception, aborted);
588
+ console.warn(
589
+ `Pinqloq: group of ${groupItems.length} logs could not be sent (${this.formatCollectionName(collectionName)}).`,
590
+ exception
591
+ );
592
+ for (const item of groupItems) raiseFailed(item.onFailed, item.entry, error);
593
+ } finally {
594
+ clearTimeout(timeout);
595
+ }
596
+ }
597
+ buildHttpError(statusCode, collectionName, errorBody) {
598
+ const reason = statusCode === 401 ? "Unauthorized" /* Unauthorized */ : statusCode === 403 ? "Forbidden" /* Forbidden */ : "HttpError" /* HttpError */;
599
+ let message;
600
+ if (reason === "Unauthorized" /* Unauthorized */) {
601
+ message = "Unauthorized (HTTP 401): the secret key is invalid or missing.";
602
+ } else if (reason === "Forbidden" /* Forbidden */) {
603
+ message = `Forbidden (HTTP 403): the secret key is not authorized for the '${this.formatCollectionName(collectionName)}' collection.`;
604
+ } else if (statusCode === 400) {
605
+ message = `The server rejected the request (HTTP 400, '${this.formatCollectionName(collectionName)}'): check the collectionName (required for keys allowed on multiple collections) or event fields.`;
606
+ } else {
607
+ message = `The server returned an error (HTTP ${statusCode}).`;
608
+ }
609
+ if (errorBody) message += ` Server response: ${errorBody}`;
610
+ if (this.shouldLogHttpFailure()) {
611
+ console.error(
612
+ `Pinqloq: batch send rejected (HTTP ${statusCode}, collection '${this.formatCollectionName(collectionName)}'); logs in this group were dropped. ${message}`
613
+ );
614
+ }
615
+ return { reason, statusCode, message };
616
+ }
617
+ buildExceptionError(exception, aborted) {
618
+ const isTimeout = aborted || exception instanceof DOMException && exception.name === "AbortError";
619
+ return isTimeout ? { reason: "Timeout" /* Timeout */, message: "The request timed out.", cause: exception } : {
620
+ reason: "Network" /* Network */,
621
+ message: `Network error: ${exception instanceof Error ? exception.message : String(exception)}`,
622
+ cause: exception
623
+ };
624
+ }
625
+ async readErrorBody(response) {
626
+ try {
627
+ const body = (await response.text()).trim();
628
+ return body.length <= MAX_ERROR_BODY_CHARACTERS ? body : body.slice(0, MAX_ERROR_BODY_CHARACTERS);
629
+ } catch (error) {
630
+ console.warn("Pinqloq: could not read the error response body; continuing without it.", error);
631
+ return "";
632
+ }
633
+ }
634
+ resolveCollectionName(entry) {
635
+ return entry.collectionName?.trim() || this.options.apiLogsCollectionName;
636
+ }
637
+ shouldLogHttpFailure() {
638
+ const now = Date.now();
639
+ if (now < this.nextHttpWarningAt) return false;
640
+ this.nextHttpWarningAt = now + HTTP_WARNING_THROTTLE_MS;
641
+ return true;
642
+ }
643
+ formatCollectionName(collectionName) {
644
+ return collectionName?.trim() ? collectionName : "(not resolved server-side)";
645
+ }
646
+ toWireItem(entry) {
647
+ return {
648
+ logLevel: entry.logLevel ?? 2,
649
+ event: entry.event,
650
+ date: entry.date?.toISOString(),
651
+ appVersionName: entry.appVersionName?.trim() ? entry.appVersionName : this.options.appVersionName,
652
+ deviceIdentifier: entry.deviceIdentifier?.trim() ? entry.deviceIdentifier : this.options.deviceIdentifier ?? "",
653
+ logSourceType: PinqloqLogSourceType[entry.logSourceType ?? 2 /* Backend */],
654
+ correlationId: entry.correlationId,
655
+ path: entry.path,
656
+ metadata: entry.metadata,
657
+ detail: entry.detail
658
+ };
659
+ }
660
+ };
661
+
662
+ // src/logging/logger.ts
663
+ var DEVICE_IDENTIFIER_REQUIRED_MESSAGE2 = "Pinqloq: deviceIdentifier is required. Set it on the entry, or configure the global PinqloqOptions.deviceIdentifier fallback.";
664
+ var DefaultPinqloqLogger = class {
665
+ constructor(buffer, dispatcher, options) {
666
+ this.buffer = buffer;
667
+ this.dispatcher = dispatcher;
668
+ this.options = options;
669
+ }
670
+ buffer;
671
+ dispatcher;
672
+ options;
673
+ enqueue(entry, onSent, onFailed) {
674
+ this.ensureDeviceIdentifier(entry);
675
+ const written = this.buffer.enqueue(entry, onSent, onFailed);
676
+ this.dispatcher.notifyEnqueued();
677
+ return written;
678
+ }
679
+ enqueueMany(entries, onSent, onFailed) {
680
+ for (const entry of entries) this.ensureDeviceIdentifier(entry);
681
+ const written = this.buffer.enqueueMany(entries, onSent, onFailed);
682
+ this.dispatcher.notifyEnqueued();
683
+ return written;
684
+ }
685
+ ensureDeviceIdentifier(entry) {
686
+ if (!entry.deviceIdentifier?.trim() && !this.options.deviceIdentifier?.trim()) {
687
+ throw new Error(DEVICE_IDENTIFIER_REQUIRED_MESSAGE2);
688
+ }
689
+ }
690
+ };
691
+
692
+ // src/client.ts
693
+ function createPinqloq(options) {
694
+ const resolved = resolveOptions(options);
695
+ const buffer = new PinqloqLogBuffer(resolved.queueCapacity);
696
+ const apiClient = new PinqloqIngestApiClient(resolved);
697
+ const dispatcher = new PinqloqLogDispatcher(buffer, apiClient, resolved);
698
+ const logger = new DefaultPinqloqLogger(buffer, dispatcher, resolved);
699
+ dispatcher.start();
700
+ return {
701
+ logger,
702
+ requestLogging: (requestOptions) => createPinqloqRequestLogging(logger, resolved, requestOptions),
703
+ shutdown: () => dispatcher.shutdown()
704
+ };
705
+ }
706
+ // Annotate the CommonJS export names for ESM import in node:
707
+ 0 && (module.exports = {
708
+ CORRELATION_ID_HEADER_NAME,
709
+ DEVICE_IDENTIFIER_HEADER_NAME,
710
+ PinqloqLogFailureReason,
711
+ PinqloqLogLevel,
712
+ PinqloqLogSourceType,
713
+ PinqloqRedactionPlan,
714
+ createPinqloq
715
+ });
716
+ //# sourceMappingURL=index.cjs.map