deepline 0.1.222 → 0.1.224

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,93 @@
1
+ import {
2
+ SpanKind,
3
+ SpanStatusCode,
4
+ trace,
5
+ type Attributes,
6
+ type Span,
7
+ } from '@opentelemetry/api';
8
+
9
+ type PrimitiveAttribute = string | number | boolean | null | undefined;
10
+
11
+ type TraceAttributes = Record<string, PrimitiveAttribute>;
12
+
13
+ function normalizeAttributes(
14
+ attributes: TraceAttributes | undefined,
15
+ ): Attributes | undefined {
16
+ if (!attributes) {
17
+ return undefined;
18
+ }
19
+
20
+ const normalized: Attributes = {};
21
+ for (const [key, value] of Object.entries(attributes)) {
22
+ if (value === undefined || value === null) {
23
+ continue;
24
+ }
25
+ if (
26
+ typeof value === 'string' ||
27
+ typeof value === 'number' ||
28
+ typeof value === 'boolean'
29
+ ) {
30
+ normalized[key] = value;
31
+ }
32
+ }
33
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
34
+ }
35
+
36
+ function recordError(span: Span, error: unknown): void {
37
+ if (error instanceof Error) {
38
+ span.recordException(error);
39
+ span.setStatus({
40
+ code: SpanStatusCode.ERROR,
41
+ message: error.message,
42
+ });
43
+ return;
44
+ }
45
+
46
+ const message = String(error);
47
+ span.recordException({ name: 'Error', message });
48
+ span.setStatus({
49
+ code: SpanStatusCode.ERROR,
50
+ message,
51
+ });
52
+ }
53
+
54
+ export async function withActiveSpan<T>(
55
+ name: string,
56
+ options: {
57
+ tracer?: string;
58
+ kind?: SpanKind;
59
+ attributes?: TraceAttributes;
60
+ },
61
+ fn: (span: Span) => Promise<T> | T,
62
+ ): Promise<T> {
63
+ const tracer = trace.getTracer(options.tracer ?? 'deepline');
64
+ return await tracer.startActiveSpan(
65
+ name,
66
+ {
67
+ kind: options.kind,
68
+ attributes: normalizeAttributes(options.attributes),
69
+ },
70
+ async (span) => {
71
+ try {
72
+ const result = await fn(span);
73
+ span.setStatus({ code: SpanStatusCode.OK });
74
+ return result;
75
+ } catch (error) {
76
+ recordError(span, error);
77
+ throw error;
78
+ } finally {
79
+ span.end();
80
+ }
81
+ },
82
+ );
83
+ }
84
+
85
+ export function setSpanAttributes(
86
+ span: Span,
87
+ attributes: TraceAttributes,
88
+ ): void {
89
+ const normalized = normalizeAttributes(attributes);
90
+ if (normalized) {
91
+ span.setAttributes(normalized);
92
+ }
93
+ }
@@ -0,0 +1,119 @@
1
+ export type TelemetryScalar = string | number | boolean | null | undefined;
2
+ export type TelemetryFields = Readonly<Record<string, unknown>>;
3
+
4
+ type TelemetryContext = {
5
+ orgId?: string | null;
6
+ playName?: string | null;
7
+ requestId?: string | null;
8
+ runId?: string | null;
9
+ };
10
+
11
+ export type TelemetryEvent = {
12
+ telemetrySchemaVersion: 1;
13
+ timestamp: string;
14
+ service: string;
15
+ component: string | null;
16
+ event: string;
17
+ tag: string;
18
+ telemetryKind: 'log';
19
+ telemetryLevel: 'info' | 'warn' | 'error';
20
+ errorType?: string;
21
+ errorMessage?: string;
22
+ } & TelemetryContext &
23
+ Record<string, TelemetryScalar>;
24
+
25
+ type TelemetryAdapter = { emit(event: TelemetryEvent): void | Promise<void> };
26
+ type TelemetryOptions = { tag?: string };
27
+ type WorkerTelemetryInput = {
28
+ service: string;
29
+ component?: string;
30
+ context?: TelemetryContext;
31
+ adapter?: TelemetryAdapter;
32
+ };
33
+
34
+ type WorkerTelemetry = {
35
+ child(input: {
36
+ component?: string;
37
+ context?: TelemetryContext;
38
+ }): WorkerTelemetry;
39
+ info(
40
+ event: string,
41
+ fields?: TelemetryFields,
42
+ options?: TelemetryOptions,
43
+ ): Promise<void>;
44
+ warn(
45
+ event: string,
46
+ fields?: TelemetryFields,
47
+ options?: TelemetryOptions,
48
+ ): Promise<void>;
49
+ error(
50
+ event: string,
51
+ error?: unknown,
52
+ fields?: TelemetryFields,
53
+ options?: TelemetryOptions,
54
+ ): Promise<void>;
55
+ };
56
+
57
+ /**
58
+ * Small, Worker-safe implementation of the canonical telemetry envelope.
59
+ * It intentionally omits server-only recursive normalization and adapters so
60
+ * it can remain in the size-capped dynamic play bundle.
61
+ */
62
+ export function createWorkerTelemetry(
63
+ input: WorkerTelemetryInput,
64
+ ): WorkerTelemetry {
65
+ const service = input.service.trim();
66
+ const component = input.component?.trim() || null;
67
+ const context = input.context ?? {};
68
+ const adapter = input.adapter ?? {
69
+ emit: (event: TelemetryEvent) =>
70
+ console[event.telemetryLevel](JSON.stringify(event)),
71
+ };
72
+
73
+ async function emit(
74
+ level: TelemetryEvent['telemetryLevel'],
75
+ event: string,
76
+ fields?: TelemetryFields,
77
+ options?: TelemetryOptions,
78
+ error?: unknown,
79
+ ): Promise<void> {
80
+ try {
81
+ await adapter.emit({
82
+ telemetrySchemaVersion: 1,
83
+ timestamp: new Date().toISOString(),
84
+ service,
85
+ component,
86
+ event,
87
+ tag: options?.tag?.trim() || `[${event}]`,
88
+ ...context,
89
+ ...(fields as Record<string, TelemetryScalar>),
90
+ telemetryKind: 'log',
91
+ telemetryLevel: level,
92
+ ...(error == null
93
+ ? {}
94
+ : {
95
+ errorType: error instanceof Error ? error.name : 'Error',
96
+ errorMessage:
97
+ error instanceof Error ? error.message : String(error),
98
+ }),
99
+ });
100
+ } catch {
101
+ // Never make workflow execution depend on telemetry delivery.
102
+ }
103
+ }
104
+
105
+ return {
106
+ child(child) {
107
+ return createWorkerTelemetry({
108
+ service,
109
+ component: child.component ?? component ?? undefined,
110
+ context: { ...context, ...child.context },
111
+ adapter,
112
+ });
113
+ },
114
+ info: (event, fields, options) => emit('info', event, fields, options),
115
+ warn: (event, fields, options) => emit('warn', event, fields, options),
116
+ error: (event, error, fields, options) =>
117
+ emit('error', event, fields, options, error),
118
+ };
119
+ }
@@ -1,37 +1,47 @@
1
1
  export const SECRET_REDACTION_PLACEHOLDER = '[REDACTED_SECRET]';
