@kaito-http/core 3.0.0-beta.16 → 3.0.0-beta.20

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/dist/index.cjs CHANGED
@@ -106,6 +106,9 @@ var KaitoResponse = class {
106
106
  return this._headers;
107
107
  }
108
108
  status(status) {
109
+ if (status === void 0) {
110
+ return this._status;
111
+ }
109
112
  this._status = status;
110
113
  return this;
111
114
  }
@@ -232,10 +235,10 @@ var Router = class _Router {
232
235
  const request = new KaitoRequest(url, req);
233
236
  const response = new KaitoResponse();
234
237
  try {
235
- const rootCtx = await server.getContext(request, response);
236
- const ctx = await route.through(rootCtx);
237
238
  const body = route.body ? await route.body.parse(await req.json()) : void 0;
238
239
  const query = _Router.parseQuery(route.query, url);
240
+ const rootCtx = await server.getContext(request, response);
241
+ const ctx = await route.through(rootCtx);
239
242
  const result = await route.run({
240
243
  ctx,
241
244
  body,
@@ -243,6 +246,9 @@ var Router = class _Router {
243
246
  params
244
247
  });
245
248
  if (result instanceof Response) {
249
+ if (server.enableClientResponseHints) {
250
+ result.headers.set("x-kaito-is-response", "1");
251
+ }
246
252
  return result;
247
253
  }
248
254
  return response.toResponse({
@@ -288,20 +294,21 @@ var Router = class _Router {
288
294
  };
289
295
 
290
296
  // src/server.ts
291
- function createKaitoHandler(config) {
292
- const match = config.router.freeze(config);
297
+ function createKaitoHandler(userConfig) {
298
+ const config = {
299
+ enableClientResponseHints: true,
300
+ ...userConfig
301
+ };
302
+ const handle = config.router.freeze(config);
293
303
  return async (request) => {
294
- let before = void 0;
295
304
  if (config.before) {
296
305
  const result = await config.before(request);
297
- if (result instanceof Response) {
298
- return result;
299
- }
300
- before = result;
306
+ if (result instanceof Response) return result;
301
307
  }
302
- const response = await match(request);
303
- if ("after" in config && config.after) {
304
- await config.after(before, response);
308
+ const response = await handle(request);
309
+ if (config.transform) {
310
+ const result = await config.transform(request, response);
311
+ if (result instanceof Response) return result;
305
312
  }
306
313
  return response;
307
314
  };
package/dist/index.d.cts CHANGED
@@ -34,10 +34,10 @@ type KaitoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIO
34
34
  * ```ts
35
35
  * const response = new KaitoResponse();
36
36
  *
37
- * response.status = 200;
38
- * response.header('Content-Type', 'application/json');
37
+ * response.status(200);
38
+ * response.headers.set('Content-Type', 'application/json');
39
39
  *
40
- * console.log(response.headers); // Headers { 'content-type': 'application/json' }
40
+ * console.log(response.headers); // Headers {'content-type': 'application/json'}
41
41
  * ```
42
42
  */
43
43
  declare class KaitoResponse {
@@ -45,6 +45,16 @@ declare class KaitoResponse {
45
45
  private _status;
46
46
  constructor();
47
47
  get headers(): Headers;
48
+ /**
49
+ * Gets the status code of this KaitoResponse instance
50
+ * @returns The status code
51
+ */
52
+ status(): number;
53
+ /**
54
+ * Sets the status code of this KaitoResponse instance
55
+ * @param status The status code to set
56
+ * @returns This KaitoResponse instance
57
+ */
48
58
  status(status: number): this;
49
59
  /**
50
60
  * Turn this KaitoResponse instance into a Response instance
@@ -54,26 +64,87 @@ declare class KaitoResponse {
54
64
  toResponse<T>(body: APIResponse<T>): Response;
55
65
  }
56
66
 
57
- type Before<BeforeAfterContext> = (req: Request) => Promise<BeforeAfterContext | Response>;
58
- type After<BeforeAfterContext> = (ctx: BeforeAfterContext, response: Response) => Promise<void>;
59
- type ServerConfigWithBefore<BeforeAfterContext> = {
60
- before: Before<BeforeAfterContext>;
61
- after?: After<BeforeAfterContext>;
62
- } | {
63
- before?: undefined;
64
- };
65
- type ServerConfig<ContextFrom, BeforeAfterContext> = ServerConfigWithBefore<BeforeAfterContext> & {
67
+ type Before = (req: Request) => Promise<Response | void | undefined>;
68
+ type ServerConfig<ContextFrom> = {
69
+ /**
70
+ * The root router to mount on this server.
71
+ */
66
72
  router: Router<ContextFrom, unknown, any>;
73
+ /**
74
+ * A function that is called to get the context for a request.
75
+ *
76
+ * This is useful for things like authentication, to pass in a database connection, etc.
77
+ *
78
+ * It's fine for this function to throw; if it does, the error is passed to the `onError` function.
79
+ */
67
80
  getContext: GetContext<ContextFrom>;
68
- onError(arg: {
81
+ /**
82
+ * A function that is called when an error occurs inside a route handler.
83
+ *
84
+ * The result of this function is used to determine the response status and message, and is
85
+ * always sent to the client. You could include logic to check for production vs development
86
+ * environments here, and this would also be a good place to include error tracking
87
+ * like Sentry or Rollbar.
88
+ *
89
+ * @param arg - The error and the request
90
+ * @returns A KaitoError or an object with a status and message
91
+ */
92
+ onError: (arg: {
69
93
  error: Error;
70
94
  req: KaitoRequest;
71
- }): Promise<KaitoError | {
95
+ }) => Promise<KaitoError | {
72
96
  status: number;
73
97
  message: string;
74
98
  }>;
99
+ /**
100
+ * A function that is called before every request. Most useful for bailing out early in the case of an OPTIONS request.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * before: async req => {
105
+ * if (req.method === 'OPTIONS') {
106
+ * return new Response(null, {status: 204});
107
+ * }
108
+ * }
109
+ * ```
110
+ */
111
+ before?: Before;
112
+ /**
113
+ * Transforms the response before it is sent to the client. Very useful for settings headers like CORS.
114
+ *
115
+ * You can also return a new response in this function, or just mutate the current one.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * transform: async (req, res) => {
120
+ * res.headers.set('Access-Control-Allow-Origin', 'http://localhost:3000');
121
+ * res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
122
+ * res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
123
+ * res.headers.set('Access-Control-Max-Age', '86400');
124
+ * res.headers.set('Access-Control-Allow-Credentials', 'true');
125
+ * }
126
+ * ```
127
+ */
128
+ transform?: (req: Request, res: Response) => Promise<Response | void | undefined>;
129
+ /**
130
+ * Controls whether the server includes a header to indicate non-JSON responses.
131
+ *
132
+ * When a route handler returns a `Response` object, this setting determines if
133
+ * the server adds a header indicating the response should not be parsed as JSON.
134
+ *
135
+ * The `@kaito-http/client` package checks for this header's presence:
136
+ * - If present: Returns the raw Response object
137
+ * - If absent: Attempts to parse the response as JSON
138
+ *
139
+ * You might want to disable this feature when:
140
+ * 1. Using a custom client that doesn't recognize this header
141
+ * 2. Security requirements prevent exposing framework details in headers
142
+ *
143
+ * @default true
144
+ */
145
+ enableClientResponseHints: boolean;
75
146
  };
76
- declare function createKaitoHandler<Context, BeforeAfterContext = null>(config: ServerConfig<Context, BeforeAfterContext>): (request: Request) => Promise<Response>;
147
+ declare function createKaitoHandler<Context>(userConfig: MakeOptional<ServerConfig<Context>, 'enableClientResponseHints'>): (request: Request) => Promise<Response>;
77
148
 
78
149
  type PrefixRoutesPathInner<R extends AnyRoute, Prefix extends `/${string}`> = R extends Route<infer ContextFrom, infer ContextTo, infer Result, infer Path, infer Method, infer Query, infer BodyOutput> ? Route<ContextFrom, ContextTo, Result, `${Prefix}${Path}`, Method, Query, BodyOutput> : never;
79
150
  type PrefixRoutesPath<Prefix extends `/${string}`, R extends AnyRoute> = R extends R ? PrefixRoutesPathInner<R, Prefix> : never;
@@ -88,17 +159,17 @@ declare class Router<ContextFrom, ContextTo, R extends AnyRoute> {
88
159
  private static parseQuery;
89
160
  constructor(options: RouterState<R, ContextFrom, ContextTo>);
90
161
  get routes(): Set<R>;
91
- add: <Result, Path extends string, Method extends KaitoMethod, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(method: Method, path: Path, route: (Method extends "GET" ? Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "body" | "path" | "method" | "through"> : Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "path" | "method" | "through">) | Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>["run"]) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, Method, Query, Body>>;
162
+ add: <Result, Path extends string, Method extends KaitoMethod, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(method: Method, path: Path, route: (Method extends "GET" ? Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "body" | "path" | "method" | "through"> : Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "path" | "method" | "through">) | Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>["run"]) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>>;
92
163
  readonly merge: <PathPrefix extends `/${string}`, OtherRoutes extends AnyRoute>(pathPrefix: PathPrefix, other: Router<ContextFrom, unknown, OtherRoutes>) => Router<ContextFrom, ContextTo, Extract<R | PrefixRoutesPath<PathPrefix, OtherRoutes>, AnyRoute>>;
93
- freeze: (server: Omit<ServerConfig<ContextFrom, any>, "router">) => (req: Request) => Promise<Response>;
164
+ freeze: (server: Omit<ServerConfig<ContextFrom>, "router">) => (req: Request) => Promise<Response>;
94
165
  private readonly method;
95
- get: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "GET", Query, Body>, "body" | "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "GET", Query, Body>>;
96
- post: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "POST", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "POST", Query, Body>>;
97
- put: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PUT", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "PUT", Query, Body>>;
98
- patch: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PATCH", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "PATCH", Query, Body>>;
99
- delete: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "DELETE", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "DELETE", Query, Body>>;
100
- head: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "HEAD", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "HEAD", Query, Body>>;
101
- options: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "OPTIONS", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "OPTIONS", Query, Body>>;
166
+ get: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "GET", Query, Body>, "body" | "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "GET", Query, Body>>;
167
+ post: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "POST", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "POST", Query, Body>>;
168
+ put: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PUT", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "PUT", Query, Body>>;
169
+ patch: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PATCH", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "PATCH", Query, Body>>;
170
+ delete: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "DELETE", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "DELETE", Query, Body>>;
171
+ head: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "HEAD", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "HEAD", Query, Body>>;
172
+ options: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "OPTIONS", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "OPTIONS", Query, Body>>;
102
173
  through: <NextContext>(transform: (context: ContextTo) => Promise<NextContext>) => Router<ContextFrom, NextContext, R>;
103
174
  }
104
175
 
@@ -114,6 +185,7 @@ type SuccessfulAPIResponse<T> = {
114
185
  };
115
186
  type APIResponse<T> = ErroredAPIResponse | SuccessfulAPIResponse<T>;
116
187
  type AnyResponse = APIResponse<unknown>;
188
+ type MakeOptional<T, K extends keyof T> = T extends T ? Omit<T, K> & Partial<Pick<T, K>> : never;
117
189
  type ExtractRouteParams<T extends string> = string extends T ? Record<string, string> : T extends `${string}:${infer Param}/${infer Rest}` ? {
118
190
  [k in Param | keyof ExtractRouteParams<Rest>]: string;
119
191
  } : T extends `${string}:${infer Param}` ? {
@@ -172,4 +244,4 @@ type Route<ContextFrom, ContextTo, Result, Path extends string, Method extends K
172
244
  };
173
245
  type AnyRoute<ContextFrom = any, ContextTo = any> = Route<ContextFrom, ContextTo, any, any, any, AnyQueryDefinition, any>;
174
246
 
175
- export { type APIResponse, type After, type AnyQueryDefinition, type AnyResponse, type AnyRoute, type Before, type ErroredAPIResponse, type ExtractRouteParams, type GetContext, type InferParsable, type InferRoutes, KaitoError, type KaitoMethod, KaitoRequest, type Parsable, type Route, type RouteArgument, Router, type RouterState, type ServerConfig, type ServerConfigWithBefore, type SuccessfulAPIResponse, type Through, WrappedError, createKaitoHandler, createUtilities, parsable };
247
+ export { type APIResponse, type AnyQueryDefinition, type AnyResponse, type AnyRoute, type Before, type ErroredAPIResponse, type ExtractRouteParams, type GetContext, type InferParsable, type InferRoutes, KaitoError, type KaitoMethod, KaitoRequest, type MakeOptional, type Parsable, type Route, type RouteArgument, Router, type RouterState, type ServerConfig, type SuccessfulAPIResponse, type Through, WrappedError, createKaitoHandler, createUtilities, parsable };
package/dist/index.d.ts CHANGED
@@ -34,10 +34,10 @@ type KaitoMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIO
34
34
  * ```ts
35
35
  * const response = new KaitoResponse();
36
36
  *
37
- * response.status = 200;
38
- * response.header('Content-Type', 'application/json');
37
+ * response.status(200);
38
+ * response.headers.set('Content-Type', 'application/json');
39
39
  *
40
- * console.log(response.headers); // Headers { 'content-type': 'application/json' }
40
+ * console.log(response.headers); // Headers {'content-type': 'application/json'}
41
41
  * ```
42
42
  */
43
43
  declare class KaitoResponse {
@@ -45,6 +45,16 @@ declare class KaitoResponse {
45
45
  private _status;
46
46
  constructor();
47
47
  get headers(): Headers;
48
+ /**
49
+ * Gets the status code of this KaitoResponse instance
50
+ * @returns The status code
51
+ */
52
+ status(): number;
53
+ /**
54
+ * Sets the status code of this KaitoResponse instance
55
+ * @param status The status code to set
56
+ * @returns This KaitoResponse instance
57
+ */
48
58
  status(status: number): this;
49
59
  /**
50
60
  * Turn this KaitoResponse instance into a Response instance
@@ -54,26 +64,87 @@ declare class KaitoResponse {
54
64
  toResponse<T>(body: APIResponse<T>): Response;
55
65
  }
56
66
 
57
- type Before<BeforeAfterContext> = (req: Request) => Promise<BeforeAfterContext | Response>;
58
- type After<BeforeAfterContext> = (ctx: BeforeAfterContext, response: Response) => Promise<void>;
59
- type ServerConfigWithBefore<BeforeAfterContext> = {
60
- before: Before<BeforeAfterContext>;
61
- after?: After<BeforeAfterContext>;
62
- } | {
63
- before?: undefined;
64
- };
65
- type ServerConfig<ContextFrom, BeforeAfterContext> = ServerConfigWithBefore<BeforeAfterContext> & {
67
+ type Before = (req: Request) => Promise<Response | void | undefined>;
68
+ type ServerConfig<ContextFrom> = {
69
+ /**
70
+ * The root router to mount on this server.
71
+ */
66
72
  router: Router<ContextFrom, unknown, any>;
73
+ /**
74
+ * A function that is called to get the context for a request.
75
+ *
76
+ * This is useful for things like authentication, to pass in a database connection, etc.
77
+ *
78
+ * It's fine for this function to throw; if it does, the error is passed to the `onError` function.
79
+ */
67
80
  getContext: GetContext<ContextFrom>;
68
- onError(arg: {
81
+ /**
82
+ * A function that is called when an error occurs inside a route handler.
83
+ *
84
+ * The result of this function is used to determine the response status and message, and is
85
+ * always sent to the client. You could include logic to check for production vs development
86
+ * environments here, and this would also be a good place to include error tracking
87
+ * like Sentry or Rollbar.
88
+ *
89
+ * @param arg - The error and the request
90
+ * @returns A KaitoError or an object with a status and message
91
+ */
92
+ onError: (arg: {
69
93
  error: Error;
70
94
  req: KaitoRequest;
71
- }): Promise<KaitoError | {
95
+ }) => Promise<KaitoError | {
72
96
  status: number;
73
97
  message: string;
74
98
  }>;
99
+ /**
100
+ * A function that is called before every request. Most useful for bailing out early in the case of an OPTIONS request.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * before: async req => {
105
+ * if (req.method === 'OPTIONS') {
106
+ * return new Response(null, {status: 204});
107
+ * }
108
+ * }
109
+ * ```
110
+ */
111
+ before?: Before;
112
+ /**
113
+ * Transforms the response before it is sent to the client. Very useful for settings headers like CORS.
114
+ *
115
+ * You can also return a new response in this function, or just mutate the current one.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * transform: async (req, res) => {
120
+ * res.headers.set('Access-Control-Allow-Origin', 'http://localhost:3000');
121
+ * res.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
122
+ * res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
123
+ * res.headers.set('Access-Control-Max-Age', '86400');
124
+ * res.headers.set('Access-Control-Allow-Credentials', 'true');
125
+ * }
126
+ * ```
127
+ */
128
+ transform?: (req: Request, res: Response) => Promise<Response | void | undefined>;
129
+ /**
130
+ * Controls whether the server includes a header to indicate non-JSON responses.
131
+ *
132
+ * When a route handler returns a `Response` object, this setting determines if
133
+ * the server adds a header indicating the response should not be parsed as JSON.
134
+ *
135
+ * The `@kaito-http/client` package checks for this header's presence:
136
+ * - If present: Returns the raw Response object
137
+ * - If absent: Attempts to parse the response as JSON
138
+ *
139
+ * You might want to disable this feature when:
140
+ * 1. Using a custom client that doesn't recognize this header
141
+ * 2. Security requirements prevent exposing framework details in headers
142
+ *
143
+ * @default true
144
+ */
145
+ enableClientResponseHints: boolean;
75
146
  };
76
- declare function createKaitoHandler<Context, BeforeAfterContext = null>(config: ServerConfig<Context, BeforeAfterContext>): (request: Request) => Promise<Response>;
147
+ declare function createKaitoHandler<Context>(userConfig: MakeOptional<ServerConfig<Context>, 'enableClientResponseHints'>): (request: Request) => Promise<Response>;
77
148
 
78
149
  type PrefixRoutesPathInner<R extends AnyRoute, Prefix extends `/${string}`> = R extends Route<infer ContextFrom, infer ContextTo, infer Result, infer Path, infer Method, infer Query, infer BodyOutput> ? Route<ContextFrom, ContextTo, Result, `${Prefix}${Path}`, Method, Query, BodyOutput> : never;
79
150
  type PrefixRoutesPath<Prefix extends `/${string}`, R extends AnyRoute> = R extends R ? PrefixRoutesPathInner<R, Prefix> : never;
@@ -88,17 +159,17 @@ declare class Router<ContextFrom, ContextTo, R extends AnyRoute> {
88
159
  private static parseQuery;
89
160
  constructor(options: RouterState<R, ContextFrom, ContextTo>);
90
161
  get routes(): Set<R>;
91
- add: <Result, Path extends string, Method extends KaitoMethod, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(method: Method, path: Path, route: (Method extends "GET" ? Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "body" | "path" | "method" | "through"> : Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "path" | "method" | "through">) | Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>["run"]) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, Method, Query, Body>>;
162
+ add: <Result, Path extends string, Method extends KaitoMethod, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(method: Method, path: Path, route: (Method extends "GET" ? Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "body" | "path" | "method" | "through"> : Omit<Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>, "path" | "method" | "through">) | Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>["run"]) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, Method, Query, Body>>;
92
163
  readonly merge: <PathPrefix extends `/${string}`, OtherRoutes extends AnyRoute>(pathPrefix: PathPrefix, other: Router<ContextFrom, unknown, OtherRoutes>) => Router<ContextFrom, ContextTo, Extract<R | PrefixRoutesPath<PathPrefix, OtherRoutes>, AnyRoute>>;
93
- freeze: (server: Omit<ServerConfig<ContextFrom, any>, "router">) => (req: Request) => Promise<Response>;
164
+ freeze: (server: Omit<ServerConfig<ContextFrom>, "router">) => (req: Request) => Promise<Response>;
94
165
  private readonly method;
95
- get: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "GET", Query, Body>, "body" | "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "GET", Query, Body>>;
96
- post: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "POST", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "POST", Query, Body>>;
97
- put: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PUT", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "PUT", Query, Body>>;
98
- patch: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PATCH", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "PATCH", Query, Body>>;
99
- delete: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "DELETE", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "DELETE", Query, Body>>;
100
- head: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "HEAD", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "HEAD", Query, Body>>;
101
- options: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "OPTIONS", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result extends Response ? unknown : Result, Path, "OPTIONS", Query, Body>>;
166
+ get: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "GET", Query, Body>, "body" | "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "GET", Query, Body>>;
167
+ post: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "POST", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "POST", Query, Body>>;
168
+ put: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PUT", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "PUT", Query, Body>>;
169
+ patch: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "PATCH", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "PATCH", Query, Body>>;
170
+ delete: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "DELETE", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "DELETE", Query, Body>>;
171
+ head: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "HEAD", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "HEAD", Query, Body>>;
172
+ options: <Result, Path extends string, Query extends AnyQueryDefinition = {}, Body extends Parsable = never>(path: Path, route: ((arg: RouteArgument<Path, ContextTo, { [Key in keyof Query]: InferParsable<Query[Key]>["output"]; }, InferParsable<Body>["output"]>) => Promise<Result>) | Omit<Route<ContextFrom, ContextTo, Result, Path, "OPTIONS", Query, Body>, "path" | "method" | "through">) => Router<ContextFrom, ContextTo, R | Route<ContextFrom, ContextTo, Result, Path, "OPTIONS", Query, Body>>;
102
173
  through: <NextContext>(transform: (context: ContextTo) => Promise<NextContext>) => Router<ContextFrom, NextContext, R>;
103
174
  }
104
175
 
@@ -114,6 +185,7 @@ type SuccessfulAPIResponse<T> = {
114
185
  };
115
186
  type APIResponse<T> = ErroredAPIResponse | SuccessfulAPIResponse<T>;
116
187
  type AnyResponse = APIResponse<unknown>;
188
+ type MakeOptional<T, K extends keyof T> = T extends T ? Omit<T, K> & Partial<Pick<T, K>> : never;
117
189
  type ExtractRouteParams<T extends string> = string extends T ? Record<string, string> : T extends `${string}:${infer Param}/${infer Rest}` ? {
118
190
  [k in Param | keyof ExtractRouteParams<Rest>]: string;
119
191
  } : T extends `${string}:${infer Param}` ? {
@@ -172,4 +244,4 @@ type Route<ContextFrom, ContextTo, Result, Path extends string, Method extends K
172
244
  };
173
245
  type AnyRoute<ContextFrom = any, ContextTo = any> = Route<ContextFrom, ContextTo, any, any, any, AnyQueryDefinition, any>;
174
246
 
175
- export { type APIResponse, type After, type AnyQueryDefinition, type AnyResponse, type AnyRoute, type Before, type ErroredAPIResponse, type ExtractRouteParams, type GetContext, type InferParsable, type InferRoutes, KaitoError, type KaitoMethod, KaitoRequest, type Parsable, type Route, type RouteArgument, Router, type RouterState, type ServerConfig, type ServerConfigWithBefore, type SuccessfulAPIResponse, type Through, WrappedError, createKaitoHandler, createUtilities, parsable };
247
+ export { type APIResponse, type AnyQueryDefinition, type AnyResponse, type AnyRoute, type Before, type ErroredAPIResponse, type ExtractRouteParams, type GetContext, type InferParsable, type InferRoutes, KaitoError, type KaitoMethod, KaitoRequest, type MakeOptional, type Parsable, type Route, type RouteArgument, Router, type RouterState, type ServerConfig, type SuccessfulAPIResponse, type Through, WrappedError, createKaitoHandler, createUtilities, parsable };
package/dist/index.js CHANGED
@@ -74,6 +74,9 @@ var KaitoResponse = class {
74
74
  return this._headers;
75
75
  }
76
76
  status(status) {
77
+ if (status === void 0) {
78
+ return this._status;
79
+ }
77
80
  this._status = status;
78
81
  return this;
79
82
  }
@@ -200,10 +203,10 @@ var Router = class _Router {
200
203
  const request = new KaitoRequest(url, req);
201
204
  const response = new KaitoResponse();
202
205
  try {
203
- const rootCtx = await server.getContext(request, response);
204
- const ctx = await route.through(rootCtx);
205
206
  const body = route.body ? await route.body.parse(await req.json()) : void 0;
206
207
  const query = _Router.parseQuery(route.query, url);
208
+ const rootCtx = await server.getContext(request, response);
209
+ const ctx = await route.through(rootCtx);
207
210
  const result = await route.run({
208
211
  ctx,
209
212
  body,
@@ -211,6 +214,9 @@ var Router = class _Router {
211
214
  params
212
215
  });
213
216
  if (result instanceof Response) {
217
+ if (server.enableClientResponseHints) {
218
+ result.headers.set("x-kaito-is-response", "1");
219
+ }
214
220
  return result;
215
221
  }
216
222
  return response.toResponse({
@@ -256,20 +262,21 @@ var Router = class _Router {
256
262
  };
257
263
 
258
264
  // src/server.ts
259
- function createKaitoHandler(config) {
260
- const match = config.router.freeze(config);
265
+ function createKaitoHandler(userConfig) {
266
+ const config = {
267
+ enableClientResponseHints: true,
268
+ ...userConfig
269
+ };
270
+ const handle = config.router.freeze(config);
261
271
  return async (request) => {
262
- let before = void 0;
263
272
  if (config.before) {
264
273
  const result = await config.before(request);
265
- if (result instanceof Response) {
266
- return result;
267
- }
268
- before = result;
274
+ if (result instanceof Response) return result;
269
275
  }
270
- const response = await match(request);
271
- if ("after" in config && config.after) {
272
- await config.after(before, response);
276
+ const response = await handle(request);
277
+ if (config.transform) {
278
+ const result = await config.transform(request, response);
279
+ if (result instanceof Response) return result;
273
280
  }
274
281
  return response;
275
282
  };
@@ -0,0 +1,110 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/stream/stream.ts
21
+ var stream_exports = {};
22
+ __export(stream_exports, {
23
+ KaitoSSEResponse: () => KaitoSSEResponse,
24
+ KaitoStreamResponse: () => KaitoStreamResponse,
25
+ SSEController: () => SSEController,
26
+ sse: () => sse,
27
+ sseEventToString: () => sseEventToString,
28
+ stream: () => stream
29
+ });
30
+ module.exports = __toCommonJS(stream_exports);
31
+ var KaitoStreamResponse = class extends Response {
32
+ constructor(body) {
33
+ super(body, {
34
+ headers: {
35
+ "Content-Type": "text/event-stream",
36
+ "Cache-Control": "no-cache",
37
+ Connection: "keep-alive"
38
+ }
39
+ });
40
+ }
41
+ async *[Symbol.asyncIterator]() {
42
+ for await (const chunk of this.body) {
43
+ yield chunk;
44
+ }
45
+ }
46
+ };
47
+ var KaitoSSEResponse = class extends KaitoStreamResponse {
48
+ };
49
+ function stream(body) {
50
+ return new KaitoStreamResponse(new ReadableStream(body));
51
+ }
52
+ function sseEventToString(event) {
53
+ let result = "";
54
+ if (event.event) {
55
+ result += `event:${event.event}
56
+ `;
57
+ }
58
+ if (event.id) {
59
+ result += `id:${event.id}
60
+ `;
61
+ }
62
+ if (event.retry) {
63
+ result += `retry:${event.retry}
64
+ `;
65
+ }
66
+ if (event.data) {
67
+ result += `data:${event.data}`;
68
+ }
69
+ return result;
70
+ }
71
+ var SSEController = class {
72
+ controller;
73
+ constructor(controller) {
74
+ this.controller = controller;
75
+ }
76
+ enqueue(event) {
77
+ this.controller.enqueue(sseEventToString(event) + "\n\n");
78
+ }
79
+ close() {
80
+ this.controller.close();
81
+ }
82
+ };
83
+ function sse(source) {
84
+ const start = source.start;
85
+ const pull = source.pull;
86
+ const cancel = source.cancel;
87
+ const readable = new ReadableStream({
88
+ ...cancel ? { cancel } : {},
89
+ ...start ? {
90
+ start: async (controller) => {
91
+ await start(new SSEController(controller));
92
+ }
93
+ } : {},
94
+ ...pull ? {
95
+ pull: async (controller) => {
96
+ await pull(new SSEController(controller));
97
+ }
98
+ } : {}
99
+ });
100
+ return new KaitoSSEResponse(readable);
101
+ }
102
+ // Annotate the CommonJS export names for ESM import in node:
103
+ 0 && (module.exports = {
104
+ KaitoSSEResponse,
105
+ KaitoStreamResponse,
106
+ SSEController,
107
+ sse,
108
+ sseEventToString,
109
+ stream
110
+ });
@@ -0,0 +1,35 @@
1
+ declare class KaitoStreamResponse<T> extends Response {
2
+ constructor(body: ReadableStream<T>);
3
+ [Symbol.asyncIterator](): AsyncGenerator<Uint8Array<ArrayBufferLike>, void, unknown>;
4
+ }
5
+ declare class KaitoSSEResponse extends KaitoStreamResponse<string> {
6
+ }
7
+ declare function stream<T = string>(body: UnderlyingDefaultSource<T>): KaitoStreamResponse<T>;
8
+ type SSEEvent = ({
9
+ data: string;
10
+ event: string;
11
+ } | {
12
+ data: string;
13
+ event?: string | undefined;
14
+ } | {
15
+ data?: string | undefined;
16
+ event: string;
17
+ }) & {
18
+ retry?: number;
19
+ id?: string;
20
+ };
21
+ declare function sseEventToString(event: SSEEvent): string;
22
+ declare class SSEController {
23
+ private readonly controller;
24
+ constructor(controller: ReadableStreamDefaultController<string>);
25
+ enqueue(event: SSEEvent): void;
26
+ close(): void;
27
+ }
28
+ interface SSESource {
29
+ cancel?: UnderlyingSourceCancelCallback;
30
+ start?(controller: SSEController): Promise<void>;
31
+ pull?(controller: SSEController): Promise<void>;
32
+ }
33
+ declare function sse(source: SSESource): KaitoSSEResponse;
34
+
35
+ export { KaitoSSEResponse, KaitoStreamResponse, SSEController, type SSEEvent, type SSESource, sse, sseEventToString, stream };