@logbrew/sdk 0.1.36 → 0.1.38

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/validation.cjs ADDED
@@ -0,0 +1,279 @@
1
+ const { SdkError } = require("./sdk-error.cjs");
2
+
3
+ /**
4
+ * @template Value
5
+ * @typedef {Value extends readonly (infer Item)[]
6
+ * ? readonly DeepReadonly<Item>[]
7
+ * : Value extends object ? { readonly [Key in keyof Value]: DeepReadonly<Value[Key]> } : Value} DeepReadonly
8
+ */
9
+
10
+ const ZERO_TRACE_ID = "00000000000000000000000000000000";
11
+ const ZERO_SPAN_ID = "0000000000000000";
12
+ const TRACE_ID_PATTERN = /^[0-9a-fA-F]{32}$/u;
13
+ const SPAN_ID_PATTERN = /^[0-9a-fA-F]{16}$/u;
14
+ const TRACE_FLAGS_PATTERN = /^[0-9a-fA-F]{2}$/u;
15
+ const NEGATIVE_OFFSET_PATTERN = /.+-.+/u;
16
+ const RFC3339_TIMESTAMP_PATTERN =
17
+ /^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d{1,9})?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/u;
18
+ const EPOCH_MS = 0;
19
+ const YEAR_ZERO = 0;
20
+ const FIRST_MONTH = 1;
21
+ const MIN_POSITIVE_INTEGER = 1;
22
+ const MIN_NON_NEGATIVE_INTEGER = 0;
23
+ const NO_UNKNOWN_KEYS = 0;
24
+ const FIRST_CODE_POINT = 0;
25
+ const LAST_C0_CONTROL = 31;
26
+ const FIRST_C1_CONTROL = 127;
27
+ const LAST_C1_CONTROL = 159;
28
+
29
+ /**
30
+ * @param {unknown} value
31
+ * @returns {value is Readonly<Record<string, unknown>>}
32
+ */
33
+ function isRecord(value) {
34
+ return typeof value === "object" && value !== null && !Array.isArray(value);
35
+ }
36
+
37
+ /**
38
+ * @param {unknown} value
39
+ * @returns {value is readonly unknown[]}
40
+ */
41
+ function isList(value) {
42
+ return Array.isArray(value);
43
+ }
44
+
45
+ /**
46
+ * @param {string} label
47
+ * @param {unknown} value
48
+ * @returns {Readonly<Record<string, unknown>>}
49
+ */
50
+ function requireObject(label, value) {
51
+ if (!isRecord(value)) {
52
+ throw new SdkError("validation_error", `${label} must be an object`);
53
+ }
54
+ return value;
55
+ }
56
+
57
+ /**
58
+ * @param {string} label
59
+ * @param {Readonly<Record<string, unknown>>} value
60
+ * @param {Readonly<ReadonlySet<string>>} allowed
61
+ * @returns {void}
62
+ */
63
+ function rejectUnknownKeys(label, value, allowed) {
64
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
65
+ if (unknown.length > NO_UNKNOWN_KEYS) {
66
+ unknown.sort();
67
+ throw new SdkError("validation_error", `${label} has unsupported fields: ${unknown.join(", ")}`);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * @param {string} value
73
+ * @returns {boolean}
74
+ */
75
+ function hasUnicodeControlCharacter(value) {
76
+ return [...value].some((character) => {
77
+ const code = character.codePointAt(FIRST_CODE_POINT);
78
+ return code !== undefined && (code <= LAST_C0_CONTROL || (code >= FIRST_C1_CONTROL && code <= LAST_C1_CONTROL));
79
+ });
80
+ }
81
+
82
+ /**
83
+ * @template Value
84
+ * @template {string} Key
85
+ * @param {Key} key
86
+ * @param {Value | undefined} value
87
+ * @returns {{ [Field in Key]?: Value }}
88
+ */
89
+ function optionalEntry(key, value) {
90
+ /** @type {{ [Field in Key]?: Value }} */
91
+ const entry = {};
92
+ if (value !== undefined) {
93
+ entry[key] = value;
94
+ }
95
+ return entry;
96
+ }
97
+
98
+ /**
99
+ * @param {string} label
100
+ * @param {unknown} value
101
+ * @returns {string}
102
+ */
103
+ function requireNonEmpty(label, value) {
104
+ if (typeof value !== "string" || value.trim() === "") {
105
+ throw new SdkError("validation_error", `${label} must be non-empty`);
106
+ }
107
+ return value;
108
+ }
109
+
110
+ /**
111
+ * @template {string} Allowed
112
+ * @param {string} value
113
+ * @param {Readonly<ReadonlySet<Allowed>>} allowedValues
114
+ * @returns {value is Allowed}
115
+ */
116
+ function isAllowedValue(value, allowedValues) {
117
+ /** @type {ReadonlySet<string>} */
118
+ const values = allowedValues;
119
+ return values.has(value);
120
+ }
121
+
122
+ /**
123
+ * @template {string} Allowed
124
+ * @param {string} label
125
+ * @param {unknown} value
126
+ * @param {Readonly<ReadonlySet<Allowed>>} allowedValues
127
+ * @returns {Allowed}
128
+ */
129
+ function requireAllowedValue(label, value, allowedValues) {
130
+ const text = requireNonEmpty(label, value);
131
+ if (!isAllowedValue(text, allowedValues)) {
132
+ throw new SdkError("validation_error", `${label} must be one of: ${[...allowedValues].join(", ")}`);
133
+ }
134
+ return text;
135
+ }
136
+
137
+ /**
138
+ * @param {string} label
139
+ * @param {unknown} value
140
+ * @returns {number}
141
+ */
142
+ function requireFiniteNumber(label, value) {
143
+ if (typeof value !== "number" || !Number.isFinite(value)) {
144
+ throw new SdkError("validation_error", `${label} must be a finite number`);
145
+ }
146
+ return value;
147
+ }
148
+
149
+ /**
150
+ * @param {string} label
151
+ * @param {unknown} value
152
+ * @returns {number}
153
+ */
154
+ function requirePositiveInteger(label, value) {
155
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_POSITIVE_INTEGER) {
156
+ throw new SdkError("validation_error", `${label} must be a positive integer`);
157
+ }
158
+ return value;
159
+ }
160
+
161
+ /**
162
+ * @param {string} label
163
+ * @param {unknown} value
164
+ * @returns {number}
165
+ */
166
+ function requireNonNegativeInteger(label, value) {
167
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < MIN_NON_NEGATIVE_INTEGER) {
168
+ throw new SdkError("validation_error", `${label} must be a non-negative integer`);
169
+ }
170
+ return value;
171
+ }
172
+
173
+ /**
174
+ * @param {unknown} traceId
175
+ * @returns {string}
176
+ */
177
+ function requireTraceId(traceId) {
178
+ if (typeof traceId !== "string" || !TRACE_ID_PATTERN.test(traceId)) {
179
+ throw new SdkError("validation_error", "traceId must be 32 lowercase or uppercase hex characters");
180
+ }
181
+ if (traceId.toLowerCase() === ZERO_TRACE_ID) {
182
+ throw new SdkError("validation_error", "traceId must not be all zeros");
183
+ }
184
+ return traceId;
185
+ }
186
+
187
+ /**
188
+ * @param {string} label
189
+ * @param {unknown} spanId
190
+ * @returns {string}
191
+ */
192
+ function requireSpanId(label, spanId) {
193
+ if (typeof spanId !== "string" || !SPAN_ID_PATTERN.test(spanId)) {
194
+ throw new SdkError("validation_error", `${label} must be 16 lowercase or uppercase hex characters`);
195
+ }
196
+ if (spanId.toLowerCase() === ZERO_SPAN_ID) {
197
+ throw new SdkError("validation_error", `${label} must not be all zeros`);
198
+ }
199
+ return spanId;
200
+ }
201
+
202
+ /**
203
+ * @param {unknown} traceFlags
204
+ * @returns {string}
205
+ */
206
+ function requireTraceFlags(traceFlags) {
207
+ if (typeof traceFlags !== "string" || !TRACE_FLAGS_PATTERN.test(traceFlags)) {
208
+ throw new SdkError("validation_error", "traceFlags must be 2 lowercase or uppercase hex characters");
209
+ }
210
+ return traceFlags;
211
+ }
212
+
213
+ /**
214
+ * @param {string} timestamp
215
+ * @returns {boolean}
216
+ */
217
+ function hasTimezone(timestamp) {
218
+ if (timestamp.endsWith("Z")) {
219
+ return true;
220
+ }
221
+ const [, timePortion] = timestamp.split("T");
222
+ return timePortion !== undefined && (timePortion.includes("+") || NEGATIVE_OFFSET_PATTERN.test(timePortion));
223
+ }
224
+
225
+ /**
226
+ * @param {string} timestamp
227
+ * @returns {boolean}
228
+ */
229
+ function isCalendarDate(timestamp) {
230
+ const parts = RFC3339_TIMESTAMP_PATTERN.exec(timestamp)?.groups;
231
+ if (parts === undefined) {
232
+ return false;
233
+ }
234
+ const year = Number(parts["year"]);
235
+ const monthIndex = Number(parts["month"]) - FIRST_MONTH;
236
+ const day = Number(parts["day"]);
237
+ const calendar = new Date(EPOCH_MS);
238
+ calendar.setUTCFullYear(year, monthIndex, day);
239
+ return year !== YEAR_ZERO
240
+ && calendar.getUTCFullYear() === year
241
+ && calendar.getUTCMonth() === monthIndex
242
+ && calendar.getUTCDate() === day;
243
+ }
244
+
245
+ /**
246
+ * @param {unknown} timestamp
247
+ * @returns {string}
248
+ */
249
+ function requireTimestamp(timestamp) {
250
+ const text = requireNonEmpty("timestamp", timestamp);
251
+ if (!hasTimezone(text)) {
252
+ throw new SdkError("validation_error", `timestamp must include a timezone offset: ${text}`);
253
+ }
254
+ if (!isCalendarDate(text)) {
255
+ throw new SdkError("validation_error", `invalid timestamp: ${text}`);
256
+ }
257
+ return text;
258
+ }
259
+
260
+ module.exports = {
261
+ ZERO_SPAN_ID,
262
+ ZERO_TRACE_ID,
263
+ hasUnicodeControlCharacter,
264
+ isAllowedValue,
265
+ isList,
266
+ isRecord,
267
+ optionalEntry,
268
+ rejectUnknownKeys,
269
+ requireAllowedValue,
270
+ requireFiniteNumber,
271
+ requireNonEmpty,
272
+ requireNonNegativeInteger,
273
+ requireObject,
274
+ requirePositiveInteger,
275
+ requireSpanId,
276
+ requireTimestamp,
277
+ requireTraceFlags,
278
+ requireTraceId
279
+ };
package/winston.cjs CHANGED
@@ -1,7 +1,12 @@
1
1
  const { Writable } = require("node:stream");
2
2
 
3
3
  const { LogBrewClient, SdkError } = require("./core.cjs");
4
- const { buildLogContextHelpers } = require("./log-context.cjs");
4
+ const {
5
+ compactMetadata,
6
+ isMetadataValue,
7
+ traceFromProvider,
8
+ traceMetadataFromLogContext
9
+ } = require("./log-context.cjs");
5
10
 
6
11
  // Runtime packages with request scopes (such as @logbrew/node) register a recorder that keeps
7
12
  // breadcrumbs with the active request instead of the whole client.
@@ -16,12 +21,6 @@ const WINSTON_RESERVED_FIELDS = new Set([
16
21
  "error",
17
22
  "stack"
18
23
  ]);
19
- const {
20
- compactMetadata,
21
- isMetadataValue,
22
- traceFromProvider,
23
- traceMetadataFromLogContext
24
- } = buildLogContextHelpers({ SdkError });
25
24
 
26
25
  function createLogBrewWinstonTransport(config) {
27
26
  if (!config || typeof config !== "object") {