@unchainedshop/events 4.8.25 → 4.8.27

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.
@@ -0,0 +1,62 @@
1
+ export type OTLPAnyValue = {
2
+ stringValue: string;
3
+ } | {
4
+ boolValue: boolean;
5
+ } | {
6
+ intValue: string;
7
+ } | {
8
+ doubleValue: number;
9
+ } | {
10
+ arrayValue: {
11
+ values: OTLPAnyValue[];
12
+ };
13
+ } | {
14
+ kvlistValue: {
15
+ values: OTLPKeyValue[];
16
+ };
17
+ };
18
+ export interface OTLPKeyValue {
19
+ key: string;
20
+ value: OTLPAnyValue;
21
+ }
22
+ export interface OTLPResource {
23
+ attributes: OTLPKeyValue[];
24
+ }
25
+ export interface OTLPInstrumentationScope {
26
+ name: string;
27
+ version?: string;
28
+ }
29
+ export declare const OTLP_SEVERITY_NUMBER: {
30
+ readonly UNSPECIFIED: 0;
31
+ readonly INFO: 9;
32
+ readonly INFO2: 10;
33
+ readonly WARN: 13;
34
+ readonly ERROR: 17;
35
+ readonly FATAL: 21;
36
+ readonly FATAL4: 24;
37
+ };
38
+ export interface OTLPLogRecord {
39
+ timeUnixNano: string;
40
+ observedTimeUnixNano: string;
41
+ severityNumber: number;
42
+ severityText?: string;
43
+ body: OTLPAnyValue;
44
+ attributes: OTLPKeyValue[];
45
+ }
46
+ export interface OTLPScopeLogs {
47
+ scope: OTLPInstrumentationScope;
48
+ logRecords: OTLPLogRecord[];
49
+ }
50
+ export interface OTLPResourceLogs {
51
+ resource: OTLPResource;
52
+ scopeLogs: OTLPScopeLogs[];
53
+ }
54
+ export interface OTLPExportLogsServiceRequest {
55
+ resourceLogs: OTLPResourceLogs[];
56
+ }
57
+ export interface OTLPExportLogsServiceResponse {
58
+ partialSuccess?: {
59
+ rejectedLogRecords?: number | string;
60
+ errorMessage?: string;
61
+ };
62
+ }
@@ -0,0 +1,9 @@
1
+ export const OTLP_SEVERITY_NUMBER = {
2
+ UNSPECIFIED: 0,
3
+ INFO: 9,
4
+ INFO2: 10,
5
+ WARN: 13,
6
+ ERROR: 17,
7
+ FATAL: 21,
8
+ FATAL4: 24,
9
+ };
@@ -0,0 +1,9 @@
1
+ import type { OCSFEvent } from './ocsf-types.ts';
2
+ import { type OTLPAnyValue, type OTLPLogRecord, type OTLPExportLogsServiceRequest } from './otlp-types.ts';
3
+ export declare function toAnyValue(value: unknown): OTLPAnyValue;
4
+ export declare function encodeOCSFLogRecord(event: OCSFEvent, observedTimeMs: number): OTLPLogRecord;
5
+ export declare function buildExportLogsRequest(events: OCSFEvent[], observedTimeMs?: number): OTLPExportLogsServiceRequest;
6
+ export declare function parseOtlpHeaders(raw?: string): Record<string, string>;
7
+ export declare function resolveCollectorUrl(explicit?: string): string | undefined;
8
+ export declare function resolveCollectorHeaders(explicit?: Record<string, string>): Record<string, string>;
9
+ export declare function exportLogs(collectorUrl: string, collectorHeaders: Record<string, string>, events: OCSFEvent[]): Promise<void>;
@@ -0,0 +1,164 @@
1
+ import { createLogger } from '@unchainedshop/logger';
2
+ import { OTLP_SEVERITY_NUMBER, } from "./otlp-types.js";
3
+ const logger = createLogger('unchained:audit');
4
+ const SEVERITY_NUMBER_MAP = {
5
+ 0: OTLP_SEVERITY_NUMBER.UNSPECIFIED,
6
+ 1: OTLP_SEVERITY_NUMBER.INFO,
7
+ 2: OTLP_SEVERITY_NUMBER.INFO2,
8
+ 3: OTLP_SEVERITY_NUMBER.WARN,
9
+ 4: OTLP_SEVERITY_NUMBER.ERROR,
10
+ 5: OTLP_SEVERITY_NUMBER.FATAL,
11
+ 6: OTLP_SEVERITY_NUMBER.FATAL4,
12
+ };
13
+ const SEVERITY_TEXT_MAP = {
14
+ 0: 'Unknown',
15
+ 1: 'Informational',
16
+ 2: 'Low',
17
+ 3: 'Medium',
18
+ 4: 'High',
19
+ 5: 'Critical',
20
+ 6: 'Fatal',
21
+ };
22
+ export function toAnyValue(value) {
23
+ if (typeof value === 'string')
24
+ return { stringValue: value };
25
+ if (typeof value === 'boolean')
26
+ return { boolValue: value };
27
+ if (typeof value === 'bigint')
28
+ return { intValue: value.toString() };
29
+ if (typeof value === 'number') {
30
+ return Number.isSafeInteger(value) ? { intValue: String(value) } : { doubleValue: value };
31
+ }
32
+ if (Array.isArray(value))
33
+ return { arrayValue: { values: value.map(toAnyValue) } };
34
+ if (value !== null && typeof value === 'object') {
35
+ const values = [];
36
+ for (const [key, entry] of Object.entries(value)) {
37
+ if (entry === undefined)
38
+ continue;
39
+ values.push({ key, value: toAnyValue(entry) });
40
+ }
41
+ return { kvlistValue: { values } };
42
+ }
43
+ return { stringValue: String(value ?? null) };
44
+ }
45
+ function toMillisTimeUnixNano(timeMs) {
46
+ return (BigInt(Math.round(timeMs)) * 1000000n).toString();
47
+ }
48
+ export function encodeOCSFLogRecord(event, observedTimeMs) {
49
+ const attributes = [];
50
+ const attr = (key, value) => {
51
+ if (value === undefined)
52
+ return;
53
+ attributes.push({ key, value: toAnyValue(value) });
54
+ };
55
+ const user = event.user ?? event.actor?.user;
56
+ const sessionId = event.session?.uid ?? event.actor?.session?.uid;
57
+ attr('ocsf.class_uid', event.class_uid);
58
+ attr('ocsf.category_uid', event.category_uid);
59
+ attr('ocsf.activity_id', event.activity_id);
60
+ attr('ocsf.type_uid', event.type_uid);
61
+ attr('ocsf.severity_id', event.severity_id);
62
+ attr('ocsf.status_id', event.status_id);
63
+ attr('user.id', user?.uid);
64
+ attr('user.name', user?.name);
65
+ attr('client.address', event.src_endpoint?.ip);
66
+ attr('session.id', sessionId);
67
+ attr('ocsf.api.operation', event.api?.operation);
68
+ attr('log.record.uid', event.metadata.uid);
69
+ return {
70
+ timeUnixNano: toMillisTimeUnixNano(event.time),
71
+ observedTimeUnixNano: toMillisTimeUnixNano(observedTimeMs),
72
+ severityNumber: SEVERITY_NUMBER_MAP[event.severity_id] ?? OTLP_SEVERITY_NUMBER.UNSPECIFIED,
73
+ severityText: SEVERITY_TEXT_MAP[event.severity_id],
74
+ body: toAnyValue(event),
75
+ attributes,
76
+ };
77
+ }
78
+ export function buildExportLogsRequest(events, observedTimeMs = Date.now()) {
79
+ const resourceAttributes = [
80
+ {
81
+ key: 'service.name',
82
+ value: toAnyValue(process.env.OTEL_SERVICE_NAME || 'unchained-engine'),
83
+ },
84
+ ];
85
+ if (process.env.npm_package_version) {
86
+ resourceAttributes.push({
87
+ key: 'service.version',
88
+ value: toAnyValue(process.env.npm_package_version),
89
+ });
90
+ }
91
+ return {
92
+ resourceLogs: [
93
+ {
94
+ resource: { attributes: resourceAttributes },
95
+ scopeLogs: [
96
+ {
97
+ scope: { name: 'unchained:audit' },
98
+ logRecords: events.map((event) => encodeOCSFLogRecord(event, observedTimeMs)),
99
+ },
100
+ ],
101
+ },
102
+ ],
103
+ };
104
+ }
105
+ export function parseOtlpHeaders(raw) {
106
+ const headers = {};
107
+ if (!raw)
108
+ return headers;
109
+ for (const pair of raw.split(',')) {
110
+ const separatorIndex = pair.indexOf('=');
111
+ if (separatorIndex <= 0)
112
+ continue;
113
+ const key = pair.slice(0, separatorIndex).trim();
114
+ let value = pair.slice(separatorIndex + 1).trim();
115
+ try {
116
+ value = decodeURIComponent(value);
117
+ }
118
+ catch {
119
+ }
120
+ if (key)
121
+ headers[key] = value;
122
+ }
123
+ return headers;
124
+ }
125
+ export function resolveCollectorUrl(explicit) {
126
+ if (explicit)
127
+ return explicit;
128
+ if (process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT) {
129
+ return process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT;
130
+ }
131
+ if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {
132
+ return `${process.env.OTEL_EXPORTER_OTLP_ENDPOINT.replace(/\/+$/, '')}/v1/logs`;
133
+ }
134
+ return undefined;
135
+ }
136
+ export function resolveCollectorHeaders(explicit) {
137
+ return {
138
+ ...parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_HEADERS),
139
+ ...parseOtlpHeaders(process.env.OTEL_EXPORTER_OTLP_LOGS_HEADERS),
140
+ ...explicit,
141
+ };
142
+ }
143
+ export async function exportLogs(collectorUrl, collectorHeaders, events) {
144
+ const response = await fetch(collectorUrl, {
145
+ method: 'POST',
146
+ headers: {
147
+ 'Content-Type': 'application/json',
148
+ ...collectorHeaders,
149
+ },
150
+ body: JSON.stringify(buildExportLogsRequest(events)),
151
+ });
152
+ if (!response.ok) {
153
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
154
+ }
155
+ try {
156
+ const body = (await response.json());
157
+ const rejected = Number(body?.partialSuccess?.rejectedLogRecords || 0);
158
+ if (rejected > 0) {
159
+ logger.warn(`Collector rejected ${rejected} audit log records: ${body.partialSuccess?.errorMessage || 'unknown reason'}`);
160
+ }
161
+ }
162
+ catch {
163
+ }
164
+ }
@@ -0,0 +1,8 @@
1
+ export interface AuditRequestContext {
2
+ userId?: string;
3
+ userName?: string;
4
+ remoteAddress?: string;
5
+ sessionId?: string;
6
+ }
7
+ export declare function runWithAuditContext<T>(context: AuditRequestContext, fn: () => T): T;
8
+ export declare function getAuditContext(): AuditRequestContext | undefined;
@@ -0,0 +1,8 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ const asyncLocalStorage = new AsyncLocalStorage();
3
+ export function runWithAuditContext(context, fn) {
4
+ return asyncLocalStorage.run(context, fn);
5
+ }
6
+ export function getAuditContext() {
7
+ return asyncLocalStorage.getStore();
8
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@unchainedshop/events",
3
3
  "description": "Event emitter abstraction layer for the Unchained Engine",
4
- "version": "4.8.25",
4
+ "version": "4.8.27",
5
5
  "main": "lib/events-index.js",
6
6
  "types": "lib/events-index.d.ts",
7
7
  "type": "module",
@@ -34,7 +34,7 @@
34
34
  },
35
35
  "homepage": "https://github.com/unchainedshop/unchained#readme",
36
36
  "dependencies": {
37
- "@unchainedshop/logger": "^4.8.12"
37
+ "@unchainedshop/logger": "^4.8.27"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^26.2.0",