@rpcbase/api 0.148.0 → 0.150.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,162 @@ 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/requestValidation.ts
27501
+ var getErrorFields = (issues) => {
27502
+ const fields = {};
27503
+ for (const issue of issues) {
27504
+ const path = issue.path.map(String).join(".");
27505
+ if (!path) continue;
27506
+ const messages = fields[path] ?? [];
27507
+ messages.push(issue.message);
27508
+ fields[path] = messages;
27509
+ }
27510
+ return Object.keys(fields).length > 0 ? fields : void 0;
27511
+ };
27512
+ var getApiRoutePath = (route) => {
27513
+ if (typeof route === "string") return route;
27514
+ return "ROUTE" in route ? route.ROUTE : route.Route;
27515
+ };
27516
+ var parseApiRequest = (schema, input) => {
27517
+ const result = schema.safeParse(input);
27518
+ if (result.success) return result.data;
27519
+ throw new ApiHttpError(400, "invalid_request", {
27520
+ message: "Invalid request payload.",
27521
+ fields: getErrorFields(result.error.issues),
27522
+ cause: result.error
27523
+ });
27524
+ };
27525
+ //#endregion
27526
+ //#region src/initApi.ts
27374
27527
  var loadInitApiClient = async () => {
27375
27528
  return (await import("@rpcbase/client")).initApiClient;
27376
27529
  };
@@ -27379,14 +27532,24 @@ process.env = {
27379
27532
  ...__rb_env__,
27380
27533
  ...process.env
27381
27534
  };
27382
- var initApi = async ({ app, routes, cors: corsOptions }) => {
27535
+ var withApiRequestIdExposed = (options) => {
27536
+ const configured = options?.exposedHeaders;
27537
+ const exposedHeaders = Array.isArray(configured) ? [...configured] : configured ? [configured] : [];
27538
+ if (!exposedHeaders.some((header) => header.toLowerCase() === "x-request-id")) exposedHeaders.push(API_REQUEST_ID_HEADER);
27539
+ return {
27540
+ ...options,
27541
+ exposedHeaders
27542
+ };
27543
+ };
27544
+ var initApi = async ({ app, routes, cors: corsOptions, errors }) => {
27383
27545
  await (await loadInitApiClient())({ app });
27384
- const registerHandler = (method, path, handler) => {
27546
+ const registerHandler = (method, route, handler) => {
27547
+ const path = getApiRoutePath(route);
27385
27548
  assert(path.startsWith("/api"), "API routes must start with /api");
27386
27549
  app[method](path, async (req, res, next) => {
27387
- const payload = method === "get" ? req.query : req.body ?? {};
27550
+ const requestPayload = method === "get" ? req.query : req.body ?? {};
27388
27551
  try {
27389
- const result = await handler(payload, {
27552
+ const result = await handler(typeof route === "string" ? requestPayload : parseApiRequest(route.requestSchema, requestPayload), {
27390
27553
  req,
27391
27554
  res
27392
27555
  });
@@ -27398,28 +27561,24 @@ var initApi = async ({ app, routes, cors: corsOptions }) => {
27398
27561
  }
27399
27562
  });
27400
27563
  };
27564
+ const createApiMethod = (method) => ((route, handler) => {
27565
+ registerHandler(method, route, handler);
27566
+ });
27401
27567
  const api = {
27402
27568
  use: (path, middleware) => {
27403
27569
  app.use(path, middleware);
27404
27570
  },
27405
- get: (path, handler) => {
27406
- registerHandler("get", path, handler);
27407
- },
27408
- post: (path, handler) => {
27409
- registerHandler("post", path, handler);
27410
- },
27411
- put: (path, handler) => {
27412
- registerHandler("put", path, handler);
27413
- },
27414
- delete: (path, handler) => {
27415
- registerHandler("delete", path, handler);
27416
- }
27571
+ get: createApiMethod("get"),
27572
+ post: createApiMethod("post"),
27573
+ put: createApiMethod("put"),
27574
+ delete: createApiMethod("delete")
27417
27575
  };
27418
- app.use("/api", (0, import_lib.default)(corsOptions));
27576
+ app.use("/api", (0, import_lib.default)(withApiRequestIdExposed(corsOptions)));
27419
27577
  app.use("/api", (0, import_express.json)({ limit: "8mb" }));
27420
27578
  app.use("/api", function __FIRST_API_MIDDLEWARE__(_req, _res, next) {
27421
27579
  next();
27422
27580
  });
27581
+ app.use("/api", apiRequestIdMiddleware);
27423
27582
  const routeEntries = Object.entries(routes);
27424
27583
  const totalRoutes = routeEntries.length;
27425
27584
  const loadedModules = /* @__PURE__ */ new Map();
@@ -27443,19 +27602,33 @@ var initApi = async ({ app, routes, cors: corsOptions }) => {
27443
27602
  }
27444
27603
  console.log("Done loading routes");
27445
27604
  app.use("/api", (req, res) => {
27446
- res.status(404).json({ error: `Not Found: ${req.method}:${req.originalUrl}` });
27605
+ const requestId = getApiRequestId(res);
27606
+ res.status(404).json(toApiErrorResponse(new ApiHttpError(404, "not_found", { message: `Not Found: ${req.method}:${req.originalUrl}` }), requestId));
27447
27607
  });
27448
27608
  app.use("/api", (err, _req, res, next) => {
27449
27609
  if (res.headersSent) return next(err);
27610
+ if (isApiHttpError(err)) {
27611
+ next(err);
27612
+ return;
27613
+ }
27450
27614
  const errLike = err;
27451
27615
  const code = typeof errLike?.code === "string" ? errLike.code : null;
27452
27616
  const statusCode = typeof errLike?.statusCode === "number" ? errLike.statusCode : typeof errLike?.status === "number" ? errLike.status : null;
27453
27617
  if (code === "invalid_pagination") {
27454
- res.status(statusCode ?? 400).json({ error: code });
27618
+ next(new ApiHttpError(statusCode && Number.isInteger(statusCode) && statusCode >= 400 && statusCode <= 599 ? statusCode : 400, code));
27619
+ return;
27620
+ }
27621
+ if (errLike.type === "entity.parse.failed") {
27622
+ next(new ApiHttpError(400, "invalid_json", { message: "Request body must contain valid JSON." }));
27623
+ return;
27624
+ }
27625
+ if (errLike.type === "entity.too.large") {
27626
+ next(new ApiHttpError(413, "payload_too_large", { message: "Request body is too large." }));
27455
27627
  return;
27456
27628
  }
27457
27629
  next(err);
27458
27630
  });
27631
+ app.use("/api", createApiErrorMiddleware(errors));
27459
27632
  };
27460
27633
  //#endregion
27461
27634
  //#region src/agentApiCatalog.ts
@@ -27471,6 +27644,6 @@ var AGENT_API_BLOCKED_REASONS = [
27471
27644
  "other"
27472
27645
  ];
27473
27646
  //#endregion
27474
- export { AGENT_API_BLOCKED_REASONS, MOCK_TYPE, initApi };
27647
+ export { AGENT_API_BLOCKED_REASONS, API_REQUEST_ID_HEADER, ApiHttpError, MOCK_TYPE, apiErrorMiddleware, apiRequestIdMiddleware, createApiErrorMiddleware, getApiRequestId, initApi, isApiErrorCode, isApiHttpError, sendApiHttpError, toApiErrorResponse };
27475
27648
 
27476
27649
  //# sourceMappingURL=index.js.map