2
2
 
3
3
  const BEARER_TOKEN_RE = /\bBearer\s+[A-Za-z0-9._~+/=-]{12,}\b/g;
4
- const JWT_RE =
5
- /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
4
+ const BASIC_CREDENTIAL_RE = /\bBasic\s+[A-Za-z0-9+/]{4,}={0,2}\b/g;
5
+ const JWT_RE = /\b[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{1,}\.[A-Za-z0-9_-]{8,}\b/g;
6
6
  const PRIVATE_KEY_RE =
7
7
  /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g;
8
+ const SENSITIVE_FIELD_NAME =
9
+ 'authorization|cookie|password|(?:client[_-]?)?secret|(?:access|refresh)[_-]?token|(?:x[_-]?)?api[_-]?key|private[_-]?key|token|credentials';
8
10
  // Only redact self-identifying credential prefixes here. Generic words such as
9
11
  // `key`, `token`, and `secret` are common in harmless play/run slugs (for
10
12
  // example `absurd-key-rotation-check`) and need an assignment context before
11
13
  // they are sensitive; HIGH_ENTROPY_ASSIGNMENT_RE handles that case below.
12
14
  const COMMON_SECRET_RE =
13
- /\b(?:(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9_./+=:-]{12,}|(?:pat|ghp|github_pat)_[A-Za-z0-9_./+=:-]{12,}|xox[baprs]-[A-Za-z0-9_./+=:-]{12,})\b/gi;
15
+ /\b(?:[prs]k_(?:live|test)_|(?:pat|ghp|github_pat)_|xox[baprs]-)[A-Za-z0-9_./+=:-]{12,}\b/gi;
14
16
  const GENERIC_PREFIXED_SECRET_RE =
15
17
  /\b(?:key|token|secret|api[_-]?key)_[A-Za-z0-9_./+=:-]{12,}\b/gi;
16
18
  const URL_SECRET_VALUE_RE =
17
- /\b(?:sk|pk|rk|pat|ghp|github_pat|xox)[A-Za-z0-9_./+=:-]{12,}\b/i;
19
+ /\b(?:[spr]k|pat|ghp|github_pat|xox)[A-Za-z0-9_./+=:-]{12,}\b/i;
18
20
  const GENERIC_SECRET_ASSIGNMENT_RE =
19
21
  /\b(?:api[_-]?key|token|secret|password)\b\s*[:=]\s*["']?[^"',\s]{12,}["']?/gi;
20
22
  const HIGH_ENTROPY_ASSIGNMENT_RE =
21
- /\b(?:api[_-]?key|token|secret|password|authorization|access[_-]?token|refresh[_-]?token)\b\s*[:=]\s*["']?[^"',\s]{16,}["']?/gi;
23
+ /\b(?:api[_-]?key|token|secret|password|authorization|(?:access|refresh)[_-]?token)\b\s*[:=]\s*["']?[^"',\s]{16,}["']?/gi;
22
24
  const SENSITIVE_URL_PARAM_RE =
23
25
  /[?#&](?:\w*(?:key|token)|secret|password|authorization|credential|signature|sig)(?:=|$)/i;
26
+ const SENSITIVE_FIELD_NAME_RE = new RegExp(
27
+ `(^|[._-])(?:${SENSITIVE_FIELD_NAME})$`,
28
+ 'i',
29
+ );
30
+ const SERIALIZED_SECRET_FIELD_RE = new RegExp(
31
+ `"(${SENSITIVE_FIELD_NAME})"\\s*:\\s*(?:"[^"\\\\]*"|\\{[^{}]*\\})`,
32
+ 'gi',
33
+ );
24
34
 
25
35
  function escapeRegExp(value: string): string {
26
36
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
27
37
  }
28
38
 
29
- function isRecord(value: unknown): value is Record<string, unknown> {
30
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
31
- }
32
-
33
39
  export function redactSecretLikeString(value: string): string {
34
- const trimmed = value.trim();
40
+ const serializedRedacted = value.replace(
41
+ SERIALIZED_SECRET_FIELD_RE,
42
+ `"$1":"${SECRET_REDACTION_PLACEHOLDER}"`,
43
+ );
44
+ const trimmed = serializedRedacted.trim();
35
45
  if (/^https?:\/\/\S+$/i.test(trimmed)) {
36
46
  if (
37
47
  /^https?:\/\/[^/?#@]+@/i.test(trimmed) ||
@@ -39,12 +49,15 @@ export function redactSecretLikeString(value: string): string {
39
49
  ) {
40
50
  return SECRET_REDACTION_PLACEHOLDER;
41
51
  }
42
- if (!URL_SECRET_VALUE_RE.test(value)) return value;
52
+ if (!URL_SECRET_VALUE_RE.test(serializedRedacted)) {
53
+ return serializedRedacted;
54
+ }
43
55
  }
44
56
 
45
- return value
57
+ return serializedRedacted
46
58
  .replace(PRIVATE_KEY_RE, SECRET_REDACTION_PLACEHOLDER)
47
59
  .replace(BEARER_TOKEN_RE, `Bearer ${SECRET_REDACTION_PLACEHOLDER}`)
60
+ .replace(BASIC_CREDENTIAL_RE, `Basic ${SECRET_REDACTION_PLACEHOLDER}`)
48
61
  .replace(JWT_RE, SECRET_REDACTION_PLACEHOLDER)
49
62
  .replace(COMMON_SECRET_RE, SECRET_REDACTION_PLACEHOLDER)
50
63
  .replace(GENERIC_PREFIXED_SECRET_RE, SECRET_REDACTION_PLACEHOLDER)
@@ -99,9 +112,14 @@ export function createSecretRedactionContext(
99
112
  function redact<T>(value: T): T {
100
113
  if (typeof value === 'string') return redactString(value) as T;
101
114
  if (Array.isArray(value)) return value.map((entry) => redact(entry)) as T;
102
- if (isRecord(value)) {
115
+ if (value && typeof value === 'object') {
103
116
  return Object.fromEntries(
104
- Object.entries(value).map(([key, entry]) => [key, redact(entry)]),
117
+ Object.entries(value).map(([key, entry]) => [
118
+ key,
119
+ SENSITIVE_FIELD_NAME_RE.test(key)
120
+ ? SECRET_REDACTION_PLACEHOLDER
121
+ : redact(entry),
122
+ ]),
105
123
  ) as T;
106
124
  }
107
125
  return value;