autotel
Version:
Write Once, Observe Anywhere
358 lines (356 loc) • 10.1 kB
JavaScript
import { context, propagation } from "@opentelemetry/api";
//#region src/business-baggage.ts
/**
* Safe baggage propagation with guardrails
*
* Provides type-safe baggage schemas with built-in protection against
* common pitfalls: high-cardinality values, PII leakage, and oversized payloads.
*
* @example Define a custom schema
* ```typescript
* import { createSafeBaggageSchema } from 'autotel/business-baggage';
*
* const OrderBaggage = createSafeBaggageSchema({
* orderId: { type: 'string' },
* customerId: { type: 'string', hash: true }, // Auto-hash for privacy
* priority: { type: 'enum', values: ['low', 'normal', 'high'] },
* });
*
* // Usage in traced function
* OrderBaggage.set(ctx, { orderId: 'ord-123', customerId: 'cust-456', priority: 'high' });
* const { orderId, priority } = OrderBaggage.get(ctx);
* ```
*
* @example Use pre-built BusinessBaggage
* ```typescript
* import { BusinessBaggage } from 'autotel/business-baggage';
*
* BusinessBaggage.set(ctx, { tenantId: 'acme', userId: 'user-123' });
* const { tenantId } = BusinessBaggage.get(ctx);
* ```
*
* @module
*/
const DEFAULT_MAX_KEY_LENGTH = 64;
const DEFAULT_MAX_VALUE_LENGTH = 256;
const DEFAULT_MAX_TOTAL_SIZE = 8192;
const PII_PATTERNS = [
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/,
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/,
/\b\d{3}[-]?\d{2}[-]?\d{4}\b/,
/\b\d{16}\b/
];
const HIGH_CARDINALITY_PATTERNS = [
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
/^\d{13,}$/,
/^[A-Za-z0-9+/]{20,}={0,2}$/
];
/**
* Create a safe baggage schema with validation and guardrails
*
* @param schema - Field definitions
* @param options - Safety options
* @returns Type-safe baggage schema
*
* @example
* ```typescript
* const MyBaggage = createSafeBaggageSchema({
* userId: { type: 'string', hash: true },
* region: { type: 'enum', values: ['us', 'eu', 'ap'] },
* debug: { type: 'boolean', defaultValue: false },
* });
* ```
*/
function createSafeBaggageSchema(schema, options = {}) {
const { maxKeyLength = DEFAULT_MAX_KEY_LENGTH, maxValueLength = DEFAULT_MAX_VALUE_LENGTH, maxTotalSize = DEFAULT_MAX_TOTAL_SIZE, prefix = "", hashHighCardinality = false, redactPII = false, allowedKeys, onError } = options;
const schemaKeys = new Set(Object.keys(schema));
if (allowedKeys) {
for (const key of schemaKeys) if (!allowedKeys.includes(key)) throw new Error(`Key "${key}" not in allowedKeys whitelist`);
}
const prefixKey = (key) => prefix ? `${prefix}.${key}` : key;
const hashValue = (value) => {
let hash = 2166136261;
for (let i = 0; i < value.length; i++) {
hash ^= value.codePointAt(i) ?? 0;
hash = hash * 16777619 >>> 0;
}
return `h_${hash.toString(16)}`;
};
const containsPII = (value) => {
return PII_PATTERNS.some((pattern) => pattern.test(value));
};
const isHighCardinality = (value) => {
return HIGH_CARDINALITY_PATTERNS.some((pattern) => pattern.test(value));
};
const validateAndTransform = (key, value, fieldDef) => {
if (prefixKey(key).length > maxKeyLength) {
onError?.({
type: "key_length",
key,
message: `Key "${key}" exceeds max length ${maxKeyLength}`
});
return null;
}
if (value === void 0 || value === null) {
if (fieldDef.required) {
onError?.({
type: "validation",
key,
message: `Required field "${key}" is missing`
});
return null;
}
if (fieldDef.defaultValue === void 0) return null;
else value = fieldDef.defaultValue;
}
let stringValue;
switch (fieldDef.type) {
case "string":
if (typeof value !== "string") {
onError?.({
type: "validation",
key,
message: `Field "${key}" expected string, got ${typeof value}`,
value
});
return null;
}
stringValue = value;
break;
case "number":
if (typeof value !== "number" || Number.isNaN(value)) {
onError?.({
type: "validation",
key,
message: `Field "${key}" expected number, got ${typeof value}`,
value
});
return null;
}
stringValue = String(value);
break;
case "boolean":
if (typeof value !== "boolean") {
onError?.({
type: "validation",
key,
message: `Field "${key}" expected boolean, got ${typeof value}`,
value
});
return null;
}
stringValue = String(value);
break;
case "enum":
if (!fieldDef.values?.includes(String(value))) {
onError?.({
type: "validation",
key,
message: `Field "${key}" value "${value}" not in allowed values: ${fieldDef.values?.join(", ")}`,
value
});
return null;
}
stringValue = String(value);
break;
default: stringValue = String(value);
}
if (fieldDef.validate && !fieldDef.validate(value)) {
onError?.({
type: "validation",
key,
message: `Field "${key}" failed custom validation`,
value
});
return null;
}
if (redactPII && containsPII(stringValue)) {
onError?.({
type: "pii",
key,
message: `Field "${key}" contains PII pattern`,
value: "[REDACTED]"
});
stringValue = hashValue(stringValue);
}
if (fieldDef.hash || hashHighCardinality && isHighCardinality(stringValue)) stringValue = hashValue(stringValue);
const maxLen = fieldDef.maxLength ?? maxValueLength;
if (stringValue.length > maxLen) {
onError?.({
type: "value_length",
key,
message: `Field "${key}" value exceeds max length ${maxLen}`,
value: stringValue
});
stringValue = stringValue.slice(0, maxLen);
}
return stringValue;
};
const parseValue = (key, stringValue, fieldDef) => {
switch (fieldDef.type) {
case "number": return Number.parseFloat(stringValue);
case "boolean": return stringValue === "true";
default: return stringValue;
}
};
return {
get() {
const baggage = propagation.getBaggage(context.active());
if (!baggage) return {};
const result = {};
for (const [key, fieldDef] of Object.entries(schema)) {
const fullKey = prefixKey(key);
const entry = baggage.getEntry(fullKey);
if (entry) result[key] = parseValue(key, entry.value, fieldDef);
else if (fieldDef.defaultValue !== void 0) result[key] = fieldDef.defaultValue;
}
return result;
},
set(ctx, values) {
let baggage = propagation.getBaggage(context.active()) ?? propagation.createBaggage();
let totalSize = 0;
for (const [key, entry] of baggage.getAllEntries()) totalSize += key.length + entry.value.length;
for (const [key, value] of Object.entries(values)) {
const fieldDef = schema[key];
if (!fieldDef) continue;
const fullKey = prefixKey(key);
const stringValue = validateAndTransform(key, value, fieldDef);
if (stringValue !== null) {
const entrySize = fullKey.length + stringValue.length;
if (totalSize + entrySize > maxTotalSize) {
onError?.({
type: "size",
key,
message: `Adding "${key}" would exceed max baggage size ${maxTotalSize}`,
value
});
continue;
}
baggage = baggage.setEntry(fullKey, { value: stringValue });
totalSize += entrySize;
}
}
const newContext = propagation.setBaggage(context.active(), baggage);
propagation.setBaggage(newContext, baggage);
},
getValue(key) {
const baggage = propagation.getBaggage(context.active());
if (!baggage) return void 0;
const fullKey = prefixKey(String(key));
const entry = baggage.getEntry(fullKey);
const fieldDef = schema[String(key)];
if (!entry) return fieldDef?.defaultValue;
if (!fieldDef) return;
return parseValue(String(key), entry.value, fieldDef);
},
setValue(key, value, ctx) {
this.set(ctx, { [key]: value });
},
clear() {
let baggage = propagation.getBaggage(context.active());
if (!baggage) return;
for (const key of Object.keys(schema)) {
const fullKey = prefixKey(key);
baggage = baggage.removeEntry(fullKey);
}
propagation.setBaggage(context.active(), baggage);
},
toHeaders() {
const headers = {};
propagation.inject(context.active(), headers);
return headers;
},
fromHeaders(headers, ctx) {
const extractedContext = propagation.extract(context.active(), headers);
const baggage = propagation.getBaggage(extractedContext);
if (baggage) {
const values = {};
for (const [key, fieldDef] of Object.entries(schema)) {
const fullKey = prefixKey(key);
const entry = baggage.getEntry(fullKey);
if (entry) values[key] = parseValue(key, entry.value, fieldDef);
}
this.set(ctx, values);
}
}
};
}
/**
* Pre-built baggage schema for common business context fields
*
* Fields:
* - `tenantId`: Multi-tenant identifier (string, max 64 chars)
* - `userId`: User identifier (hashed for privacy)
* - `correlationId`: Request correlation ID (string)
* - `workflowId`: Workflow/saga instance ID (string)
* - `priority`: Request priority (low, normal, high, critical)
* - `region`: Geographic region (string)
* - `channel`: Request channel (web, mobile, api, internal)
*
* @example
* ```typescript
* import { BusinessBaggage } from 'autotel/business-baggage';
*
* // Set business context at entry point
* BusinessBaggage.set(ctx, {
* tenantId: 'acme-corp',
* userId: 'user-123',
* priority: 'high',
* channel: 'api',
* });
*
* // Access anywhere in the trace
* const { tenantId, priority } = BusinessBaggage.get(ctx);
* ```
*/
const BusinessBaggage = createSafeBaggageSchema({
tenantId: {
type: "string",
maxLength: 64
},
userId: {
type: "string",
hash: true,
maxLength: 64
},
correlationId: {
type: "string",
maxLength: 128
},
workflowId: {
type: "string",
maxLength: 128
},
priority: {
type: "enum",
values: [
"low",
"normal",
"high",
"critical"
],
defaultValue: "normal"
},
region: {
type: "string",
maxLength: 32
},
channel: {
type: "enum",
values: [
"web",
"mobile",
"api",
"internal",
"webhook",
"scheduled"
]
}
}, {
prefix: "biz",
redactPII: true,
hashHighCardinality: true
});
//#endregion
export { BusinessBaggage, createSafeBaggageSchema };
//# sourceMappingURL=business-baggage.js.map