@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/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# @outfitter/contracts
|
|
2
|
+
|
|
3
|
+
Result/Error patterns, error taxonomy, handler contracts, and shared interfaces for the Outfitter ecosystem.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
**Scaffold** - Types and interfaces defined, implementations pending.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bun add @outfitter/contracts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
This package provides the foundational contracts that all Outfitter packages depend on:
|
|
18
|
+
|
|
19
|
+
- **Error taxonomy** - 10 concrete error classes with category-based exit/status codes
|
|
20
|
+
- **Handler contract** - Transport-agnostic domain logic interface
|
|
21
|
+
- **Validation** - Zod-based input validation returning Results
|
|
22
|
+
- **Serialization** - Safe JSON handling with redaction
|
|
23
|
+
- **Adapters** - Pluggable interfaces for indexing, caching, auth, and storage
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import {
|
|
29
|
+
Result,
|
|
30
|
+
NotFoundError,
|
|
31
|
+
Handler,
|
|
32
|
+
HandlerContext,
|
|
33
|
+
createContext,
|
|
34
|
+
} from "@outfitter/contracts";
|
|
35
|
+
|
|
36
|
+
// Define a handler
|
|
37
|
+
const getNote: Handler<{ id: string }, Note, NotFoundError> = async (input, ctx) => {
|
|
38
|
+
const note = await db.notes.find(input.id);
|
|
39
|
+
if (!note) return Result.err(new NotFoundError("note", input.id));
|
|
40
|
+
return Result.ok(note);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Create context and invoke
|
|
44
|
+
const ctx = createContext({ logger, config });
|
|
45
|
+
const result = await getNote({ id: "abc123" }, ctx);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
MIT
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { TaggedErrorClass } from "better-result";
|
|
2
|
+
declare const ValidationErrorBase: TaggedErrorClass<"ValidationError", {
|
|
3
|
+
message: string;
|
|
4
|
+
field?: string;
|
|
5
|
+
}>;
|
|
6
|
+
declare const AssertionErrorBase: TaggedErrorClass<"AssertionError", {
|
|
7
|
+
message: string;
|
|
8
|
+
}>;
|
|
9
|
+
declare const NotFoundErrorBase: TaggedErrorClass<"NotFoundError", {
|
|
10
|
+
message: string;
|
|
11
|
+
resourceType: string;
|
|
12
|
+
resourceId: string;
|
|
13
|
+
}>;
|
|
14
|
+
declare const ConflictErrorBase: TaggedErrorClass<"ConflictError", {
|
|
15
|
+
message: string;
|
|
16
|
+
context?: Record<string, unknown>;
|
|
17
|
+
}>;
|
|
18
|
+
declare const PermissionErrorBase: TaggedErrorClass<"PermissionError", {
|
|
19
|
+
message: string;
|
|
20
|
+
context?: Record<string, unknown>;
|
|
21
|
+
}>;
|
|
22
|
+
declare const TimeoutErrorBase: TaggedErrorClass<"TimeoutError", {
|
|
23
|
+
message: string;
|
|
24
|
+
operation: string;
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
}>;
|
|
27
|
+
declare const RateLimitErrorBase: TaggedErrorClass<"RateLimitError", {
|
|
28
|
+
message: string;
|
|
29
|
+
retryAfterSeconds?: number;
|
|
30
|
+
}>;
|
|
31
|
+
declare const NetworkErrorBase: TaggedErrorClass<"NetworkError", {
|
|
32
|
+
message: string;
|
|
33
|
+
context?: Record<string, unknown>;
|
|
34
|
+
}>;
|
|
35
|
+
declare const InternalErrorBase: TaggedErrorClass<"InternalError", {
|
|
36
|
+
message: string;
|
|
37
|
+
context?: Record<string, unknown>;
|
|
38
|
+
}>;
|
|
39
|
+
declare const AuthErrorBase: TaggedErrorClass<"AuthError", {
|
|
40
|
+
message: string;
|
|
41
|
+
reason?: "missing" | "invalid" | "expired";
|
|
42
|
+
}>;
|
|
43
|
+
declare const CancelledErrorBase: TaggedErrorClass<"CancelledError", {
|
|
44
|
+
message: string;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Input validation failed.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* new ValidationError({ message: "Email format invalid", field: "email" });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
declare class ValidationError extends ValidationErrorBase {
|
|
55
|
+
readonly category: "validation";
|
|
56
|
+
exitCode(): number;
|
|
57
|
+
statusCode(): number;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Assertion failed (invariant violation).
|
|
61
|
+
*
|
|
62
|
+
* Used by assertion utilities that return Result types instead of throwing.
|
|
63
|
+
* AssertionError indicates a programming bug — an invariant that should
|
|
64
|
+
* never be violated was broken. These are internal errors, not user input
|
|
65
|
+
* validation failures.
|
|
66
|
+
*
|
|
67
|
+
* **Category rationale**: Uses `internal` (not `validation`) because:
|
|
68
|
+
* - Assertions check **invariants** (programmer assumptions), not user input
|
|
69
|
+
* - A failed assertion means "this should be impossible if the code is correct"
|
|
70
|
+
* - User-facing validation uses {@link ValidationError} with helpful field info
|
|
71
|
+
* - HTTP 500 is correct: this is a server bug, not a client mistake
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* // In domain logic after validation has passed
|
|
76
|
+
* const result = assertDefined(cachedValue, "Cache should always have value after init");
|
|
77
|
+
* if (result.isErr()) {
|
|
78
|
+
* return result; // Propagate as internal error
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @see ValidationError - For user input validation failures (HTTP 400)
|
|
83
|
+
*/
|
|
84
|
+
declare class AssertionError extends AssertionErrorBase {
|
|
85
|
+
readonly category: "internal";
|
|
86
|
+
exitCode(): number;
|
|
87
|
+
statusCode(): number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Requested resource not found.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* new NotFoundError({ message: "note not found: abc123", resourceType: "note", resourceId: "abc123" });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
declare class NotFoundError extends NotFoundErrorBase {
|
|
98
|
+
readonly category: "not_found";
|
|
99
|
+
exitCode(): number;
|
|
100
|
+
statusCode(): number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* State conflict (optimistic locking, concurrent modification).
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* new ConflictError({ message: "Resource was modified by another process" });
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
declare class ConflictError extends ConflictErrorBase {
|
|
111
|
+
readonly category: "conflict";
|
|
112
|
+
exitCode(): number;
|
|
113
|
+
statusCode(): number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Authorization denied.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* new PermissionError({ message: "Cannot delete read-only resource" });
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
declare class PermissionError extends PermissionErrorBase {
|
|
124
|
+
readonly category: "permission";
|
|
125
|
+
exitCode(): number;
|
|
126
|
+
statusCode(): number;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Operation timed out.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```typescript
|
|
133
|
+
* new TimeoutError({ message: "Database query timed out after 5000ms", operation: "Database query", timeoutMs: 5000 });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
declare class TimeoutError extends TimeoutErrorBase {
|
|
137
|
+
readonly category: "timeout";
|
|
138
|
+
exitCode(): number;
|
|
139
|
+
statusCode(): number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Rate limit exceeded.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```typescript
|
|
146
|
+
* new RateLimitError({ message: "Rate limit exceeded, retry after 60s", retryAfterSeconds: 60 });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
declare class RateLimitError extends RateLimitErrorBase {
|
|
150
|
+
readonly category: "rate_limit";
|
|
151
|
+
exitCode(): number;
|
|
152
|
+
statusCode(): number;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Network/transport failure.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* new NetworkError({ message: "Connection refused to api.example.com" });
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
declare class NetworkError extends NetworkErrorBase {
|
|
163
|
+
readonly category: "network";
|
|
164
|
+
exitCode(): number;
|
|
165
|
+
statusCode(): number;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Unexpected internal error.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```typescript
|
|
172
|
+
* new InternalError({ message: "Unexpected state in processor" });
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
declare class InternalError extends InternalErrorBase {
|
|
176
|
+
readonly category: "internal";
|
|
177
|
+
exitCode(): number;
|
|
178
|
+
statusCode(): number;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Authentication failed (missing or invalid credentials).
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```typescript
|
|
185
|
+
* new AuthError({ message: "Invalid API key", reason: "invalid" });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
declare class AuthError extends AuthErrorBase {
|
|
189
|
+
readonly category: "auth";
|
|
190
|
+
exitCode(): number;
|
|
191
|
+
statusCode(): number;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Operation cancelled by user or signal.
|
|
195
|
+
*
|
|
196
|
+
* @example
|
|
197
|
+
* ```typescript
|
|
198
|
+
* new CancelledError({ message: "Operation cancelled by user" });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
declare class CancelledError extends CancelledErrorBase {
|
|
202
|
+
readonly category: "cancelled";
|
|
203
|
+
exitCode(): number;
|
|
204
|
+
statusCode(): number;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Union type of all concrete error class instances.
|
|
208
|
+
*/
|
|
209
|
+
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>;
|
|
210
|
+
/**
|
|
211
|
+
* Type alias for backwards compatibility with handler signatures.
|
|
212
|
+
* Use AnyKitError for the union type.
|
|
213
|
+
*/
|
|
214
|
+
type OutfitterError = AnyKitError;
|
|
215
|
+
import { Result } from "better-result";
|
|
216
|
+
/**
|
|
217
|
+
* Logger interface for handler context.
|
|
218
|
+
* Implementations provided by @outfitter/logging.
|
|
219
|
+
*
|
|
220
|
+
* All log methods accept an optional context object that will be merged
|
|
221
|
+
* with any context inherited from parent loggers created via `child()`.
|
|
222
|
+
*/
|
|
223
|
+
interface Logger {
|
|
224
|
+
trace(message: string, metadata?: Record<string, unknown>): void;
|
|
225
|
+
debug(message: string, metadata?: Record<string, unknown>): void;
|
|
226
|
+
info(message: string, metadata?: Record<string, unknown>): void;
|
|
227
|
+
warn(message: string, metadata?: Record<string, unknown>): void;
|
|
228
|
+
error(message: string, metadata?: Record<string, unknown>): void;
|
|
229
|
+
fatal(message: string, metadata?: Record<string, unknown>): void;
|
|
230
|
+
/**
|
|
231
|
+
* Creates a child logger with additional context.
|
|
232
|
+
*
|
|
233
|
+
* Context from the child is merged with the parent's context,
|
|
234
|
+
* with child context taking precedence for duplicate keys.
|
|
235
|
+
* Child loggers are composable (can create nested children).
|
|
236
|
+
*
|
|
237
|
+
* @param context - Additional context to include in all log messages
|
|
238
|
+
* @returns A new Logger instance with the merged context
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const requestLogger = ctx.logger.child({ requestId: ctx.requestId });
|
|
243
|
+
* requestLogger.info("Processing request"); // includes requestId
|
|
244
|
+
*
|
|
245
|
+
* const opLogger = requestLogger.child({ operation: "create" });
|
|
246
|
+
* opLogger.debug("Starting"); // includes requestId + operation
|
|
247
|
+
* ```
|
|
248
|
+
*/
|
|
249
|
+
child(context: Record<string, unknown>): Logger;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Resolved configuration interface.
|
|
253
|
+
* Implementations provided by @outfitter/config.
|
|
254
|
+
*/
|
|
255
|
+
interface ResolvedConfig {
|
|
256
|
+
get<T>(key: string): T | undefined;
|
|
257
|
+
getRequired<T>(key: string): T;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Handler context - provides cross-cutting concerns without polluting handler signatures.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* const handler: Handler<Input, Output, NotFoundError> = async (input, ctx) => {
|
|
265
|
+
* ctx.logger.debug("Processing request", { requestId: ctx.requestId });
|
|
266
|
+
* // ... handler logic
|
|
267
|
+
* };
|
|
268
|
+
* ```
|
|
269
|
+
*/
|
|
270
|
+
interface HandlerContext {
|
|
271
|
+
/** Abort signal for cancellation propagation */
|
|
272
|
+
signal?: AbortSignal;
|
|
273
|
+
/** Unique request identifier for tracing (UUIDv7) */
|
|
274
|
+
requestId: string;
|
|
275
|
+
/** Structured logger with automatic redaction */
|
|
276
|
+
logger: Logger;
|
|
277
|
+
/** Resolved configuration values */
|
|
278
|
+
config?: ResolvedConfig;
|
|
279
|
+
/** Workspace root path, if detected */
|
|
280
|
+
workspaceRoot?: string;
|
|
281
|
+
/** Current working directory */
|
|
282
|
+
cwd: string;
|
|
283
|
+
/** Environment variables (filtered, redacted) */
|
|
284
|
+
env: Record<string, string | undefined>;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Handler - transport-agnostic domain logic unit.
|
|
288
|
+
*
|
|
289
|
+
* Handlers receive typed input, return Results, and know nothing about
|
|
290
|
+
* transport or output format. CLI and MCP are thin adapters over handlers.
|
|
291
|
+
*
|
|
292
|
+
* @typeParam TInput - Validated input parameters
|
|
293
|
+
* @typeParam TOutput - Success return type
|
|
294
|
+
* @typeParam TError - Error type (must extend OutfitterError)
|
|
295
|
+
*
|
|
296
|
+
* @example
|
|
297
|
+
* ```typescript
|
|
298
|
+
* const getNote: Handler<{ id: string }, Note, NotFoundError> = async (input, ctx) => {
|
|
299
|
+
* const note = await ctx.db.notes.find(input.id);
|
|
300
|
+
* if (!note) return Result.err(new NotFoundError("note", input.id));
|
|
301
|
+
* return Result.ok(note);
|
|
302
|
+
* };
|
|
303
|
+
* ```
|
|
304
|
+
*/
|
|
305
|
+
type Handler<
|
|
306
|
+
TInput,
|
|
307
|
+
TOutput,
|
|
308
|
+
TError extends OutfitterError = OutfitterError
|
|
309
|
+
> = (input: TInput, ctx: HandlerContext) => Promise<Result<TOutput, TError>>;
|
|
310
|
+
/**
|
|
311
|
+
* Synchronous handler variant for operations that don't need async.
|
|
312
|
+
*/
|
|
313
|
+
type SyncHandler<
|
|
314
|
+
TInput,
|
|
315
|
+
TOutput,
|
|
316
|
+
TError extends OutfitterError = OutfitterError
|
|
317
|
+
> = (input: TInput, ctx: HandlerContext) => Result<TOutput, TError>;
|
|
318
|
+
import { z } from "zod";
|
|
319
|
+
declare const ACTION_SURFACES: readonly ["cli", "mcp", "api", "server"];
|
|
320
|
+
type ActionSurface = (typeof ACTION_SURFACES)[number];
|
|
321
|
+
declare const DEFAULT_REGISTRY_SURFACES: readonly ActionSurface[];
|
|
322
|
+
interface ActionCliOption {
|
|
323
|
+
readonly flags: string;
|
|
324
|
+
readonly description: string;
|
|
325
|
+
readonly defaultValue?: string | boolean | string[];
|
|
326
|
+
readonly required?: boolean;
|
|
327
|
+
}
|
|
328
|
+
interface ActionCliInputContext {
|
|
329
|
+
readonly args: readonly string[];
|
|
330
|
+
readonly flags: Record<string, unknown>;
|
|
331
|
+
}
|
|
332
|
+
interface ActionCliSpec<TInput = unknown> {
|
|
333
|
+
readonly group?: string;
|
|
334
|
+
readonly command?: string;
|
|
335
|
+
readonly description?: string;
|
|
336
|
+
readonly aliases?: readonly string[];
|
|
337
|
+
readonly options?: readonly ActionCliOption[];
|
|
338
|
+
readonly mapInput?: (context: ActionCliInputContext) => TInput;
|
|
339
|
+
}
|
|
340
|
+
interface ActionMcpSpec<TInput = unknown> {
|
|
341
|
+
readonly tool?: string;
|
|
342
|
+
readonly description?: string;
|
|
343
|
+
readonly deferLoading?: boolean;
|
|
344
|
+
readonly mapInput?: (input: unknown) => TInput;
|
|
345
|
+
}
|
|
346
|
+
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
347
|
+
interface ActionApiSpec {
|
|
348
|
+
readonly method?: HttpMethod;
|
|
349
|
+
readonly path?: string;
|
|
350
|
+
readonly tags?: readonly string[];
|
|
351
|
+
}
|
|
352
|
+
interface ActionTrpcSpec {
|
|
353
|
+
readonly path?: string;
|
|
354
|
+
}
|
|
355
|
+
interface ActionSpec<
|
|
356
|
+
TInput,
|
|
357
|
+
TOutput,
|
|
358
|
+
TError extends OutfitterError = OutfitterError
|
|
359
|
+
> {
|
|
360
|
+
readonly id: string;
|
|
361
|
+
readonly description?: string;
|
|
362
|
+
readonly surfaces?: readonly ActionSurface[];
|
|
363
|
+
readonly input: z.ZodType<TInput>;
|
|
364
|
+
readonly output?: z.ZodType<TOutput>;
|
|
365
|
+
readonly handler: Handler<TInput, TOutput, TError> | SyncHandler<TInput, TOutput, TError>;
|
|
366
|
+
readonly cli?: ActionCliSpec<TInput>;
|
|
367
|
+
readonly mcp?: ActionMcpSpec<TInput>;
|
|
368
|
+
readonly api?: ActionApiSpec;
|
|
369
|
+
readonly trpc?: ActionTrpcSpec;
|
|
370
|
+
}
|
|
371
|
+
type AnyActionSpec = ActionSpec<unknown, unknown, OutfitterError>;
|
|
372
|
+
interface ActionRegistry {
|
|
373
|
+
add<
|
|
374
|
+
TInput,
|
|
375
|
+
TOutput,
|
|
376
|
+
TError extends OutfitterError = OutfitterError
|
|
377
|
+
>(action: ActionSpec<TInput, TOutput, TError>): ActionRegistry;
|
|
378
|
+
list(): AnyActionSpec[];
|
|
379
|
+
get(id: string): AnyActionSpec | undefined;
|
|
380
|
+
forSurface(surface: ActionSurface): AnyActionSpec[];
|
|
381
|
+
}
|
|
382
|
+
declare function defineAction<
|
|
383
|
+
TInput,
|
|
384
|
+
TOutput,
|
|
385
|
+
TError extends OutfitterError = OutfitterError
|
|
386
|
+
>(action: ActionSpec<TInput, TOutput, TError>): ActionSpec<TInput, TOutput, TError>;
|
|
387
|
+
declare function createActionRegistry(): ActionRegistry;
|
|
388
|
+
export { defineAction, createActionRegistry, HttpMethod, DEFAULT_REGISTRY_SURFACES, AnyActionSpec, ActionTrpcSpec, ActionSurface, ActionSpec, ActionRegistry, ActionMcpSpec, ActionCliSpec, ActionCliOption, ActionCliInputContext, ActionApiSpec, ACTION_SURFACES };
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/contracts/src/actions.ts
|
|
3
|
+
var ACTION_SURFACES = ["cli", "mcp", "api", "server"];
|
|
4
|
+
var DEFAULT_REGISTRY_SURFACES = ACTION_SURFACES;
|
|
5
|
+
function defineAction(action) {
|
|
6
|
+
return action;
|
|
7
|
+
}
|
|
8
|
+
function createActionRegistry() {
|
|
9
|
+
const actions = new Map;
|
|
10
|
+
return {
|
|
11
|
+
add(action) {
|
|
12
|
+
actions.set(action.id, action);
|
|
13
|
+
return this;
|
|
14
|
+
},
|
|
15
|
+
list() {
|
|
16
|
+
return Array.from(actions.values());
|
|
17
|
+
},
|
|
18
|
+
get(id) {
|
|
19
|
+
return actions.get(id);
|
|
20
|
+
},
|
|
21
|
+
forSurface(surface) {
|
|
22
|
+
const defaults = DEFAULT_REGISTRY_SURFACES;
|
|
23
|
+
return Array.from(actions.values()).filter((action) => (action.surfaces ?? defaults).includes(surface));
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export {
|
|
28
|
+
defineAction,
|
|
29
|
+
createActionRegistry,
|
|
30
|
+
DEFAULT_REGISTRY_SURFACES,
|
|
31
|
+
ACTION_SURFACES
|
|
32
|
+
};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Result } from "better-result";
|
|
2
|
+
/**
|
|
3
|
+
* Error types for adapter operations.
|
|
4
|
+
* These extend the base error taxonomy for adapter-specific failures.
|
|
5
|
+
*/
|
|
6
|
+
/** Error during indexing operations */
|
|
7
|
+
interface IndexError {
|
|
8
|
+
readonly _tag: "IndexError";
|
|
9
|
+
readonly message: string;
|
|
10
|
+
readonly cause?: unknown;
|
|
11
|
+
}
|
|
12
|
+
/** Error during cache operations */
|
|
13
|
+
interface CacheError {
|
|
14
|
+
readonly _tag: "CacheError";
|
|
15
|
+
readonly message: string;
|
|
16
|
+
readonly cause?: unknown;
|
|
17
|
+
}
|
|
18
|
+
/** Error during auth/credential operations */
|
|
19
|
+
interface AdapterAuthError {
|
|
20
|
+
readonly _tag: "AdapterAuthError";
|
|
21
|
+
readonly message: string;
|
|
22
|
+
readonly cause?: unknown;
|
|
23
|
+
}
|
|
24
|
+
/** Error during storage operations */
|
|
25
|
+
interface StorageError {
|
|
26
|
+
readonly _tag: "StorageError";
|
|
27
|
+
readonly message: string;
|
|
28
|
+
readonly cause?: unknown;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Search options for index adapter.
|
|
32
|
+
*/
|
|
33
|
+
interface SearchOptions {
|
|
34
|
+
/** Maximum results to return */
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** Offset for pagination */
|
|
37
|
+
offset?: number;
|
|
38
|
+
/** Field-specific filters */
|
|
39
|
+
filters?: Record<string, unknown>;
|
|
40
|
+
/** Fields to boost in relevance scoring */
|
|
41
|
+
boostFields?: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Search result from index adapter.
|
|
45
|
+
*/
|
|
46
|
+
interface SearchResult<T> {
|
|
47
|
+
/** Matched items with relevance scores */
|
|
48
|
+
hits: Array<{
|
|
49
|
+
item: T;
|
|
50
|
+
score: number;
|
|
51
|
+
highlights?: Record<string, string[]>;
|
|
52
|
+
}>;
|
|
53
|
+
/** Total number of matches (for pagination) */
|
|
54
|
+
total: number;
|
|
55
|
+
/** Search execution time in milliseconds */
|
|
56
|
+
took: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Index statistics.
|
|
60
|
+
*/
|
|
61
|
+
interface IndexStats {
|
|
62
|
+
/** Total documents indexed */
|
|
63
|
+
documentCount: number;
|
|
64
|
+
/** Index size in bytes (if available) */
|
|
65
|
+
sizeBytes?: number;
|
|
66
|
+
/** Last update timestamp */
|
|
67
|
+
lastUpdated: Date | null;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Index adapter - pluggable full-text search backends.
|
|
71
|
+
*
|
|
72
|
+
* Implementations: SQLite FTS5, future: Tantivy, Meilisearch
|
|
73
|
+
*
|
|
74
|
+
* @typeParam T - The indexed document type
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const sqliteIndex = new SqliteFts5Adapter<Note>({
|
|
79
|
+
* db: database,
|
|
80
|
+
* table: "notes_fts",
|
|
81
|
+
* fields: ["title", "content", "tags"],
|
|
82
|
+
* });
|
|
83
|
+
*
|
|
84
|
+
* await sqliteIndex.index(notes);
|
|
85
|
+
* const results = await sqliteIndex.search("authentication patterns");
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
interface IndexAdapter<T> {
|
|
89
|
+
/** Add or update documents in the index */
|
|
90
|
+
index(items: T[]): Promise<Result<void, IndexError>>;
|
|
91
|
+
/** Full-text search with optional filters */
|
|
92
|
+
search(query: string, options?: SearchOptions): Promise<Result<SearchResult<T>, IndexError>>;
|
|
93
|
+
/** Remove documents by ID */
|
|
94
|
+
remove(ids: string[]): Promise<Result<void, IndexError>>;
|
|
95
|
+
/** Clear all indexed documents */
|
|
96
|
+
clear(): Promise<Result<void, IndexError>>;
|
|
97
|
+
/** Get index statistics */
|
|
98
|
+
stats(): Promise<Result<IndexStats, IndexError>>;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Cache adapter - pluggable caching backends.
|
|
102
|
+
*
|
|
103
|
+
* Implementations: SQLite, in-memory LRU, future: Redis via Bun.RedisClient
|
|
104
|
+
*
|
|
105
|
+
* @typeParam T - The cached value type
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```typescript
|
|
109
|
+
* const cache = new SqliteCacheAdapter<User>({ db, table: "user_cache" });
|
|
110
|
+
*
|
|
111
|
+
* await cache.set("user:123", user, 3600); // 1 hour TTL
|
|
112
|
+
* const cached = await cache.get("user:123");
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
interface CacheAdapter<T> {
|
|
116
|
+
/** Get cached value, null if not found or expired */
|
|
117
|
+
get(key: string): Promise<Result<T | null, CacheError>>;
|
|
118
|
+
/** Set value with optional TTL in seconds */
|
|
119
|
+
set(key: string, value: T, ttlSeconds?: number): Promise<Result<void, CacheError>>;
|
|
120
|
+
/** Delete cached value, returns true if existed */
|
|
121
|
+
delete(key: string): Promise<Result<boolean, CacheError>>;
|
|
122
|
+
/** Clear all cached values */
|
|
123
|
+
clear(): Promise<Result<void, CacheError>>;
|
|
124
|
+
/** Check if key exists (without retrieving value) */
|
|
125
|
+
has(key: string): Promise<Result<boolean, CacheError>>;
|
|
126
|
+
/** Get multiple values at once */
|
|
127
|
+
getMany(keys: string[]): Promise<Result<Map<string, T>, CacheError>>;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Auth adapter - pluggable credential storage.
|
|
131
|
+
*
|
|
132
|
+
* Implementations: Environment, OS Keychain (via Bun.secrets), file-based
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```typescript
|
|
136
|
+
* const auth = new KeychainAuthAdapter({ service: "outfitter" });
|
|
137
|
+
*
|
|
138
|
+
* await auth.set("github_token", token);
|
|
139
|
+
* const stored = await auth.get("github_token");
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
interface AuthAdapter {
|
|
143
|
+
/** Retrieve credential by key */
|
|
144
|
+
get(key: string): Promise<Result<string | null, AdapterAuthError>>;
|
|
145
|
+
/** Store credential */
|
|
146
|
+
set(key: string, value: string): Promise<Result<void, AdapterAuthError>>;
|
|
147
|
+
/** Remove credential */
|
|
148
|
+
delete(key: string): Promise<Result<boolean, AdapterAuthError>>;
|
|
149
|
+
/** List available credential keys (not values) */
|
|
150
|
+
list(): Promise<Result<string[], AdapterAuthError>>;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Storage adapter - pluggable blob/file storage.
|
|
154
|
+
*
|
|
155
|
+
* Implementations: Local filesystem, S3 via Bun.S3Client, R2
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```typescript
|
|
159
|
+
* const storage = new LocalStorageAdapter({ basePath: "/data" });
|
|
160
|
+
*
|
|
161
|
+
* await storage.write("notes/abc.md", content);
|
|
162
|
+
* const data = await storage.read("notes/abc.md");
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
interface StorageAdapter {
|
|
166
|
+
/** Read file contents */
|
|
167
|
+
read(path: string): Promise<Result<Uint8Array, StorageError>>;
|
|
168
|
+
/** Write file contents */
|
|
169
|
+
write(path: string, data: Uint8Array): Promise<Result<void, StorageError>>;
|
|
170
|
+
/** Delete file */
|
|
171
|
+
delete(path: string): Promise<Result<boolean, StorageError>>;
|
|
172
|
+
/** Check if file exists */
|
|
173
|
+
exists(path: string): Promise<Result<boolean, StorageError>>;
|
|
174
|
+
/** List files in directory */
|
|
175
|
+
list(prefix: string): Promise<Result<string[], StorageError>>;
|
|
176
|
+
/** Get file metadata (size, modified time) */
|
|
177
|
+
stat(path: string): Promise<Result<{
|
|
178
|
+
size: number;
|
|
179
|
+
modifiedAt: Date;
|
|
180
|
+
} | null, StorageError>>;
|
|
181
|
+
}
|
|
182
|
+
export { StorageError, StorageAdapter, SearchResult, SearchOptions, IndexStats, IndexError, IndexAdapter, CacheError, CacheAdapter, AuthAdapter, AdapterAuthError };
|
package/dist/adapters.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// @bun
|