@nebutra/errors 0.1.1 → 2.0.0
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/LICENSE +21 -676
- package/README.md +3 -3
- package/dist/index.d.ts +129 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +294 -0
- package/package.json +18 -5
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/AGENTS.md +0 -65
- package/CHANGELOG.md +0 -7
- package/src/index.test.ts +0 -22
- package/src/index.ts +0 -392
- package/tsconfig.json +0 -10
package/README.md
CHANGED
|
@@ -74,7 +74,7 @@ try {
|
|
|
74
74
|
} catch (error) {
|
|
75
75
|
if (isAppError(error)) {
|
|
76
76
|
// Known error with code and status
|
|
77
|
-
|
|
77
|
+
return handleKnownError(error.code, error.statusCode);
|
|
78
78
|
} else {
|
|
79
79
|
// Unknown error
|
|
80
80
|
throw new InternalError("Unexpected error");
|
|
@@ -99,5 +99,5 @@ try {
|
|
|
99
99
|
|
|
100
100
|
## Related
|
|
101
101
|
|
|
102
|
-
- [API Gateway](
|
|
103
|
-
- [Observability](
|
|
102
|
+
- [API Gateway](../../../backends/gateway/)
|
|
103
|
+
- [Observability](../../../infra/ops/observability/)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors - Unified error handling for Nebutra services
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Typed error classes
|
|
6
|
+
* - Consistent API error responses
|
|
7
|
+
* - Error serialization for logging
|
|
8
|
+
*/
|
|
9
|
+
export declare const ERROR_CODES: {
|
|
10
|
+
readonly BAD_REQUEST: "BAD_REQUEST";
|
|
11
|
+
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
12
|
+
readonly FORBIDDEN: "FORBIDDEN";
|
|
13
|
+
readonly NOT_FOUND: "NOT_FOUND";
|
|
14
|
+
readonly CONFLICT: "CONFLICT";
|
|
15
|
+
readonly VALIDATION_ERROR: "VALIDATION_ERROR";
|
|
16
|
+
readonly RATE_LIMITED: "RATE_LIMITED";
|
|
17
|
+
readonly QUOTA_EXCEEDED: "QUOTA_EXCEEDED";
|
|
18
|
+
readonly INTERNAL_ERROR: "INTERNAL_ERROR";
|
|
19
|
+
readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
|
|
20
|
+
readonly EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR";
|
|
21
|
+
readonly DATABASE_ERROR: "DATABASE_ERROR";
|
|
22
|
+
readonly TIMEOUT: "TIMEOUT";
|
|
23
|
+
readonly PAYMENT_REQUIRED: "PAYMENT_REQUIRED";
|
|
24
|
+
readonly SUBSCRIPTION_EXPIRED: "SUBSCRIPTION_EXPIRED";
|
|
25
|
+
readonly FEATURE_DISABLED: "FEATURE_DISABLED";
|
|
26
|
+
readonly TENANT_SUSPENDED: "TENANT_SUSPENDED";
|
|
27
|
+
};
|
|
28
|
+
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
29
|
+
export interface AppErrorOptions {
|
|
30
|
+
code: ErrorCode;
|
|
31
|
+
message: string;
|
|
32
|
+
statusCode?: number;
|
|
33
|
+
cause?: Error;
|
|
34
|
+
metadata?: Record<string, unknown>;
|
|
35
|
+
suggestion?: string;
|
|
36
|
+
isOperational?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export declare class AppError extends Error {
|
|
39
|
+
readonly code: ErrorCode;
|
|
40
|
+
readonly statusCode: number;
|
|
41
|
+
readonly isOperational: boolean;
|
|
42
|
+
readonly metadata?: Record<string, unknown>;
|
|
43
|
+
readonly suggestion?: string;
|
|
44
|
+
readonly timestamp: string;
|
|
45
|
+
constructor(options: AppErrorOptions);
|
|
46
|
+
toJSON(): Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
export interface CapabilityErrorOptions {
|
|
49
|
+
statusCode?: number;
|
|
50
|
+
cause?: Error;
|
|
51
|
+
metadata?: Record<string, unknown>;
|
|
52
|
+
suggestion: string;
|
|
53
|
+
code?: ErrorCode;
|
|
54
|
+
}
|
|
55
|
+
export declare class CapabilityError extends AppError {
|
|
56
|
+
readonly capability: string;
|
|
57
|
+
constructor(capability: string, message: string, options: CapabilityErrorOptions);
|
|
58
|
+
}
|
|
59
|
+
export declare class ValidationError extends AppError {
|
|
60
|
+
readonly fields?: Record<string, string[]>;
|
|
61
|
+
constructor(message: string, fields?: Record<string, string[]>);
|
|
62
|
+
}
|
|
63
|
+
export declare class UnauthorizedError extends AppError {
|
|
64
|
+
constructor(message?: string);
|
|
65
|
+
}
|
|
66
|
+
export declare class ForbiddenError extends AppError {
|
|
67
|
+
constructor(message?: string);
|
|
68
|
+
}
|
|
69
|
+
export declare class NotFoundError extends AppError {
|
|
70
|
+
constructor(resource?: string, id?: string);
|
|
71
|
+
}
|
|
72
|
+
export declare class ConflictError extends AppError {
|
|
73
|
+
constructor(message?: string);
|
|
74
|
+
}
|
|
75
|
+
export declare class RateLimitError extends AppError {
|
|
76
|
+
readonly retryAfter?: number;
|
|
77
|
+
constructor(retryAfter?: number);
|
|
78
|
+
}
|
|
79
|
+
export declare class QuotaExceededError extends AppError {
|
|
80
|
+
constructor(quota: string, limit: number, current: number);
|
|
81
|
+
}
|
|
82
|
+
export declare class ExternalServiceError extends AppError {
|
|
83
|
+
constructor(service: string, cause?: Error);
|
|
84
|
+
}
|
|
85
|
+
export declare class DatabaseError extends AppError {
|
|
86
|
+
constructor(operation: string, cause?: Error);
|
|
87
|
+
}
|
|
88
|
+
export interface ApiErrorResponse {
|
|
89
|
+
error: {
|
|
90
|
+
code: ErrorCode;
|
|
91
|
+
message: string;
|
|
92
|
+
details?: Record<string, unknown>;
|
|
93
|
+
};
|
|
94
|
+
requestId?: string;
|
|
95
|
+
}
|
|
96
|
+
export declare function toApiError(error: unknown, requestId?: string): ApiErrorResponse;
|
|
97
|
+
export declare function getStatusCode(error: unknown): number;
|
|
98
|
+
interface HonoContext {
|
|
99
|
+
req: {
|
|
100
|
+
header: (name: string) => string | undefined;
|
|
101
|
+
};
|
|
102
|
+
json: (data: unknown, status?: number) => unknown;
|
|
103
|
+
}
|
|
104
|
+
export interface ErrorHandlerOptions {
|
|
105
|
+
/**
|
|
106
|
+
* Called for every caught error so callers can route it to their structured
|
|
107
|
+
* logger (e.g. @nebutra/logger). Defaults to a no-op — DO NOT rely on the
|
|
108
|
+
* previous process.stderr.write behaviour; pass an onError callback instead.
|
|
109
|
+
*/
|
|
110
|
+
onError?: (error: unknown, meta: {
|
|
111
|
+
requestId?: string;
|
|
112
|
+
statusCode: number;
|
|
113
|
+
}) => void;
|
|
114
|
+
}
|
|
115
|
+
export declare function errorHandler(options?: ErrorHandlerOptions): (c: HonoContext, next: () => Promise<void>) => Promise<unknown>;
|
|
116
|
+
/**
|
|
117
|
+
* Wrap async function with error handling
|
|
118
|
+
*/
|
|
119
|
+
export declare function tryCatch<T>(fn: () => Promise<T>, errorHandler?: (error: unknown) => T | Promise<T>): Promise<T>;
|
|
120
|
+
/**
|
|
121
|
+
* Assert condition or throw error
|
|
122
|
+
*/
|
|
123
|
+
export declare function assert(condition: unknown, message: string): asserts condition;
|
|
124
|
+
/**
|
|
125
|
+
* Assert not null/undefined or throw NotFoundError
|
|
126
|
+
*/
|
|
127
|
+
export declare function assertFound<T>(value: T | null | undefined, resource: string, id?: string): asserts value is T;
|
|
128
|
+
export {};
|
|
129
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;CAuBd,CAAC;AAEX,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,WAAW,CAAC,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC;AAMvE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,qBAAa,QAAS,SAAQ,KAAK;IACjC,SAAgB,IAAI,EAAE,SAAS,CAAC;IAChC,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,aAAa,EAAE,OAAO,CAAC;IACvC,SAAgB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnD,SAAgB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpC,SAAgB,SAAS,EAAE,MAAM,CAAC;gBAEtB,OAAO,EAAE,eAAe;IAqBpC,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAWlC;AAED,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,SAAgB,UAAU,EAAE,MAAM,CAAC;gBAEvB,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,sBAAsB;CAejF;AAMD,qBAAa,eAAgB,SAAQ,QAAQ;IAC3C,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;gBAEtC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC;CAY/D;AAED,qBAAa,iBAAkB,SAAQ,QAAQ;gBACjC,OAAO,SAAiB;CAIrC;AAED,qBAAa,cAAe,SAAQ,QAAQ;gBAC9B,OAAO,SAAc;CAIlC;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC7B,QAAQ,SAAa,EAAE,EAAE,CAAC,EAAE,MAAM;CAK/C;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC7B,OAAO,SAA4B;CAIhD;AAED,qBAAa,cAAe,SAAQ,QAAQ;IAC1C,SAAgB,UAAU,CAAC,EAAE,MAAM,CAAC;gBAExB,UAAU,CAAC,EAAE,MAAM;CAYhC;AAED,qBAAa,kBAAmB,SAAQ,QAAQ;gBAClC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM;CAS1D;AAED,qBAAa,oBAAqB,SAAQ,QAAQ;gBACpC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK;CAU3C;AAED,qBAAa,aAAc,SAAQ,QAAQ;gBAC7B,SAAS,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK;CAW7C;AAMD,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE;QACL,IAAI,EAAE,SAAS,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACnC,CAAC;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,gBAAgB,CAyB/E;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKpD;AAMD,UAAU,WAAW;IACnB,GAAG,EAAE;QAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAA;KAAE,CAAC;IACtD,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;CACnD;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACtF;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,IAC9C,GAAG,WAAW,EAAE,MAAM,MAAM,OAAO,CAAC,IAAI,CAAC,sBAgBxD;AAyCD;;GAEG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EACxB,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,EACpB,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,GAChD,OAAO,CAAC,CAAC,CAAC,CAOZ;AAED;;GAEG;AACH,wBAAgB,MAAM,CAAC,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAO7E;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,KAAK,EAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EAC3B,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,MAAM,GACV,OAAO,CAAC,KAAK,IAAI,CAAC,CAIpB"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Errors - Unified error handling for Nebutra services
|
|
3
|
+
*
|
|
4
|
+
* Provides:
|
|
5
|
+
* - Typed error classes
|
|
6
|
+
* - Consistent API error responses
|
|
7
|
+
* - Error serialization for logging
|
|
8
|
+
*/
|
|
9
|
+
// ============================================
|
|
10
|
+
// Error Codes
|
|
11
|
+
// ============================================
|
|
12
|
+
export const ERROR_CODES = {
|
|
13
|
+
// Client errors (4xx)
|
|
14
|
+
BAD_REQUEST: "BAD_REQUEST",
|
|
15
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
16
|
+
FORBIDDEN: "FORBIDDEN",
|
|
17
|
+
NOT_FOUND: "NOT_FOUND",
|
|
18
|
+
CONFLICT: "CONFLICT",
|
|
19
|
+
VALIDATION_ERROR: "VALIDATION_ERROR",
|
|
20
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
21
|
+
QUOTA_EXCEEDED: "QUOTA_EXCEEDED",
|
|
22
|
+
// Server errors (5xx)
|
|
23
|
+
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
24
|
+
SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
|
|
25
|
+
EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
|
|
26
|
+
DATABASE_ERROR: "DATABASE_ERROR",
|
|
27
|
+
TIMEOUT: "TIMEOUT",
|
|
28
|
+
// Business errors
|
|
29
|
+
PAYMENT_REQUIRED: "PAYMENT_REQUIRED",
|
|
30
|
+
SUBSCRIPTION_EXPIRED: "SUBSCRIPTION_EXPIRED",
|
|
31
|
+
FEATURE_DISABLED: "FEATURE_DISABLED",
|
|
32
|
+
TENANT_SUSPENDED: "TENANT_SUSPENDED",
|
|
33
|
+
};
|
|
34
|
+
export class AppError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
statusCode;
|
|
37
|
+
isOperational;
|
|
38
|
+
metadata;
|
|
39
|
+
suggestion;
|
|
40
|
+
timestamp;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
super(options.message);
|
|
43
|
+
this.name = "AppError";
|
|
44
|
+
this.code = options.code;
|
|
45
|
+
this.statusCode = options.statusCode || getDefaultStatusCode(options.code);
|
|
46
|
+
this.isOperational = options.isOperational ?? true;
|
|
47
|
+
if (options.metadata !== undefined) {
|
|
48
|
+
this.metadata = options.metadata;
|
|
49
|
+
}
|
|
50
|
+
if (options.suggestion !== undefined) {
|
|
51
|
+
this.suggestion = options.suggestion;
|
|
52
|
+
}
|
|
53
|
+
this.timestamp = new Date().toISOString();
|
|
54
|
+
if (options.cause) {
|
|
55
|
+
this.cause = options.cause;
|
|
56
|
+
}
|
|
57
|
+
Error.captureStackTrace(this, this.constructor);
|
|
58
|
+
}
|
|
59
|
+
toJSON() {
|
|
60
|
+
return {
|
|
61
|
+
name: this.name,
|
|
62
|
+
code: this.code,
|
|
63
|
+
message: this.message,
|
|
64
|
+
statusCode: this.statusCode,
|
|
65
|
+
timestamp: this.timestamp,
|
|
66
|
+
suggestion: this.suggestion,
|
|
67
|
+
metadata: this.metadata,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export class CapabilityError extends AppError {
|
|
72
|
+
capability;
|
|
73
|
+
constructor(capability, message, options) {
|
|
74
|
+
super({
|
|
75
|
+
code: options.code ?? ERROR_CODES.EXTERNAL_SERVICE_ERROR,
|
|
76
|
+
message,
|
|
77
|
+
statusCode: options.statusCode ?? 502,
|
|
78
|
+
...(options.cause !== undefined ? { cause: options.cause } : {}),
|
|
79
|
+
suggestion: options.suggestion,
|
|
80
|
+
metadata: {
|
|
81
|
+
capability,
|
|
82
|
+
...(options.metadata ?? {}),
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
this.name = "CapabilityError";
|
|
86
|
+
this.capability = capability;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// ============================================
|
|
90
|
+
// Specific Error Classes
|
|
91
|
+
// ============================================
|
|
92
|
+
export class ValidationError extends AppError {
|
|
93
|
+
fields;
|
|
94
|
+
constructor(message, fields) {
|
|
95
|
+
super({
|
|
96
|
+
code: ERROR_CODES.VALIDATION_ERROR,
|
|
97
|
+
message,
|
|
98
|
+
statusCode: 400,
|
|
99
|
+
...(fields !== undefined && { metadata: { fields } }),
|
|
100
|
+
});
|
|
101
|
+
this.name = "ValidationError";
|
|
102
|
+
if (fields !== undefined) {
|
|
103
|
+
this.fields = fields;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export class UnauthorizedError extends AppError {
|
|
108
|
+
constructor(message = "Unauthorized") {
|
|
109
|
+
super({ code: ERROR_CODES.UNAUTHORIZED, message, statusCode: 401 });
|
|
110
|
+
this.name = "UnauthorizedError";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
export class ForbiddenError extends AppError {
|
|
114
|
+
constructor(message = "Forbidden") {
|
|
115
|
+
super({ code: ERROR_CODES.FORBIDDEN, message, statusCode: 403 });
|
|
116
|
+
this.name = "ForbiddenError";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
export class NotFoundError extends AppError {
|
|
120
|
+
constructor(resource = "Resource", id) {
|
|
121
|
+
const message = id ? `${resource} with id '${id}' not found` : `${resource} not found`;
|
|
122
|
+
super({ code: ERROR_CODES.NOT_FOUND, message, statusCode: 404 });
|
|
123
|
+
this.name = "NotFoundError";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
export class ConflictError extends AppError {
|
|
127
|
+
constructor(message = "Resource already exists") {
|
|
128
|
+
super({ code: ERROR_CODES.CONFLICT, message, statusCode: 409 });
|
|
129
|
+
this.name = "ConflictError";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export class RateLimitError extends AppError {
|
|
133
|
+
retryAfter;
|
|
134
|
+
constructor(retryAfter) {
|
|
135
|
+
super({
|
|
136
|
+
code: ERROR_CODES.RATE_LIMITED,
|
|
137
|
+
message: "Too many requests",
|
|
138
|
+
statusCode: 429,
|
|
139
|
+
...(retryAfter !== undefined && { metadata: { retryAfter } }),
|
|
140
|
+
});
|
|
141
|
+
this.name = "RateLimitError";
|
|
142
|
+
if (retryAfter !== undefined) {
|
|
143
|
+
this.retryAfter = retryAfter;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export class QuotaExceededError extends AppError {
|
|
148
|
+
constructor(quota, limit, current) {
|
|
149
|
+
super({
|
|
150
|
+
code: ERROR_CODES.QUOTA_EXCEEDED,
|
|
151
|
+
message: `${quota} quota exceeded (${current}/${limit})`,
|
|
152
|
+
statusCode: 429,
|
|
153
|
+
metadata: { quota, limit, current },
|
|
154
|
+
});
|
|
155
|
+
this.name = "QuotaExceededError";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export class ExternalServiceError extends AppError {
|
|
159
|
+
constructor(service, cause) {
|
|
160
|
+
super({
|
|
161
|
+
code: ERROR_CODES.EXTERNAL_SERVICE_ERROR,
|
|
162
|
+
message: `External service '${service}' failed`,
|
|
163
|
+
statusCode: 502,
|
|
164
|
+
...(cause !== undefined && { cause }),
|
|
165
|
+
metadata: { service },
|
|
166
|
+
});
|
|
167
|
+
this.name = "ExternalServiceError";
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
export class DatabaseError extends AppError {
|
|
171
|
+
constructor(operation, cause) {
|
|
172
|
+
super({
|
|
173
|
+
code: ERROR_CODES.DATABASE_ERROR,
|
|
174
|
+
message: `Database operation '${operation}' failed`,
|
|
175
|
+
statusCode: 500,
|
|
176
|
+
...(cause !== undefined && { cause }),
|
|
177
|
+
isOperational: false,
|
|
178
|
+
metadata: { operation },
|
|
179
|
+
});
|
|
180
|
+
this.name = "DatabaseError";
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function toApiError(error, requestId) {
|
|
184
|
+
if (error instanceof AppError) {
|
|
185
|
+
return {
|
|
186
|
+
error: {
|
|
187
|
+
code: error.code,
|
|
188
|
+
message: error.message,
|
|
189
|
+
...((error.metadata !== undefined || error.suggestion !== undefined) && {
|
|
190
|
+
details: {
|
|
191
|
+
...(error.metadata ?? {}),
|
|
192
|
+
...(error.suggestion !== undefined && { suggestion: error.suggestion }),
|
|
193
|
+
},
|
|
194
|
+
}),
|
|
195
|
+
},
|
|
196
|
+
...(requestId !== undefined && { requestId }),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// Unknown error - don't leak details
|
|
200
|
+
return {
|
|
201
|
+
error: {
|
|
202
|
+
code: ERROR_CODES.INTERNAL_ERROR,
|
|
203
|
+
message: "An unexpected error occurred",
|
|
204
|
+
},
|
|
205
|
+
...(requestId !== undefined && { requestId }),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
export function getStatusCode(error) {
|
|
209
|
+
if (error instanceof AppError) {
|
|
210
|
+
return error.statusCode;
|
|
211
|
+
}
|
|
212
|
+
return 500;
|
|
213
|
+
}
|
|
214
|
+
export function errorHandler(options = {}) {
|
|
215
|
+
return async (c, next) => {
|
|
216
|
+
try {
|
|
217
|
+
await next();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
const requestId = c.req.header("x-request-id");
|
|
221
|
+
const statusCode = getStatusCode(error);
|
|
222
|
+
const response = toApiError(error, requestId);
|
|
223
|
+
options.onError?.(error, requestId !== undefined ? { requestId, statusCode } : { statusCode });
|
|
224
|
+
return c.json(response, statusCode);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
// ============================================
|
|
229
|
+
// Utility Functions
|
|
230
|
+
// ============================================
|
|
231
|
+
function getDefaultStatusCode(code) {
|
|
232
|
+
switch (code) {
|
|
233
|
+
case ERROR_CODES.BAD_REQUEST:
|
|
234
|
+
case ERROR_CODES.VALIDATION_ERROR:
|
|
235
|
+
return 400;
|
|
236
|
+
case ERROR_CODES.UNAUTHORIZED:
|
|
237
|
+
return 401;
|
|
238
|
+
case ERROR_CODES.PAYMENT_REQUIRED:
|
|
239
|
+
case ERROR_CODES.SUBSCRIPTION_EXPIRED:
|
|
240
|
+
return 402;
|
|
241
|
+
case ERROR_CODES.FORBIDDEN:
|
|
242
|
+
case ERROR_CODES.FEATURE_DISABLED:
|
|
243
|
+
case ERROR_CODES.TENANT_SUSPENDED:
|
|
244
|
+
return 403;
|
|
245
|
+
case ERROR_CODES.NOT_FOUND:
|
|
246
|
+
return 404;
|
|
247
|
+
case ERROR_CODES.CONFLICT:
|
|
248
|
+
return 409;
|
|
249
|
+
case ERROR_CODES.RATE_LIMITED:
|
|
250
|
+
case ERROR_CODES.QUOTA_EXCEEDED:
|
|
251
|
+
return 429;
|
|
252
|
+
case ERROR_CODES.INTERNAL_ERROR:
|
|
253
|
+
case ERROR_CODES.DATABASE_ERROR:
|
|
254
|
+
return 500;
|
|
255
|
+
case ERROR_CODES.EXTERNAL_SERVICE_ERROR:
|
|
256
|
+
return 502;
|
|
257
|
+
case ERROR_CODES.SERVICE_UNAVAILABLE:
|
|
258
|
+
return 503;
|
|
259
|
+
case ERROR_CODES.TIMEOUT:
|
|
260
|
+
return 504;
|
|
261
|
+
default:
|
|
262
|
+
return 500;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Wrap async function with error handling
|
|
267
|
+
*/
|
|
268
|
+
export function tryCatch(fn, errorHandler) {
|
|
269
|
+
return fn().catch((error) => {
|
|
270
|
+
if (errorHandler) {
|
|
271
|
+
return errorHandler(error);
|
|
272
|
+
}
|
|
273
|
+
throw error;
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Assert condition or throw error
|
|
278
|
+
*/
|
|
279
|
+
export function assert(condition, message) {
|
|
280
|
+
if (!condition) {
|
|
281
|
+
throw new AppError({
|
|
282
|
+
code: ERROR_CODES.BAD_REQUEST,
|
|
283
|
+
message,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Assert not null/undefined or throw NotFoundError
|
|
289
|
+
*/
|
|
290
|
+
export function assertFound(value, resource, id) {
|
|
291
|
+
if (value === null || value === undefined) {
|
|
292
|
+
throw new NotFoundError(resource, id);
|
|
293
|
+
}
|
|
294
|
+
}
|
package/package.json
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/errors",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Unified error handling and API error responses",
|
|
5
5
|
"private": false,
|
|
6
|
+
"nebutra": {
|
|
7
|
+
"status": "foundation",
|
|
8
|
+
"graph": "core",
|
|
9
|
+
"productionReady": false
|
|
10
|
+
},
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"type": "module",
|
|
8
|
-
"main": "./
|
|
9
|
-
"types": "./
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
10
15
|
"exports": {
|
|
11
|
-
".":
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
}
|
|
12
21
|
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
13
25
|
"devDependencies": {
|
|
14
26
|
"@types/node": "^22.19.15",
|
|
15
27
|
"typescript": "^5.9.3",
|
|
@@ -28,7 +40,8 @@
|
|
|
28
40
|
"access": "public"
|
|
29
41
|
},
|
|
30
42
|
"scripts": {
|
|
43
|
+
"build": "tsc -p tsconfig.json",
|
|
31
44
|
"test": "vitest run",
|
|
32
|
-
"typecheck": "tsc --noEmit"
|
|
45
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
33
46
|
}
|
|
34
47
|
}
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @nebutra/errors@0.1.1 test /home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/platform/errors
|
|
3
|
-
> vitest run
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[1m[30m[46m RUN [49m[39m[22m [36mv4.1.4 [39m[90m/home/runner/work/Nebutra-Sailor/Nebutra-Sailor/packages/platform/errors[39m
|
|
7
|
-
|
|
8
|
-
[32m✓[39m src/index.test.ts [2m([22m[2m1 test[22m[2m)[22m[32m 50[2mms[22m[39m
|
|
9
|
-
|
|
10
|
-
[2m Test Files [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
11
|
-
[2m Tests [22m [1m[32m1 passed[39m[22m[90m (1)[39m
|
|
12
|
-
[2m Start at [22m 08:14:48
|
|
13
|
-
[2m Duration [22m 1.58s[2m (transform 455ms, setup 0ms, import 577ms, tests 50ms, environment 0ms)[22m
|
|
14
|
-
|
package/AGENTS.md
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# AGENTS.md — packages/errors
|
|
2
|
-
|
|
3
|
-
Execution contract for Nebutra's shared error semantics package.
|
|
4
|
-
|
|
5
|
-
## Scope
|
|
6
|
-
|
|
7
|
-
Applies to everything under `packages/platform/errors/`.
|
|
8
|
-
|
|
9
|
-
This package owns canonical application error codes, typed error classes,
|
|
10
|
-
API-safe serialization helpers, and the framework-agnostic error middleware
|
|
11
|
-
shape. It is the shared error vocabulary layer, not the place for service-local
|
|
12
|
-
logging policy or provider-specific exception translation.
|
|
13
|
-
|
|
14
|
-
## Source Of Truth
|
|
15
|
-
|
|
16
|
-
- Public package surface: `package.json`, `src/index.ts`
|
|
17
|
-
- Canonical error code catalog: `ERROR_CODES`
|
|
18
|
-
- Base error class and serialization behavior: `AppError`, `AppErrorOptions`,
|
|
19
|
-
`AppError.toJSON()`
|
|
20
|
-
- Specific error subclasses and their default status semantics:
|
|
21
|
-
`ValidationError`, `UnauthorizedError`, `ForbiddenError`, `NotFoundError`,
|
|
22
|
-
`ConflictError`, `RateLimitError`, `QuotaExceededError`,
|
|
23
|
-
`ExternalServiceError`, `DatabaseError`
|
|
24
|
-
- API response and status helpers: `toApiError`, `getStatusCode`
|
|
25
|
-
- Framework boundary for request-safe error responses: `errorHandler`
|
|
26
|
-
- Assertion and wrapper helpers: `tryCatch`, `assert`, `assertFound`
|
|
27
|
-
|
|
28
|
-
Treat `README.md` as descriptive only. If docs drift, update `src/index.ts`
|
|
29
|
-
instead of preserving outdated examples.
|
|
30
|
-
|
|
31
|
-
## Contract Boundaries
|
|
32
|
-
|
|
33
|
-
- Keep `ERROR_CODES` as the canonical shared vocabulary. Additive changes are
|
|
34
|
-
safest; renames or removals are compatibility changes for callers, handlers,
|
|
35
|
-
and logs.
|
|
36
|
-
- Treat `AppError` and `ApiErrorResponse` as the stable serialization boundary.
|
|
37
|
-
Do not leak raw unknown errors or provider-specific details through
|
|
38
|
-
`toApiError`.
|
|
39
|
-
- Preserve default status-code mapping in `getDefaultStatusCode()` unless the
|
|
40
|
-
compatibility change is deliberate and coordinated with consumers.
|
|
41
|
-
- Keep `errorHandler()` framework-agnostic and request-safe. Its job is to map
|
|
42
|
-
errors into structured JSON and invoke the optional `onError` callback, not
|
|
43
|
-
to own logging destinations or transport-specific side effects.
|
|
44
|
-
- Preserve the distinction between operational and non-operational errors.
|
|
45
|
-
`DatabaseError` and other server faults should not be quietly normalized into
|
|
46
|
-
benign client semantics.
|
|
47
|
-
- Keep assertion helpers thin wrappers over the shared error types. Do not
|
|
48
|
-
embed app-specific policy or database lookups here.
|
|
49
|
-
|
|
50
|
-
## Generated And Derived Files
|
|
51
|
-
|
|
52
|
-
- This package currently exports source directly and has no checked-in
|
|
53
|
-
generated source of truth.
|
|
54
|
-
- Do not hand-edit future build output, coverage artifacts, or transient
|
|
55
|
-
TypeScript output.
|
|
56
|
-
- If packaging changes later, update the source files above rather than derived
|
|
57
|
-
artifacts.
|
|
58
|
-
|
|
59
|
-
## Validation
|
|
60
|
-
|
|
61
|
-
- Error type, code, or middleware changes:
|
|
62
|
-
`pnpm --filter @nebutra/errors exec tsc --noEmit`
|
|
63
|
-
- Because this package currently has no package-local tests, verify the
|
|
64
|
-
narrowest downstream consumer that exercises the changed error contract when
|
|
65
|
-
behavior changes are non-trivial.
|
package/CHANGELOG.md
DELETED
package/src/index.test.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { CapabilityError, toApiError } from "./index";
|
|
3
|
-
|
|
4
|
-
describe("CapabilityError", () => {
|
|
5
|
-
it("serializes a suggestion for API callers", () => {
|
|
6
|
-
const error = new CapabilityError("provider-registry", "Missing local model", {
|
|
7
|
-
suggestion: "Run provider-registry:doctor and install a local model.",
|
|
8
|
-
metadata: { provider: "ollama" },
|
|
9
|
-
});
|
|
10
|
-
|
|
11
|
-
expect(error.capability).toBe("provider-registry");
|
|
12
|
-
expect(error.suggestion).toContain("provider-registry:doctor");
|
|
13
|
-
expect(error.toJSON()).toMatchObject({
|
|
14
|
-
code: "EXTERNAL_SERVICE_ERROR",
|
|
15
|
-
suggestion: "Run provider-registry:doctor and install a local model.",
|
|
16
|
-
metadata: { capability: "provider-registry", provider: "ollama" },
|
|
17
|
-
});
|
|
18
|
-
expect(toApiError(error).error.details).toMatchObject({
|
|
19
|
-
suggestion: "Run provider-registry:doctor and install a local model.",
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
});
|