@rpcbase/api 0.148.0 → 0.149.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.
@@ -0,0 +1,48 @@
1
+ import { ErrorRequestHandler, Request, RequestHandler, Response } from 'express';
2
+ export declare const API_REQUEST_ID_HEADER = "x-request-id";
3
+ export type ApiErrorFields = Record<string, string[]>;
4
+ export type ApiErrorResponse = {
5
+ success: false;
6
+ error: string;
7
+ message?: string;
8
+ fields?: ApiErrorFields;
9
+ requestId: string;
10
+ };
11
+ export type ApiHttpErrorOptions = {
12
+ message?: string;
13
+ fields?: ApiErrorFields;
14
+ cause?: unknown;
15
+ };
16
+ export declare const isApiErrorCode: (value: unknown) => value is string;
17
+ export declare class ApiHttpError extends Error {
18
+ readonly __rpcbaseApiHttpError = true;
19
+ readonly status: number;
20
+ readonly code: string;
21
+ readonly publicMessage?: string;
22
+ readonly fields?: ApiErrorFields;
23
+ constructor(status: number, code: string, options?: ApiHttpErrorOptions);
24
+ }
25
+ type ApiHttpErrorLike = Error & {
26
+ __rpcbaseApiHttpError: true;
27
+ status: number;
28
+ code: string;
29
+ publicMessage?: string;
30
+ fields?: ApiErrorFields;
31
+ };
32
+ export declare const isApiHttpError: (error: unknown) => error is ApiHttpErrorLike;
33
+ export declare const getApiRequestId: (res: Response) => string;
34
+ export declare const apiRequestIdMiddleware: RequestHandler;
35
+ export declare const toApiErrorResponse: (error: Pick<ApiHttpErrorLike, "code" | "publicMessage" | "fields">, requestId: string) => ApiErrorResponse;
36
+ export declare const sendApiHttpError: (res: Response, error: ApiHttpErrorLike) => void;
37
+ export type ApiErrorMiddlewareContext = {
38
+ requestId: string;
39
+ req: Request;
40
+ };
41
+ export type ApiErrorMiddlewareOptions = {
42
+ internalMessage?: string;
43
+ onError?: (error: unknown, context: ApiErrorMiddlewareContext) => void | Promise<void>;
44
+ };
45
+ export declare const createApiErrorMiddleware: (options?: ApiErrorMiddlewareOptions) => ErrorRequestHandler;
46
+ export declare const apiErrorMiddleware: ErrorRequestHandler<import('express-serve-static-core').ParamsDictionary, any, any, import('qs').ParsedQs, Record<string, any>>;
47
+ export {};
48
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAGrF,eAAO,MAAM,qBAAqB,iBAAiB,CAAA;AAKnD,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,KAAK,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,cAAc,CAAA;IACvB,KAAK,CAAC,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,MACQ,CAAA;AAEjE,qBAAa,YAAa,SAAQ,KAAK;IACrC,QAAQ,CAAC,qBAAqB,QAAO;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAA;gBAEpB,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,mBAAwB;CAgB5E;AAED,KAAK,gBAAgB,GAAG,KAAK,GAAG;IAC9B,qBAAqB,EAAE,IAAI,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,MAAM,CAAC,EAAE,cAAc,CAAA;CACxB,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,gBAWxD,CAAA;AAqCD,eAAO,MAAM,eAAe,GAAI,KAAK,QAAQ,KAAG,MAuB/C,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,cAGpC,CAAA;AAED,eAAO,MAAM,kBAAkB,GAC7B,OAAO,IAAI,CAAC,gBAAgB,EAAE,MAAM,GAAG,eAAe,GAAG,QAAQ,CAAC,EAClE,WAAW,MAAM,KAChB,gBAMD,CAAA;AAEF,eAAO,MAAM,gBAAgB,GAAI,KAAK,QAAQ,EAAE,OAAO,gBAAgB,KAAG,IAGzE,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,CAAA;IACjB,GAAG,EAAE,OAAO,CAAA;CACb,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,yBAAyB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvF,CAAA;AA6BD,eAAO,MAAM,wBAAwB,GACnC,UAAS,yBAA8B,KACtC,mBA6BF,CAAA;AAED,eAAO,MAAM,kBAAkB,iIAA6B,CAAA"}
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './types';
2
2
  export * from './initApi';
