@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/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 };
@@ -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 };
@@ -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
+ };