@outfitter/contracts 0.1.0-rc.1
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/README.md +50 -0
- package/dist/actions.d.ts +388 -0
- package/dist/actions.js +32 -0
- package/dist/adapters.d.ts +182 -0
- package/dist/adapters.js +1 -0
- package/dist/assert/index.d.ts +63 -0
- package/dist/assert/index.js +36 -0
- package/dist/capabilities.d.ts +19 -0
- package/dist/capabilities.js +66 -0
- package/dist/context.d.ts +125 -0
- package/dist/context.js +36 -0
- package/dist/envelope.d.ts +328 -0
- package/dist/envelope.js +56 -0
- package/dist/errors.d.ts +261 -0
- package/dist/errors.js +171 -0
- package/dist/handler.d.ts +318 -0
- package/dist/handler.js +1 -0
- package/dist/index.d.ts +1354 -0
- package/dist/index.js +137 -0
- package/dist/recovery.d.ts +150 -0
- package/dist/recovery.js +56 -0
- package/dist/redactor.d.ts +100 -0
- package/dist/redactor.js +111 -0
- package/dist/resilience.d.ts +299 -0
- package/dist/resilience.js +82 -0
- package/dist/result/index.d.ts +103 -0
- package/dist/result/index.js +13 -0
- package/dist/result/utilities.d.ts +103 -0
- package/dist/result/utilities.js +31 -0
- package/dist/serialization.d.ts +313 -0
- package/dist/serialization.js +270 -0
- package/dist/validation.d.ts +59 -0
- package/dist/validation.js +42 -0
- package/package.json +143 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
createValidator,
|
|
4
|
+
validateInput
|
|
5
|
+
} from "./validation.js";
|
|
6
|
+
import {
|
|
7
|
+
ACTION_SURFACES,
|
|
8
|
+
DEFAULT_REGISTRY_SURFACES,
|
|
9
|
+
createActionRegistry,
|
|
10
|
+
defineAction
|
|
11
|
+
} from "./actions.js";
|
|
12
|
+
import {
|
|
13
|
+
getBackoffDelay,
|
|
14
|
+
isRecoverable,
|
|
15
|
+
isRetryable,
|
|
16
|
+
shouldRetry
|
|
17
|
+
} from "./recovery.js";
|
|
18
|
+
import {
|
|
19
|
+
assertDefined,
|
|
20
|
+
assertMatches,
|
|
21
|
+
assertNonEmpty,
|
|
22
|
+
isNonEmptyArray
|
|
23
|
+
} from "./assert/index.js";
|
|
24
|
+
import"./result/index.js";
|
|
25
|
+
import {
|
|
26
|
+
combine2,
|
|
27
|
+
combine3,
|
|
28
|
+
orElse,
|
|
29
|
+
unwrapOrElse
|
|
30
|
+
} from "./result/utilities.js";
|
|
31
|
+
import {
|
|
32
|
+
toEnvelope,
|
|
33
|
+
toHttpResponse
|
|
34
|
+
} from "./envelope.js";
|
|
35
|
+
import {
|
|
36
|
+
retry,
|
|
37
|
+
withTimeout
|
|
38
|
+
} from "./resilience.js";
|
|
39
|
+
import {
|
|
40
|
+
deserializeError,
|
|
41
|
+
safeParse,
|
|
42
|
+
safeStringify,
|
|
43
|
+
serializeError
|
|
44
|
+
} from "./serialization.js";
|
|
45
|
+
import {
|
|
46
|
+
DEFAULT_PATTERNS,
|
|
47
|
+
DEFAULT_SENSITIVE_KEYS,
|
|
48
|
+
createRedactor
|
|
49
|
+
} from "./redactor.js";
|
|
50
|
+
import {
|
|
51
|
+
AssertionError,
|
|
52
|
+
AuthError,
|
|
53
|
+
CancelledError,
|
|
54
|
+
ConflictError,
|
|
55
|
+
InternalError,
|
|
56
|
+
NetworkError,
|
|
57
|
+
NotFoundError,
|
|
58
|
+
PermissionError,
|
|
59
|
+
RateLimitError,
|
|
60
|
+
TimeoutError,
|
|
61
|
+
ValidationError,
|
|
62
|
+
exitCodeMap,
|
|
63
|
+
getExitCode,
|
|
64
|
+
getStatusCode,
|
|
65
|
+
statusCodeMap
|
|
66
|
+
} from "./errors.js";
|
|
67
|
+
import {
|
|
68
|
+
createContext,
|
|
69
|
+
generateRequestId
|
|
70
|
+
} from "./context.js";
|
|
71
|
+
import {
|
|
72
|
+
ACTION_CAPABILITIES,
|
|
73
|
+
CAPABILITY_SURFACES,
|
|
74
|
+
DEFAULT_ACTION_SURFACES,
|
|
75
|
+
capability,
|
|
76
|
+
capabilityAll,
|
|
77
|
+
getActionsForSurface
|
|
78
|
+
} from "./capabilities.js";
|
|
79
|
+
|
|
80
|
+
// packages/contracts/src/index.ts
|
|
81
|
+
import { Result, TaggedError } from "better-result";
|
|
82
|
+
export {
|
|
83
|
+
withTimeout,
|
|
84
|
+
validateInput,
|
|
85
|
+
unwrapOrElse,
|
|
86
|
+
toHttpResponse,
|
|
87
|
+
toEnvelope,
|
|
88
|
+
statusCodeMap,
|
|
89
|
+
shouldRetry,
|
|
90
|
+
serializeError,
|
|
91
|
+
safeStringify,
|
|
92
|
+
safeParse,
|
|
93
|
+
retry,
|
|
94
|
+
orElse,
|
|
95
|
+
isRetryable,
|
|
96
|
+
isRecoverable,
|
|
97
|
+
isNonEmptyArray,
|
|
98
|
+
getStatusCode,
|
|
99
|
+
getExitCode,
|
|
100
|
+
getBackoffDelay,
|
|
101
|
+
getActionsForSurface,
|
|
102
|
+
generateRequestId,
|
|
103
|
+
exitCodeMap,
|
|
104
|
+
deserializeError,
|
|
105
|
+
defineAction,
|
|
106
|
+
createValidator,
|
|
107
|
+
createRedactor,
|
|
108
|
+
createContext,
|
|
109
|
+
createActionRegistry,
|
|
110
|
+
combine3,
|
|
111
|
+
combine2,
|
|
112
|
+
capabilityAll,
|
|
113
|
+
capability,
|
|
114
|
+
assertNonEmpty,
|
|
115
|
+
assertMatches,
|
|
116
|
+
assertDefined,
|
|
117
|
+
ValidationError,
|
|
118
|
+
TimeoutError,
|
|
119
|
+
TaggedError,
|
|
120
|
+
Result,
|
|
121
|
+
RateLimitError,
|
|
122
|
+
PermissionError,
|
|
123
|
+
NotFoundError,
|
|
124
|
+
NetworkError,
|
|
125
|
+
InternalError,
|
|
126
|
+
DEFAULT_SENSITIVE_KEYS,
|
|
127
|
+
DEFAULT_REGISTRY_SURFACES,
|
|
128
|
+
DEFAULT_PATTERNS,
|
|
129
|
+
DEFAULT_ACTION_SURFACES,
|
|
130
|
+
ConflictError,
|
|
131
|
+
CancelledError,
|
|
132
|
+
CAPABILITY_SURFACES,
|
|
133
|
+
AuthError,
|
|
134
|
+
AssertionError,
|
|
135
|
+
ACTION_SURFACES,
|
|
136
|
+
ACTION_CAPABILITIES
|
|
137
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error categories for classification, exit codes, and HTTP status mapping.
|
|
3
|
+
*
|
|
4
|
+
* Used for:
|
|
5
|
+
* - CLI exit code determination
|
|
6
|
+
* - HTTP status code mapping
|
|
7
|
+
* - Error grouping in logs and metrics
|
|
8
|
+
* - Client retry decisions (transient vs permanent)
|
|
9
|
+
*/
|
|
10
|
+
type ErrorCategory = "validation" | "not_found" | "conflict" | "permission" | "timeout" | "rate_limit" | "network" | "internal" | "auth" | "cancelled";
|
|
11
|
+
/**
|
|
12
|
+
* Backoff strategy configuration options
|
|
13
|
+
*/
|
|
14
|
+
interface BackoffOptions {
|
|
15
|
+
/** Base delay in milliseconds (default: 100) */
|
|
16
|
+
baseDelayMs?: number;
|
|
17
|
+
/** Maximum delay cap in milliseconds (default: 30000) */
|
|
18
|
+
maxDelayMs?: number;
|
|
19
|
+
/** Backoff strategy (default: "exponential") */
|
|
20
|
+
strategy?: "linear" | "exponential" | "constant";
|
|
21
|
+
/** Whether to add jitter to prevent thundering herd (default: true) */
|
|
22
|
+
useJitter?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Determines if an error is potentially recoverable.
|
|
26
|
+
*
|
|
27
|
+
* Recoverable errors might succeed on retry or with user intervention.
|
|
28
|
+
* This includes transient failures (network, timeout), rate limiting,
|
|
29
|
+
* and optimistic lock conflicts.
|
|
30
|
+
*
|
|
31
|
+
* @param error - Error object with category property
|
|
32
|
+
* @returns True if the error might be recoverable
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```typescript
|
|
36
|
+
* import { isRecoverable, NetworkError } from '@outfitter/contracts';
|
|
37
|
+
*
|
|
38
|
+
* const networkError = new NetworkError({ message: "Connection refused" });
|
|
39
|
+
* console.log(isRecoverable(networkError)); // true
|
|
40
|
+
*
|
|
41
|
+
* const validationError = new ValidationError({ message: "Invalid input" });
|
|
42
|
+
* console.log(isRecoverable(validationError)); // false
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare const isRecoverable: (error: {
|
|
46
|
+
readonly category: ErrorCategory;
|
|
47
|
+
}) => boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Determines if an error should trigger automatic retry.
|
|
50
|
+
*
|
|
51
|
+
* More restrictive than isRecoverable - only transient failures that
|
|
52
|
+
* are good candidates for immediate retry without user intervention.
|
|
53
|
+
*
|
|
54
|
+
* Retryable categories:
|
|
55
|
+
* - network: Connection issues may be temporary
|
|
56
|
+
* - timeout: May succeed with another attempt
|
|
57
|
+
*
|
|
58
|
+
* NOT retryable (even though recoverable):
|
|
59
|
+
* - rate_limit: Should respect retryAfterSeconds header
|
|
60
|
+
* - conflict: May need user to resolve the conflict
|
|
61
|
+
*
|
|
62
|
+
* @param error - Error object with category property
|
|
63
|
+
* @returns True if the operation should be automatically retried
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```typescript
|
|
67
|
+
* import { isRetryable, TimeoutError } from '@outfitter/contracts';
|
|
68
|
+
*
|
|
69
|
+
* const timeout = new TimeoutError({
|
|
70
|
+
* message: "Operation timed out",
|
|
71
|
+
* operation: "fetch",
|
|
72
|
+
* timeoutMs: 5000,
|
|
73
|
+
* });
|
|
74
|
+
* console.log(isRetryable(timeout)); // true
|
|
75
|
+
*
|
|
76
|
+
* const rateLimitError = new RateLimitError({ message: "Rate limit exceeded" });
|
|
77
|
+
* console.log(isRetryable(rateLimitError)); // false (use retryAfterSeconds)
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare const isRetryable: (error: {
|
|
81
|
+
readonly category: ErrorCategory;
|
|
82
|
+
}) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Calculate appropriate backoff delay for retry.
|
|
85
|
+
*
|
|
86
|
+
* Supports three strategies:
|
|
87
|
+
* - exponential (default): delay = baseDelayMs * 2^attempt
|
|
88
|
+
* - linear: delay = baseDelayMs * (attempt + 1)
|
|
89
|
+
* - constant: delay = baseDelayMs
|
|
90
|
+
*
|
|
91
|
+
* By default, adds jitter (+/-10%) to prevent thundering herd problems
|
|
92
|
+
* when multiple clients retry simultaneously.
|
|
93
|
+
*
|
|
94
|
+
* @param attempt - The attempt number (0-indexed)
|
|
95
|
+
* @param options - Backoff configuration options
|
|
96
|
+
* @returns Delay in milliseconds before next retry
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* import { getBackoffDelay } from '@outfitter/contracts';
|
|
101
|
+
*
|
|
102
|
+
* // Exponential backoff (default): 100ms, 200ms, 400ms, 800ms...
|
|
103
|
+
* const delay = getBackoffDelay(2); // ~400ms (with jitter)
|
|
104
|
+
*
|
|
105
|
+
* // Linear backoff: 100ms, 200ms, 300ms...
|
|
106
|
+
* const linearDelay = getBackoffDelay(2, { strategy: "linear" }); // ~300ms
|
|
107
|
+
*
|
|
108
|
+
* // Constant delay: 500ms, 500ms, 500ms...
|
|
109
|
+
* const constantDelay = getBackoffDelay(2, {
|
|
110
|
+
* strategy: "constant",
|
|
111
|
+
* baseDelayMs: 500,
|
|
112
|
+
* }); // ~500ms
|
|
113
|
+
*
|
|
114
|
+
* // No jitter for deterministic timing
|
|
115
|
+
* const exactDelay = getBackoffDelay(2, { useJitter: false }); // exactly 400ms
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
declare const getBackoffDelay: (attempt: number, options?: BackoffOptions) => number;
|
|
119
|
+
/**
|
|
120
|
+
* Convenience function combining retryability check with attempt limit.
|
|
121
|
+
*
|
|
122
|
+
* Returns true only if:
|
|
123
|
+
* 1. The error is retryable (network or timeout)
|
|
124
|
+
* 2. We haven't exceeded maxAttempts
|
|
125
|
+
*
|
|
126
|
+
* @param error - Error object with category property
|
|
127
|
+
* @param attempt - Current attempt number (0-indexed)
|
|
128
|
+
* @param maxAttempts - Maximum number of retry attempts (default: 3)
|
|
129
|
+
* @returns True if the operation should be retried
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* import { shouldRetry, NetworkError } from '@outfitter/contracts';
|
|
134
|
+
*
|
|
135
|
+
* const error = new NetworkError({ message: "Connection timeout" });
|
|
136
|
+
*
|
|
137
|
+
* // First attempt failed
|
|
138
|
+
* console.log(shouldRetry(error, 0)); // true (will retry)
|
|
139
|
+
*
|
|
140
|
+
* // Fourth attempt failed (exceeded default max of 3)
|
|
141
|
+
* console.log(shouldRetry(error, 3)); // false (no more retries)
|
|
142
|
+
*
|
|
143
|
+
* // Custom max attempts
|
|
144
|
+
* console.log(shouldRetry(error, 4, 5)); // true (under custom limit)
|
|
145
|
+
* ```
|
|
146
|
+
*/
|
|
147
|
+
declare const shouldRetry: (error: {
|
|
148
|
+
readonly category: ErrorCategory;
|
|
149
|
+
}, attempt: number, maxAttempts?: number) => boolean;
|
|
150
|
+
export { shouldRetry, isRetryable, isRecoverable, getBackoffDelay, BackoffOptions };
|
package/dist/recovery.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/contracts/src/recovery.ts
|
|
3
|
+
var RECOVERABLE_CATEGORIES = [
|
|
4
|
+
"network",
|
|
5
|
+
"timeout",
|
|
6
|
+
"rate_limit",
|
|
7
|
+
"conflict"
|
|
8
|
+
];
|
|
9
|
+
var RETRYABLE_CATEGORIES = ["network", "timeout"];
|
|
10
|
+
var isRecoverable = (error) => {
|
|
11
|
+
return RECOVERABLE_CATEGORIES.includes(error.category);
|
|
12
|
+
};
|
|
13
|
+
var isRetryable = (error) => {
|
|
14
|
+
return RETRYABLE_CATEGORIES.includes(error.category);
|
|
15
|
+
};
|
|
16
|
+
var getBackoffDelay = (attempt, options = {}) => {
|
|
17
|
+
const {
|
|
18
|
+
baseDelayMs = 100,
|
|
19
|
+
maxDelayMs = 30000,
|
|
20
|
+
strategy = "exponential",
|
|
21
|
+
useJitter = true
|
|
22
|
+
} = options;
|
|
23
|
+
let delay;
|
|
24
|
+
switch (strategy) {
|
|
25
|
+
case "constant": {
|
|
26
|
+
delay = baseDelayMs;
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "linear": {
|
|
30
|
+
delay = baseDelayMs * (attempt + 1);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
default: {
|
|
34
|
+
delay = baseDelayMs * 2 ** attempt;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
delay = Math.min(delay, maxDelayMs);
|
|
38
|
+
if (useJitter) {
|
|
39
|
+
const jitterFactor = 0.1;
|
|
40
|
+
const jitter = delay * jitterFactor * (Math.random() * 2 - 1);
|
|
41
|
+
delay = Math.round(delay + jitter);
|
|
42
|
+
}
|
|
43
|
+
return Math.min(delay, maxDelayMs);
|
|
44
|
+
};
|
|
45
|
+
var shouldRetry = (error, attempt, maxAttempts = 3) => {
|
|
46
|
+
if (attempt >= maxAttempts) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return isRetryable(error);
|
|
50
|
+
};
|
|
51
|
+
export {
|
|
52
|
+
shouldRetry,
|
|
53
|
+
isRetryable,
|
|
54
|
+
isRecoverable,
|
|
55
|
+
getBackoffDelay
|
|
56
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for creating a redactor.
|
|
3
|
+
*/
|
|
4
|
+
interface RedactorConfig {
|
|
5
|
+
/** Regex patterns to match and redact */
|
|
6
|
+
patterns: RegExp[];
|
|
7
|
+
/** Object keys whose values should always be redacted */
|
|
8
|
+
keys: string[];
|
|
9
|
+
/** Replacement string (default: "[REDACTED]") */
|
|
10
|
+
replacement?: string;
|
|
11
|
+
/** Whether to redact recursively in nested objects (default: true) */
|
|
12
|
+
deep?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Redaction event for audit logging.
|
|
16
|
+
*/
|
|
17
|
+
interface RedactionEvent {
|
|
18
|
+
/** Type of redaction applied */
|
|
19
|
+
redactedBy: "pattern" | "key";
|
|
20
|
+
/** Identifier of the pattern/key that matched */
|
|
21
|
+
matcher: string;
|
|
22
|
+
/** Location in the object path (e.g., "config.auth.apiKey") */
|
|
23
|
+
path: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Callback for redaction events.
|
|
27
|
+
*/
|
|
28
|
+
type RedactionCallback = (event: RedactionEvent) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Redactor - sensitive data scrubbing for logs, errors, and output.
|
|
31
|
+
*
|
|
32
|
+
* Applied automatically by @outfitter/logging. Manual application
|
|
33
|
+
* required when building custom output or error context.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const redactor = createRedactor({
|
|
38
|
+
* patterns: [
|
|
39
|
+
* /Bearer [A-Za-z0-9-_]+/g, // Auth headers
|
|
40
|
+
* /sk-[A-Za-z0-9]{48}/g, // OpenAI keys
|
|
41
|
+
* /ghp_[A-Za-z0-9]{36}/g, // GitHub PATs
|
|
42
|
+
* /password[:=]\s*["']?[^"'\s]+/gi, // Password fields
|
|
43
|
+
* ],
|
|
44
|
+
* keys: ["apiKey", "secret", "token", "password", "credential"],
|
|
45
|
+
* replacement: "[REDACTED]",
|
|
46
|
+
* });
|
|
47
|
+
*
|
|
48
|
+
* const safeLog = redactor.redact(sensitiveObject);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
interface Redactor {
|
|
52
|
+
/** Redact sensitive values from an object (deep) */
|
|
53
|
+
redact<T>(value: T): T;
|
|
54
|
+
/** Redact sensitive values from a string */
|
|
55
|
+
redactString(value: string): string;
|
|
56
|
+
/** Check if a key name is sensitive */
|
|
57
|
+
isSensitiveKey(key: string): boolean;
|
|
58
|
+
/** Add a pattern at runtime */
|
|
59
|
+
addPattern(pattern: RegExp): void;
|
|
60
|
+
/** Add a sensitive key at runtime */
|
|
61
|
+
addSensitiveKey(key: string): void;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Default patterns for common secrets.
|
|
65
|
+
*
|
|
66
|
+
* Covers:
|
|
67
|
+
* - API keys (OpenAI, Anthropic, GitHub, etc.)
|
|
68
|
+
* - Auth headers (Bearer tokens)
|
|
69
|
+
* - Connection strings (database URLs)
|
|
70
|
+
* - Password fields in various formats
|
|
71
|
+
*/
|
|
72
|
+
declare const DEFAULT_PATTERNS: RegExp[];
|
|
73
|
+
/**
|
|
74
|
+
* Default sensitive key names.
|
|
75
|
+
*
|
|
76
|
+
* Object keys matching these (case-insensitive) will have their values redacted.
|
|
77
|
+
*/
|
|
78
|
+
declare const DEFAULT_SENSITIVE_KEYS: string[];
|
|
79
|
+
/**
|
|
80
|
+
* Create a redactor instance with the given configuration.
|
|
81
|
+
*
|
|
82
|
+
* @param config - Redactor configuration
|
|
83
|
+
* @returns Configured Redactor instance
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const redactor = createRedactor({
|
|
88
|
+
* patterns: [...DEFAULT_PATTERNS],
|
|
89
|
+
* keys: [...DEFAULT_SENSITIVE_KEYS],
|
|
90
|
+
* });
|
|
91
|
+
*
|
|
92
|
+
* const safe = redactor.redact({
|
|
93
|
+
* user: "alice",
|
|
94
|
+
* apiKey: "sk-abc123...",
|
|
95
|
+
* });
|
|
96
|
+
* // { user: "alice", apiKey: "[REDACTED]" }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
declare function createRedactor(config: RedactorConfig): Redactor;
|
|
100
|
+
export { createRedactor, RedactorConfig, Redactor, RedactionEvent, RedactionCallback, DEFAULT_SENSITIVE_KEYS, DEFAULT_PATTERNS };
|
package/dist/redactor.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/contracts/src/redactor.ts
|
|
3
|
+
var DEFAULT_PATTERNS = [
|
|
4
|
+
/Bearer [A-Za-z0-9-_.]+/g,
|
|
5
|
+
/Basic [A-Za-z0-9+/=]+/g,
|
|
6
|
+
/sk-[A-Za-z0-9]{32,}/g,
|
|
7
|
+
/sk-ant-[A-Za-z0-9-_]{32,}/g,
|
|
8
|
+
/ghp_[A-Za-z0-9]{36,}/g,
|
|
9
|
+
/gho_[A-Za-z0-9]{36}/g,
|
|
10
|
+
/ghu_[A-Za-z0-9]{36}/g,
|
|
11
|
+
/ghs_[A-Za-z0-9]{36}/g,
|
|
12
|
+
/ghr_[A-Za-z0-9]{36}/g,
|
|
13
|
+
/github_pat_[A-Za-z0-9_]{22,}/g,
|
|
14
|
+
/AKIA[A-Z0-9]{16}/g,
|
|
15
|
+
/xox[baprs]-[A-Za-z0-9-]+/g,
|
|
16
|
+
/-----BEGIN [A-Z ]+ KEY-----/g,
|
|
17
|
+
/password[:=]\s*["']?[^"'\s]+/gi,
|
|
18
|
+
/secret[:=]\s*["']?[^"'\s]+/gi,
|
|
19
|
+
/(?:postgres|mysql|mongodb|redis):\/\/[^@\s]+@[^\s]+/g
|
|
20
|
+
];
|
|
21
|
+
var DEFAULT_SENSITIVE_KEYS = [
|
|
22
|
+
"apiKey",
|
|
23
|
+
"api_key",
|
|
24
|
+
"apikey",
|
|
25
|
+
"secret",
|
|
26
|
+
"secretKey",
|
|
27
|
+
"secret_key",
|
|
28
|
+
"token",
|
|
29
|
+
"accessToken",
|
|
30
|
+
"access_token",
|
|
31
|
+
"refreshToken",
|
|
32
|
+
"refresh_token",
|
|
33
|
+
"password",
|
|
34
|
+
"passwd",
|
|
35
|
+
"credential",
|
|
36
|
+
"credentials",
|
|
37
|
+
"private",
|
|
38
|
+
"privateKey",
|
|
39
|
+
"private_key",
|
|
40
|
+
"authorization",
|
|
41
|
+
"auth"
|
|
42
|
+
];
|
|
43
|
+
function createRedactor(config) {
|
|
44
|
+
const replacement = config.replacement ?? "[REDACTED]";
|
|
45
|
+
const deep = config.deep ?? true;
|
|
46
|
+
const patterns = [...config.patterns];
|
|
47
|
+
const sensitiveKeys = new Set(config.keys.map((k) => k.toLowerCase()));
|
|
48
|
+
function isSensitiveKey(key) {
|
|
49
|
+
return sensitiveKeys.has(key.toLowerCase());
|
|
50
|
+
}
|
|
51
|
+
function redactString(value) {
|
|
52
|
+
let result = value;
|
|
53
|
+
for (const pattern of patterns) {
|
|
54
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
55
|
+
result = result.replace(regex, replacement);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function redact(value) {
|
|
60
|
+
return redactValue(value, deep);
|
|
61
|
+
}
|
|
62
|
+
function redactValue(value, deepMode) {
|
|
63
|
+
if (value === null || value === undefined) {
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === "string") {
|
|
67
|
+
return redactString(value);
|
|
68
|
+
}
|
|
69
|
+
if (Array.isArray(value)) {
|
|
70
|
+
if (!deepMode) {
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
return value.map((item) => redactValue(item, deepMode));
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === "object") {
|
|
76
|
+
if (!deepMode) {
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
const result = {};
|
|
80
|
+
for (const [key, val] of Object.entries(value)) {
|
|
81
|
+
if (isSensitiveKey(key) && val !== null && val !== undefined) {
|
|
82
|
+
result[key] = replacement;
|
|
83
|
+
} else if (typeof val === "string") {
|
|
84
|
+
result[key] = redactString(val);
|
|
85
|
+
} else {
|
|
86
|
+
result[key] = redactValue(val, deepMode);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
function addPattern(pattern) {
|
|
94
|
+
patterns.push(pattern);
|
|
95
|
+
}
|
|
96
|
+
function addSensitiveKey(key) {
|
|
97
|
+
sensitiveKeys.add(key.toLowerCase());
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
redact,
|
|
101
|
+
redactString,
|
|
102
|
+
isSensitiveKey,
|
|
103
|
+
addPattern,
|
|
104
|
+
addSensitiveKey
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
createRedactor,
|
|
109
|
+
DEFAULT_SENSITIVE_KEYS,
|
|
110
|
+
DEFAULT_PATTERNS
|
|
111
|
+
};
|