@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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { TaggedErrorClass } from "better-result";
|
|
2
|
+
declare const AssertionErrorBase: TaggedErrorClass<"AssertionError", {
|
|
3
|
+
message: string;
|
|
4
|
+
}>;
|
|
5
|
+
/**
|
|
6
|
+
* Assertion failed (invariant violation).
|
|
7
|
+
*
|
|
8
|
+
* Used by assertion utilities that return Result types instead of throwing.
|
|
9
|
+
* AssertionError indicates a programming bug — an invariant that should
|
|
10
|
+
* never be violated was broken. These are internal errors, not user input
|
|
11
|
+
* validation failures.
|
|
12
|
+
*
|
|
13
|
+
* **Category rationale**: Uses `internal` (not `validation`) because:
|
|
14
|
+
* - Assertions check **invariants** (programmer assumptions), not user input
|
|
15
|
+
* - A failed assertion means "this should be impossible if the code is correct"
|
|
16
|
+
* - User-facing validation uses {@link ValidationError} with helpful field info
|
|
17
|
+
* - HTTP 500 is correct: this is a server bug, not a client mistake
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* // In domain logic after validation has passed
|
|
22
|
+
* const result = assertDefined(cachedValue, "Cache should always have value after init");
|
|
23
|
+
* if (result.isErr()) {
|
|
24
|
+
* return result; // Propagate as internal error
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @see ValidationError - For user input validation failures (HTTP 400)
|
|
29
|
+
*/
|
|
30
|
+
declare class AssertionError extends AssertionErrorBase {
|
|
31
|
+
readonly category: "internal";
|
|
32
|
+
exitCode(): number;
|
|
33
|
+
statusCode(): number;
|
|
34
|
+
}
|
|
35
|
+
import { Result } from "better-result";
|
|
36
|
+
/**
|
|
37
|
+
* Array type guaranteed to have at least one element.
|
|
38
|
+
*/
|
|
39
|
+
type NonEmptyArray<T> = [T, ...T[]];
|
|
40
|
+
/**
|
|
41
|
+
* Type guard for NonEmptyArray.
|
|
42
|
+
*/
|
|
43
|
+
declare const isNonEmptyArray: <T>(arr: readonly T[]) => arr is NonEmptyArray<T>;
|
|
44
|
+
/**
|
|
45
|
+
* Assert a value is defined (not null or undefined).
|
|
46
|
+
* Returns Result instead of throwing.
|
|
47
|
+
*/
|
|
48
|
+
declare const assertDefined: <T>(value: T | null | undefined, message?: string) => Result<T, InstanceType<typeof AssertionError>>;
|
|
49
|
+
/**
|
|
50
|
+
* Assert array has at least one element.
|
|
51
|
+
* Returns NonEmptyArray on success.
|
|
52
|
+
*/
|
|
53
|
+
declare const assertNonEmpty: <T>(arr: readonly T[], message?: string) => Result<NonEmptyArray<T>, InstanceType<typeof AssertionError>>;
|
|
54
|
+
/**
|
|
55
|
+
* Assert value matches a predicate.
|
|
56
|
+
* Supports type guard predicates for narrowing.
|
|
57
|
+
*/
|
|
58
|
+
declare function assertMatches<
|
|
59
|
+
T,
|
|
60
|
+
U extends T
|
|
61
|
+
>(value: T, predicate: (v: T) => v is U, message?: string): Result<U, InstanceType<typeof AssertionError>>;
|
|
62
|
+
declare function assertMatches<T>(value: T, predicate: (v: T) => boolean, message?: string): Result<T, InstanceType<typeof AssertionError>>;
|
|
63
|
+
export { isNonEmptyArray, assertNonEmpty, assertMatches, assertDefined, NonEmptyArray };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
AssertionError
|
|
4
|
+
} from "../errors.js";
|
|
5
|
+
|
|
6
|
+
// packages/contracts/src/assert/index.ts
|
|
7
|
+
import { Result } from "better-result";
|
|
8
|
+
var isNonEmptyArray = (arr) => {
|
|
9
|
+
return arr.length > 0;
|
|
10
|
+
};
|
|
11
|
+
var assertDefined = (value, message) => {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return Result.err(new AssertionError({ message: message ?? "Value is null or undefined" }));
|
|
14
|
+
}
|
|
15
|
+
return Result.ok(value);
|
|
16
|
+
};
|
|
17
|
+
var assertNonEmpty = (arr, message) => {
|
|
18
|
+
if (arr.length === 0) {
|
|
19
|
+
return Result.err(new AssertionError({ message: message ?? "Array is empty" }));
|
|
20
|
+
}
|
|
21
|
+
return Result.ok(arr);
|
|
22
|
+
};
|
|
23
|
+
function assertMatches(value, predicate, message) {
|
|
24
|
+
if (!predicate(value)) {
|
|
25
|
+
return Result.err(new AssertionError({
|
|
26
|
+
message: message ?? "Value does not match predicate"
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
return Result.ok(value);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
isNonEmptyArray,
|
|
33
|
+
assertNonEmpty,
|
|
34
|
+
assertMatches,
|
|
35
|
+
assertDefined
|
|
36
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @outfitter/contracts - Capability manifest
|
|
3
|
+
*
|
|
4
|
+
* Shared action capability manifest for CLI/MCP/API/server parity.
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
declare const CAPABILITY_SURFACES: readonly ["cli", "mcp", "api", "server"];
|
|
9
|
+
type CapabilitySurface = (typeof CAPABILITY_SURFACES)[number];
|
|
10
|
+
interface ActionCapability {
|
|
11
|
+
surfaces: readonly CapabilitySurface[];
|
|
12
|
+
notes?: string;
|
|
13
|
+
}
|
|
14
|
+
declare const DEFAULT_ACTION_SURFACES: readonly ["cli", "mcp"];
|
|
15
|
+
declare function capability(surfaces?: readonly CapabilitySurface[], notes?: string): ActionCapability;
|
|
16
|
+
declare function capabilityAll(notes?: string): ActionCapability;
|
|
17
|
+
declare const ACTION_CAPABILITIES: Record<string, ActionCapability>;
|
|
18
|
+
declare function getActionsForSurface(surface: CapabilitySurface): string[];
|
|
19
|
+
export { getActionsForSurface, capabilityAll, capability, DEFAULT_ACTION_SURFACES, CapabilitySurface, CAPABILITY_SURFACES, ActionCapability, ACTION_CAPABILITIES };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/contracts/src/capabilities.ts
|
|
3
|
+
var CAPABILITY_SURFACES = ["cli", "mcp", "api", "server"];
|
|
4
|
+
var DEFAULT_ACTION_SURFACES = ["cli", "mcp"];
|
|
5
|
+
function capability(surfaces = DEFAULT_ACTION_SURFACES, notes) {
|
|
6
|
+
return notes ? { surfaces, notes } : { surfaces };
|
|
7
|
+
}
|
|
8
|
+
function capabilityAll(notes) {
|
|
9
|
+
return capability(CAPABILITY_SURFACES, notes);
|
|
10
|
+
}
|
|
11
|
+
var ACTION_CAPABILITIES = {
|
|
12
|
+
navigate: capability(),
|
|
13
|
+
back: capability(),
|
|
14
|
+
forward: capability(),
|
|
15
|
+
reload: capability(),
|
|
16
|
+
tab: capability(),
|
|
17
|
+
tabs: capability(),
|
|
18
|
+
newTab: capability(),
|
|
19
|
+
closeTab: capability(),
|
|
20
|
+
click: capability(),
|
|
21
|
+
type: capability(),
|
|
22
|
+
select: capability(),
|
|
23
|
+
hover: capability(),
|
|
24
|
+
focus: capability(["mcp"]),
|
|
25
|
+
scroll: capability(),
|
|
26
|
+
press: capability(),
|
|
27
|
+
fill: capability(),
|
|
28
|
+
find: capability(),
|
|
29
|
+
check: capability(),
|
|
30
|
+
uncheck: capability(),
|
|
31
|
+
upload: capability(),
|
|
32
|
+
download: capability(["server"], "Server-only for now"),
|
|
33
|
+
dialog: capability(),
|
|
34
|
+
waitFor: capability(["mcp"]),
|
|
35
|
+
waitForNavigation: capability(["mcp"]),
|
|
36
|
+
wait: capability(["mcp"]),
|
|
37
|
+
snap: capability(),
|
|
38
|
+
screenshot: capability(),
|
|
39
|
+
html: capability(["mcp"]),
|
|
40
|
+
text: capability(["mcp"]),
|
|
41
|
+
marker: capability(),
|
|
42
|
+
markers: capability(),
|
|
43
|
+
markerGet: capability(),
|
|
44
|
+
markerRead: capability(["mcp"]),
|
|
45
|
+
markerCompare: capability(),
|
|
46
|
+
markerDelete: capability(),
|
|
47
|
+
markerResolve: capability(["cli"], "CLI-only for now"),
|
|
48
|
+
viewport: capability(),
|
|
49
|
+
colorScheme: capability(),
|
|
50
|
+
mode: capability(["mcp"]),
|
|
51
|
+
evaluate: capability(["mcp"]),
|
|
52
|
+
session: capability(),
|
|
53
|
+
sessions: capability(["mcp"]),
|
|
54
|
+
steps: capability()
|
|
55
|
+
};
|
|
56
|
+
function getActionsForSurface(surface) {
|
|
57
|
+
return Object.entries(ACTION_CAPABILITIES).filter(([, capability2]) => capability2.surfaces.includes(surface)).map(([action]) => action);
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
getActionsForSurface,
|
|
61
|
+
capabilityAll,
|
|
62
|
+
capability,
|
|
63
|
+
DEFAULT_ACTION_SURFACES,
|
|
64
|
+
CAPABILITY_SURFACES,
|
|
65
|
+
ACTION_CAPABILITIES
|
|
66
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger interface for handler context.
|
|
3
|
+
* Implementations provided by @outfitter/logging.
|
|
4
|
+
*
|
|
5
|
+
* All log methods accept an optional context object that will be merged
|
|
6
|
+
* with any context inherited from parent loggers created via `child()`.
|
|
7
|
+
*/
|
|
8
|
+
interface Logger {
|
|
9
|
+
trace(message: string, metadata?: Record<string, unknown>): void;
|
|
10
|
+
debug(message: string, metadata?: Record<string, unknown>): void;
|
|
11
|
+
info(message: string, metadata?: Record<string, unknown>): void;
|
|
12
|
+
warn(message: string, metadata?: Record<string, unknown>): void;
|
|
13
|
+
error(message: string, metadata?: Record<string, unknown>): void;
|
|
14
|
+
fatal(message: string, metadata?: Record<string, unknown>): void;
|
|
15
|
+
/**
|
|
16
|
+
* Creates a child logger with additional context.
|
|
17
|
+
*
|
|
18
|
+
* Context from the child is merged with the parent's context,
|
|
19
|
+
* with child context taking precedence for duplicate keys.
|
|
20
|
+
* Child loggers are composable (can create nested children).
|
|
21
|
+
*
|
|
22
|
+
* @param context - Additional context to include in all log messages
|
|
23
|
+
* @returns A new Logger instance with the merged context
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const requestLogger = ctx.logger.child({ requestId: ctx.requestId });
|
|
28
|
+
* requestLogger.info("Processing request"); // includes requestId
|
|
29
|
+
*
|
|
30
|
+
* const opLogger = requestLogger.child({ operation: "create" });
|
|
31
|
+
* opLogger.debug("Starting"); // includes requestId + operation
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
child(context: Record<string, unknown>): Logger;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolved configuration interface.
|
|
38
|
+
* Implementations provided by @outfitter/config.
|
|
39
|
+
*/
|
|
40
|
+
interface ResolvedConfig {
|
|
41
|
+
get<T>(key: string): T | undefined;
|
|
42
|
+
getRequired<T>(key: string): T;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Handler context - provides cross-cutting concerns without polluting handler signatures.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* const handler: Handler<Input, Output, NotFoundError> = async (input, ctx) => {
|
|
50
|
+
* ctx.logger.debug("Processing request", { requestId: ctx.requestId });
|
|
51
|
+
* // ... handler logic
|
|
52
|
+
* };
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
interface HandlerContext {
|
|
56
|
+
/** Abort signal for cancellation propagation */
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
/** Unique request identifier for tracing (UUIDv7) */
|
|
59
|
+
requestId: string;
|
|
60
|
+
/** Structured logger with automatic redaction */
|
|
61
|
+
logger: Logger;
|
|
62
|
+
/** Resolved configuration values */
|
|
63
|
+
config?: ResolvedConfig;
|
|
64
|
+
/** Workspace root path, if detected */
|
|
65
|
+
workspaceRoot?: string;
|
|
66
|
+
/** Current working directory */
|
|
67
|
+
cwd: string;
|
|
68
|
+
/** Environment variables (filtered, redacted) */
|
|
69
|
+
env: Record<string, string | undefined>;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Options for creating a handler context.
|
|
73
|
+
*/
|
|
74
|
+
interface CreateContextOptions {
|
|
75
|
+
/** Logger instance (uses no-op logger if not provided) */
|
|
76
|
+
logger?: Logger;
|
|
77
|
+
/** Resolved configuration */
|
|
78
|
+
config?: ResolvedConfig;
|
|
79
|
+
/** Abort signal for cancellation */
|
|
80
|
+
signal?: AbortSignal;
|
|
81
|
+
/** Explicit request ID (generates UUIDv7 if not provided) */
|
|
82
|
+
requestId?: string;
|
|
83
|
+
/** Workspace root path */
|
|
84
|
+
workspaceRoot?: string;
|
|
85
|
+
/** Current working directory (defaults to process.cwd()) */
|
|
86
|
+
cwd?: string;
|
|
87
|
+
/** Environment variables to include */
|
|
88
|
+
env?: Record<string, string | undefined>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create a HandlerContext for a new request.
|
|
92
|
+
*
|
|
93
|
+
* Auto-generates requestId using Bun.randomUUIDv7() if not provided.
|
|
94
|
+
*
|
|
95
|
+
* @param options - Context configuration options
|
|
96
|
+
* @returns Fully populated HandlerContext
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```typescript
|
|
100
|
+
* const ctx = createContext({
|
|
101
|
+
* logger: createLogger(),
|
|
102
|
+
* config: resolvedConfig,
|
|
103
|
+
* signal: controller.signal,
|
|
104
|
+
* });
|
|
105
|
+
*
|
|
106
|
+
* const result = await handler(input, ctx);
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
declare function createContext(options: CreateContextOptions): HandlerContext;
|
|
110
|
+
/**
|
|
111
|
+
* Generate a sortable request ID (UUIDv7).
|
|
112
|
+
*
|
|
113
|
+
* UUIDv7 is time-ordered, making it ideal for request tracing
|
|
114
|
+
* as IDs sort chronologically.
|
|
115
|
+
*
|
|
116
|
+
* @returns UUIDv7 string
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* const requestId = generateRequestId();
|
|
121
|
+
* // "018e4f3c-1a2b-7000-8000-000000000001"
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
declare function generateRequestId(): string;
|
|
125
|
+
export { generateRequestId, createContext, CreateContextOptions };
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/contracts/src/context.ts
|
|
3
|
+
var noopLogger = {
|
|
4
|
+
trace: () => {},
|
|
5
|
+
debug: () => {},
|
|
6
|
+
info: () => {},
|
|
7
|
+
warn: () => {},
|
|
8
|
+
error: () => {},
|
|
9
|
+
fatal: () => {},
|
|
10
|
+
child: () => noopLogger
|
|
11
|
+
};
|
|
12
|
+
function createContext(options) {
|
|
13
|
+
const ctx = {
|
|
14
|
+
requestId: options.requestId ?? generateRequestId(),
|
|
15
|
+
logger: options.logger ?? noopLogger,
|
|
16
|
+
cwd: options.cwd ?? process.cwd(),
|
|
17
|
+
env: options.env ?? {}
|
|
18
|
+
};
|
|
19
|
+
if (options.config !== undefined) {
|
|
20
|
+
ctx.config = options.config;
|
|
21
|
+
}
|
|
22
|
+
if (options.signal !== undefined) {
|
|
23
|
+
ctx.signal = options.signal;
|
|
24
|
+
}
|
|
25
|
+
if (options.workspaceRoot !== undefined) {
|
|
26
|
+
ctx.workspaceRoot = options.workspaceRoot;
|
|
27
|
+
}
|
|
28
|
+
return ctx;
|
|
29
|
+
}
|
|
30
|
+
function generateRequestId() {
|
|
31
|
+
return Bun.randomUUIDv7();
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
generateRequestId,
|
|
35
|
+
createContext
|
|
36
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { TaggedErrorClass } from "better-result";
|
|
2
|
+
/**
|
|
3
|
+
* Error categories for classification, exit codes, and HTTP status mapping.
|
|
4
|
+
*
|
|
5
|
+
* Used for:
|
|
6
|
+
* - CLI exit code determination
|
|
7
|
+
* - HTTP status code mapping
|
|
8
|
+
* - Error grouping in logs and metrics
|
|
9
|
+
* - Client retry decisions (transient vs permanent)
|
|
10
|
+
*/
|
|
11
|
+
type ErrorCategory = "validation" | "not_found" | "conflict" | "permission" | "timeout" | "rate_limit" | "network" | "internal" | "auth" | "cancelled";
|
|
12
|
+
/**
|
|
13
|
+
* Serialized error format for JSON transport.
|
|
14
|
+
*/
|
|
15
|
+
interface SerializedError {
|
|
16
|
+
_tag: string;
|
|
17
|
+
category: ErrorCategory;
|
|
18
|
+
message: string;
|
|
19
|
+
context?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
declare const ValidationErrorBase: TaggedErrorClass<"ValidationError", {
|
|
22
|
+
message: string;
|
|
23
|
+
field?: string;
|
|
24
|
+
}>;
|
|
25
|
+
declare const AssertionErrorBase: TaggedErrorClass<"AssertionError", {
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
28
|
+
declare const NotFoundErrorBase: TaggedErrorClass<"NotFoundError", {
|
|
29
|
+
message: string;
|
|
30
|
+
resourceType: string;
|
|
31
|
+
resourceId: string;
|
|
32
|
+
}>;
|
|
33
|
+
declare const ConflictErrorBase: TaggedErrorClass<"ConflictError", {
|
|
34
|
+
message: string;
|
|
35
|
+
context?: Record<string, unknown>;
|
|
36
|
+
}>;
|
|
37
|
+
declare const PermissionErrorBase: TaggedErrorClass<"PermissionError", {
|
|
38
|
+
message: string;
|
|
39
|
+
context?: Record<string, unknown>;
|
|
40
|
+
}>;
|
|
41
|
+
declare const TimeoutErrorBase: TaggedErrorClass<"TimeoutError", {
|
|
42
|
+
message: string;
|
|
43
|
+
operation: string;
|
|
44
|
+
timeoutMs: number;
|
|
45
|
+
}>;
|
|
46
|
+
declare const RateLimitErrorBase: TaggedErrorClass<"RateLimitError", {
|
|
47
|
+
message: string;
|
|
48
|
+
retryAfterSeconds?: number;
|
|
49
|
+
}>;
|
|
50
|
+
declare const NetworkErrorBase: TaggedErrorClass<"NetworkError", {
|
|
51
|
+
message: string;
|
|
52
|
+
context?: Record<string, unknown>;
|
|
53
|
+
}>;
|
|
54
|
+
declare const InternalErrorBase: TaggedErrorClass<"InternalError", {
|
|
55
|
+
message: string;
|
|
56
|
+
context?: Record<string, unknown>;
|
|
57
|
+
}>;
|
|
58
|
+
declare const AuthErrorBase: TaggedErrorClass<"AuthError", {
|
|
59
|
+
message: string;
|
|
60
|
+
reason?: "missing" | "invalid" | "expired";
|
|
61
|
+
}>;
|
|
62
|
+
declare const CancelledErrorBase: TaggedErrorClass<"CancelledError", {
|
|
63
|
+
message: string;
|
|
64
|
+
}>;
|
|
65
|
+
/**
|
|
66
|
+
* Input validation failed.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```typescript
|
|
70
|
+
* new ValidationError({ message: "Email format invalid", field: "email" });
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare class ValidationError extends ValidationErrorBase {
|
|
74
|
+
readonly category: "validation";
|
|
75
|
+
exitCode(): number;
|
|
76
|
+
statusCode(): number;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Assertion failed (invariant violation).
|
|
80
|
+
*
|
|
81
|
+
* Used by assertion utilities that return Result types instead of throwing.
|
|
82
|
+
* AssertionError indicates a programming bug — an invariant that should
|
|
83
|
+
* never be violated was broken. These are internal errors, not user input
|
|
84
|
+
* validation failures.
|
|
85
|
+
*
|
|
86
|
+
* **Category rationale**: Uses `internal` (not `validation`) because:
|
|
87
|
+
* - Assertions check **invariants** (programmer assumptions), not user input
|
|
88
|
+
* - A failed assertion means "this should be impossible if the code is correct"
|
|
89
|
+
* - User-facing validation uses {@link ValidationError} with helpful field info
|
|
90
|
+
* - HTTP 500 is correct: this is a server bug, not a client mistake
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* // In domain logic after validation has passed
|
|
95
|
+
* const result = assertDefined(cachedValue, "Cache should always have value after init");
|
|
96
|
+
* if (result.isErr()) {
|
|
97
|
+
* return result; // Propagate as internal error
|
|
98
|
+
* }
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @see ValidationError - For user input validation failures (HTTP 400)
|
|
102
|
+
*/
|
|
103
|
+
declare class AssertionError extends AssertionErrorBase {
|
|
104
|
+
readonly category: "internal";
|
|
105
|
+
exitCode(): number;
|
|
106
|
+
statusCode(): number;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Requested resource not found.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```typescript
|
|
113
|
+
* new NotFoundError({ message: "note not found: abc123", resourceType: "note", resourceId: "abc123" });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
declare class NotFoundError extends NotFoundErrorBase {
|
|
117
|
+
readonly category: "not_found";
|
|
118
|
+
exitCode(): number;
|
|
119
|
+
statusCode(): number;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* State conflict (optimistic locking, concurrent modification).
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```typescript
|
|
126
|
+
* new ConflictError({ message: "Resource was modified by another process" });
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
declare class ConflictError extends ConflictErrorBase {
|
|
130
|
+
readonly category: "conflict";
|
|
131
|
+
exitCode(): number;
|
|
132
|
+
statusCode(): number;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Authorization denied.
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* ```typescript
|
|
139
|
+
* new PermissionError({ message: "Cannot delete read-only resource" });
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
declare class PermissionError extends PermissionErrorBase {
|
|
143
|
+
readonly category: "permission";
|
|
144
|
+
exitCode(): number;
|
|
145
|
+
statusCode(): number;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Operation timed out.
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* new TimeoutError({ message: "Database query timed out after 5000ms", operation: "Database query", timeoutMs: 5000 });
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
declare class TimeoutError extends TimeoutErrorBase {
|
|
156
|
+
readonly category: "timeout";
|
|
157
|
+
exitCode(): number;
|
|
158
|
+
statusCode(): number;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Rate limit exceeded.
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* new RateLimitError({ message: "Rate limit exceeded, retry after 60s", retryAfterSeconds: 60 });
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
declare class RateLimitError extends RateLimitErrorBase {
|
|
169
|
+
readonly category: "rate_limit";
|
|
170
|
+
exitCode(): number;
|
|
171
|
+
statusCode(): number;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Network/transport failure.
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* ```typescript
|
|
178
|
+
* new NetworkError({ message: "Connection refused to api.example.com" });
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
declare class NetworkError extends NetworkErrorBase {
|
|
182
|
+
readonly category: "network";
|
|
183
|
+
exitCode(): number;
|
|
184
|
+
statusCode(): number;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Unexpected internal error.
|
|
188
|
+
*
|
|
189
|
+
* @example
|
|
190
|
+
* ```typescript
|
|
191
|
+
* new InternalError({ message: "Unexpected state in processor" });
|
|
192
|
+
* ```
|
|
193
|
+
*/
|
|
194
|
+
declare class InternalError extends InternalErrorBase {
|
|
195
|
+
readonly category: "internal";
|
|
196
|
+
exitCode(): number;
|
|
197
|
+
statusCode(): number;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Authentication failed (missing or invalid credentials).
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* ```typescript
|
|
204
|
+
* new AuthError({ message: "Invalid API key", reason: "invalid" });
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
declare class AuthError extends AuthErrorBase {
|
|
208
|
+
readonly category: "auth";
|
|
209
|
+
exitCode(): number;
|
|
210
|
+
statusCode(): number;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Operation cancelled by user or signal.
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```typescript
|
|
217
|
+
* new CancelledError({ message: "Operation cancelled by user" });
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
declare class CancelledError extends CancelledErrorBase {
|
|
221
|
+
readonly category: "cancelled";
|
|
222
|
+
exitCode(): number;
|
|
223
|
+
statusCode(): number;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Union type of all concrete error class instances.
|
|
227
|
+
*/
|
|
228
|
+
type AnyKitError = InstanceType<typeof ValidationError> | InstanceType<typeof AssertionError> | InstanceType<typeof NotFoundError> | InstanceType<typeof ConflictError> | InstanceType<typeof PermissionError> | InstanceType<typeof TimeoutError> | InstanceType<typeof RateLimitError> | InstanceType<typeof NetworkError> | InstanceType<typeof InternalError> | InstanceType<typeof AuthError> | InstanceType<typeof CancelledError>;
|
|
229
|
+
/**
|
|
230
|
+
* Type alias for backwards compatibility with handler signatures.
|
|
231
|
+
* Use AnyKitError for the union type.
|
|
232
|
+
*/
|
|
233
|
+
type OutfitterError = AnyKitError;
|
|
234
|
+
import { Result } from "better-result";
|
|
235
|
+
/**
|
|
236
|
+
* Metadata attached to every response envelope.
|
|
237
|
+
*/
|
|
238
|
+
interface EnvelopeMeta {
|
|
239
|
+
/** Unique request identifier for tracing */
|
|
240
|
+
requestId: string;
|
|
241
|
+
/** ISO timestamp of response generation */
|
|
242
|
+
timestamp: string;
|
|
243
|
+
/** Operation duration in milliseconds */
|
|
244
|
+
durationMs?: number;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Pagination metadata for list responses.
|
|
248
|
+
*/
|
|
249
|
+
interface PaginationMeta {
|
|
250
|
+
/** Total number of items (if known) */
|
|
251
|
+
total?: number;
|
|
252
|
+
/** Number of items returned */
|
|
253
|
+
count: number;
|
|
254
|
+
/** Cursor for next page (null if no more pages) */
|
|
255
|
+
nextCursor: string | null;
|
|
256
|
+
/** Whether more pages exist */
|
|
257
|
+
hasMore: boolean;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Success envelope structure.
|
|
261
|
+
*/
|
|
262
|
+
interface SuccessEnvelope<T> {
|
|
263
|
+
ok: true;
|
|
264
|
+
data: T;
|
|
265
|
+
meta: EnvelopeMeta;
|
|
266
|
+
pagination?: PaginationMeta;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Error envelope structure.
|
|
270
|
+
*/
|
|
271
|
+
interface ErrorEnvelope {
|
|
272
|
+
ok: false;
|
|
273
|
+
error: SerializedError;
|
|
274
|
+
meta: EnvelopeMeta;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Response envelope - consistent wrapper for all handler responses.
|
|
278
|
+
*/
|
|
279
|
+
type Envelope<T> = SuccessEnvelope<T> | ErrorEnvelope;
|
|
280
|
+
/**
|
|
281
|
+
* HTTP-style response with status code.
|
|
282
|
+
*/
|
|
283
|
+
interface HttpResponse<T> {
|
|
284
|
+
status: number;
|
|
285
|
+
body: Envelope<T>;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Convert a Result to a response envelope.
|
|
289
|
+
*
|
|
290
|
+
* @typeParam T - Success data type
|
|
291
|
+
* @typeParam E - Error type (extends OutfitterError)
|
|
292
|
+
* @param result - Handler result to wrap
|
|
293
|
+
* @param meta - Optional metadata overrides
|
|
294
|
+
* @returns Envelope with success data or serialized error
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* const result = await getNote({ id: "abc123" }, ctx);
|
|
299
|
+
* const envelope = toEnvelope(result, { requestId: ctx.requestId });
|
|
300
|
+
* ```
|
|
301
|
+
*/
|
|
302
|
+
declare function toEnvelope<
|
|
303
|
+
T,
|
|
304
|
+
E extends OutfitterError
|
|
305
|
+
>(result: Result<T, E>, meta?: Partial<EnvelopeMeta>): Envelope<T>;
|
|
306
|
+
/**
|
|
307
|
+
* Convert a Result to HTTP-style response (for MCP over HTTP).
|
|
308
|
+
*
|
|
309
|
+
* Maps error category to appropriate HTTP status code.
|
|
310
|
+
*
|
|
311
|
+
* @typeParam T - Success data type
|
|
312
|
+
* @typeParam E - Error type (extends OutfitterError)
|
|
313
|
+
* @param result - Handler result to convert
|
|
314
|
+
* @returns HTTP response with status code and envelope body
|
|
315
|
+
*
|
|
316
|
+
* @example
|
|
317
|
+
* ```typescript
|
|
318
|
+
* const result = await getNote({ id: "abc123" }, ctx);
|
|
319
|
+
* const response = toHttpResponse(result);
|
|
320
|
+
* // { status: 200, body: { ok: true, data: note, meta: {...} } }
|
|
321
|
+
* // or { status: 404, body: { ok: false, error: {...}, meta: {...} } }
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
declare function toHttpResponse<
|
|
325
|
+
T,
|
|
326
|
+
E extends OutfitterError
|
|
327
|
+
>(result: Result<T, E>): HttpResponse<T>;
|
|
328
|
+
export { toHttpResponse, toEnvelope, SuccessEnvelope, PaginationMeta, HttpResponse, ErrorEnvelope, EnvelopeMeta, Envelope };
|