3
+ export * from './errors';
3
4
  export * from './pagination';
4
5
  export * from './agentApiCatalog';
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,WAAW,CAAA;AACzB,cAAc,UAAU,CAAA;AACxB,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA"}
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ import * as m$3 from "node:querystring";
17
17
  import * as m$2 from "node:buffer";
18
18
  import * as m$1 from "node:net";
19
19
  import * as m from "util";
20
+ import { randomUUID } from "node:crypto";
20
21
  //#region \0rolldown/runtime.js
21
22
  var __create = Object.create;
22
23
  var __defProp = Object.defineProperty;
@@ -27367,10 +27368,136 @@ var require_lib = /* @__PURE__ */ __commonJSMin(((exports, module) => {
27367
27368
  })();
27368
27369
  }));
27369
27370
  //#endregion
27370
- //#region src/initApi.ts
27371
+ //#region src/errors.ts
27371
27372
  var import_bluebird = /* @__PURE__ */ __toESM(require_bluebird(), 1);
27372
27373
  var import_express = require_express();
27373
27374
  var import_lib = /* @__PURE__ */ __toESM(require_lib(), 1);
27375
+ var API_REQUEST_ID_HEADER = "x-request-id";
27376
+ var API_REQUEST_ID_LOCAL = "rpcbaseApiRequestId";
27377
+ var API_ERROR_CODE_PATTERN = /^[a-z][A-Za-z0-9_.:-]*$/u;
27378
+ var isApiErrorCode = (value) => typeof value === "string" && API_ERROR_CODE_PATTERN.test(value);
27379
+ var ApiHttpError = class extends Error {
27380
+ __rpcbaseApiHttpError = true;
27381
+ status;
27382
+ code;
27383
+ publicMessage;
27384
+ fields;
27385
+ constructor(status, code, options = {}) {
27386
+ if (!Number.isInteger(status) || status < 400 || status > 599) throw new RangeError("ApiHttpError status must be an integer between 400 and 599");
27387
+ if (!isApiErrorCode(code)) throw new TypeError("ApiHttpError code must start with a lowercase letter and contain only letters, numbers, '.', ':', '_' or '-'");
27388
+ super(options.message ?? code, { cause: options.cause });
27389
+ this.name = "ApiHttpError";
27390
+ this.status = status;
27391
+ this.code = code;
27392
+ this.publicMessage = options.message;
27393
+ this.fields = options.fields;
27394
+ }
27395
+ };
27396
+ var isApiHttpError = (error) => {
27397
+ if (error instanceof ApiHttpError) return true;
27398
+ if (!error || typeof error !== "object") return false;
27399
+ const candidate = error;
27400
+ return candidate.__rpcbaseApiHttpError === true && Number.isInteger(candidate.status) && typeof candidate.status === "number" && candidate.status >= 400 && candidate.status <= 599 && isApiErrorCode(candidate.code);
27401
+ };
27402
+ var getExposedApiHttpError = (error) => {
27403
+ if (!error || typeof error !== "object") return null;
27404
+ const candidate = error;
27405
+ if (candidate.expose !== true) return null;
27406
+ const status = [candidate.status, candidate.statusCode].find((value) => typeof value === "number" && Number.isInteger(value) && value >= 400 && value < 500);
27407
+ if (typeof status !== "number") return null;
27408
+ const code = isApiErrorCode(candidate.type) ? candidate.type : isApiErrorCode(candidate.code) ? candidate.code : "http_error";
27409
+ const message = typeof candidate.message === "string" && candidate.message.trim() ? candidate.message.trim() : void 0;
27410
+ return new ApiHttpError(status, code, {
27411
+ ...message ? { message } : {},
27412
+ cause: error
27413
+ });
27414
+ };
27415
+ var getApiRequestId = (res) => {
27416
+ res.locals ??= {};
27417
+ const existing = res.locals[API_REQUEST_ID_LOCAL];
27418
+ if (typeof existing === "string" && existing.trim()) {
27419
+ const requestId = existing.trim();
27420
+ res.locals[API_REQUEST_ID_LOCAL] = requestId;
27421
+ if (!res.headersSent) res.setHeader(API_REQUEST_ID_HEADER, requestId);
27422
+ return requestId;
27423
+ }
27424
+ const existingHeader = res.getHeader(API_REQUEST_ID_HEADER);
27425
+ if (typeof existingHeader === "string" && existingHeader.trim()) {
27426
+ const requestId = existingHeader.trim();
27427
+ res.locals[API_REQUEST_ID_LOCAL] = requestId;
27428
+ if (!res.headersSent) res.setHeader(API_REQUEST_ID_HEADER, requestId);
27429
+ return requestId;
27430
+ }
27431
+ const requestId = randomUUID();
27432
+ res.locals[API_REQUEST_ID_LOCAL] = requestId;
27433
+ if (!res.headersSent) res.setHeader(API_REQUEST_ID_HEADER, requestId);
27434
+ return requestId;
27435
+ };
27436
+ var apiRequestIdMiddleware = (_req, res, next) => {
27437
+ getApiRequestId(res);
27438
+ next();
27439
+ };
27440
+ var toApiErrorResponse = (error, requestId) => ({
27441
+ success: false,
27442
+ error: error.code,
27443
+ ...error.publicMessage ? { message: error.publicMessage } : {},
27444
+ ...error.fields ? { fields: error.fields } : {},
27445
+ requestId
27446
+ });
27447
+ var sendApiHttpError = (res, error) => {
27448
+ const requestId = getApiRequestId(res);
27449
+ res.status(error.status).json(toApiErrorResponse(error, requestId));
27450
+ };
27451
+ var reportApiError = (error, context, onError) => {
27452
+ if (onError) {
27453
+ try {
27454
+ const reportingResult = onError(error, context);
27455
+ if (reportingResult && typeof reportingResult.then === "function") reportingResult.catch((reportingError) => {
27456
+ console.error("[rpcbase/api] error reporter failed", reportingError);
27457
+ });
27458
+ } catch (reportingError) {
27459
+ console.error("[rpcbase/api] error reporter failed", reportingError);
27460
+ }
27461
+ return;
27462
+ }
27463
+ console.error("[rpcbase/api] API error", {
27464
+ requestId: context.requestId,
27465
+ method: context.req.method,
27466
+ path: context.req.originalUrl,
27467
+ error
27468
+ });
27469
+ };
27470
+ var createApiErrorMiddleware = (options = {}) => (error, req, res, next) => {
27471
+ if (res.headersSent) {
27472
+ next(error);
27473
+ return;
27474
+ }
27475
+ const requestId = getApiRequestId(res);
27476
+ const context = {
27477
+ requestId,
27478
+ req
27479
+ };
27480
+ if (isApiHttpError(error)) {
27481
+ if (error.status >= 500) reportApiError(error, context, options.onError);
27482
+ sendApiHttpError(res, error);
27483
+ return;
27484
+ }
27485
+ const exposedError = getExposedApiHttpError(error);
27486
+ if (exposedError) {
27487
+ sendApiHttpError(res, exposedError);
27488
+ return;
27489
+ }
27490
+ reportApiError(error, context, options.onError);
27491
+ res.status(500).json({
27492
+ success: false,
27493
+ error: "internal_error",
27494
+ ...options.internalMessage ? { message: options.internalMessage } : {},
27495
+ requestId
27496
+ });
27497
+ };
27498
+ var apiErrorMiddleware = createApiErrorMiddleware();
27499
+ //#endregion
27500
+ //#region src/initApi.ts
27374
27501
  var loadInitApiClient = async () => {
27375
27502
  return (await import("@rpcbase/client")).initApiClient;
27376
27503
  };
@@ -27379,7 +27506,16 @@ process.env = {
27379
27506
  ...__rb_env__,
27380
27507
  ...process.env
27381
27508
  };
27382
- var initApi = async ({ app, routes, cors: corsOptions }) => {
27509
+ var withApiRequestIdExposed = (options) => {
27510
+ const configured = options?.exposedHeaders;
27511
+ const exposedHeaders = Array.isArray(configured) ? [...configured] : configured ? [configured] : [];
27512
+ if (!exposedHeaders.some((header) => header.toLowerCase() === "x-request-id")) exposedHeaders.push(API_REQUEST_ID_HEADER);
27513
+ return {
27514
+ ...options,
27515
+ exposedHeaders
27516
+ };
27517
+ };
27518
+ var initApi = async ({ app, routes, cors: corsOptions, errors }) => {
27383
27519
  await (await loadInitApiClient())({ app });
27384
27520
  const registerHandler = (method, path, handler) => {
27385
27521
  assert(path.startsWith("/api"), "API routes must start with /api");
@@ -27415,11 +27551,12 @@ var initApi = async ({ app, routes, cors: corsOptions }) => {
27415
27551
  registerHandler("delete", path, handler);
27416
27552
  }
27417
27553
  };
27418
- app.use("/api", (0, import_lib.default)(corsOptions));
27554
+ app.use("/api", (0, import_lib.default)(withApiRequestIdExposed(corsOptions)));
27419
27555
  app.use("/api", (0, import_express.json)({ limit: "8mb" }));
27420
27556
  app.use("/api", function __FIRST_API_MIDDLEWARE__(_req, _res, next) {
27421
27557
  next();
27422
27558
  });
27559
+ app.use("/api", apiRequestIdMiddleware);
27423
27560
  const routeEntries = Object.entries(routes);
27424
27561
  const totalRoutes = routeEntries.length;
27425
27562
  const loadedModules = /* @__PURE__ */ new Map();
@@ -27443,19 +27580,33 @@ var initApi = async ({ app, routes, cors: corsOptions }) => {
27443
27580
  }
27444
27581
  console.log("Done loading routes");
27445
27582
  app.use("/api", (req, res) => {
27446
- res.status(404).json({ error: `Not Found: ${req.method}:${req.originalUrl}` });
27583
+ const requestId = getApiRequestId(res);
27584
+ res.status(404).json(toApiErrorResponse(new ApiHttpError(404, "not_found", { message: `Not Found: ${req.method}:${req.originalUrl}` }), requestId));
27447
27585
  });
27448
27586
  app.use("/api", (err, _req, res, next) => {
27449
27587
  if (res.headersSent) return next(err);
27588
+ if (isApiHttpError(err)) {
27589
+ next(err);
27590
+ return;
27591
+ }
27450
27592
  const errLike = err;
27451
27593
  const code = typeof errLike?.code === "string" ? errLike.code : null;
27452
27594
  const statusCode = typeof errLike?.statusCode === "number" ? errLike.statusCode : typeof errLike?.status === "number" ? errLike.status : null;
27453
27595
  if (code === "invalid_pagination") {
27454
- res.status(statusCode ?? 400).json({ error: code });
27596
+ next(new ApiHttpError(statusCode && Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599 ? statusCode : 400, code));
27597
+ return;
27598
+ }
27599
+ if (errLike.type === "entity.parse.failed") {
27600
+ next(new ApiHttpError(400, "invalid_json", { message: "Request body must contain valid JSON." }));
27601
+ return;
27602
+ }
27603
+ if (errLike.type === "entity.too.large") {
27604
+ next(new ApiHttpError(413, "payload_too_large", { message: "Request body is too large." }));
27455
27605
  return;
27456
27606
  }
27457
27607
  next(err);
27458
27608
  });
27609
+ app.use("/api", createApiErrorMiddleware(errors));
27459
27610
  };
27460
27611
  //#endregion
27461
27612
  //#region src/agentApiCatalog.ts
@@ -27471,6 +27622,6 @@ var AGENT_API_BLOCKED_REASONS = [
27471
27622
  "other"
27472
27623
  ];
27473
27624
  //#endregion
27474
- export { AGENT_API_BLOCKED_REASONS, MOCK_TYPE, initApi };
27625
+ export { AGENT_API_BLOCKED_REASONS, API_REQUEST_ID_HEADER, ApiHttpError, MOCK_TYPE, apiErrorMiddleware, apiRequestIdMiddleware, createApiErrorMiddleware, getApiRequestId, initApi, isApiErrorCode, isApiHttpError, sendApiHttpError, toApiErrorResponse };
27475
27626
 
27476
27627
  //# sourceMappingURL=index.js.map