@kaito-http/core 4.0.0-beta.6 → 4.0.0-beta.8

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.
@@ -28,13 +28,20 @@ function experimental_createOriginMatcher(origins) {
28
28
  if (origins.length === 0) {
29
29
  return () => false;
30
30
  }
31
+ const escapedCharsRegex = /[.+?^${}()|[\]\\]/g;
31
32
  const source = origins.map((origin) => {
32
- if (origin.startsWith("*.")) {
33
- const escapedDomain = origin.slice(2).replace(/[.+?^${}()|[\]\\]/g, "\\$&");
34
- return `^(?:https?://)[^.]+\\.${escapedDomain}$`;
33
+ if (origin.includes("://*.")) {
34
+ const parts = origin.split("://");
35
+ if (parts.length !== 2) {
36
+ throw new Error(`Invalid origin pattern: ${origin}. Must include protocol (e.g., https://*.example.com)`);
37
+ }
38
+ const [protocol, rest] = parts;
39
+ const domain = rest.slice(2).replace(escapedCharsRegex, "\\$&");
40
+ const pattern = `^${protocol.replace(escapedCharsRegex, "\\$&")}:\\/\\/[^.]+\\.${domain}$`;
41
+ return pattern;
35
42
  } else {
36
- const escapedOrigin = origin.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
37
- return `^${escapedOrigin}$`;
43
+ const pattern = `^${origin.replace(escapedCharsRegex, "\\$&")}$`;
44
+ return pattern;
38
45
  }
39
46
  }).join("|");
40
47
  const regex = new RegExp(source);
@@ -46,7 +53,7 @@ function experimental_createCORSTransform(origins) {
46
53
  const origin = request.headers.get("Origin");
47
54
  if (origin && matcher(origin)) {
48
55
  response.headers.set("Access-Control-Allow-Origin", origin);
49
- response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
56
+ response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
50
57
  response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
51
58
  response.headers.set("Access-Control-Max-Age", "86400");
52
59
  response.headers.set("Access-Control-Allow-Credentials", "true");
@@ -1,31 +1,56 @@
1
1
  /**
2
2
  * Creates a function that matches origins against a predefined set of patterns, supporting wildcards.
3
- * The matcher handles both exact matches and wildcard subdomain patterns (e.g., '*.example.com').
3
+ * The matcher handles both exact matches and wildcard subdomain patterns.
4
4
  *
5
5
  * **⚠️ This API is experimental and may change or even be removed in the future. ⚠️**
6
6
  *
7
7
  * @param origins Array of origin patterns to match against.
8
- * Patterns can be exact origins (e.g., 'https://example.com') or wildcard patterns (e.g., '*.example.com') that match subdomains.
8
+ * Each pattern MUST include the protocol (http:// or https://).
9
+ * Two types of patterns are supported:
10
+ * 1. Exact matches (e.g., 'https://example.com') - matches only the exact domain with exact protocol
11
+ * 2. Wildcard subdomain patterns (e.g., 'https://*.example.com') - matches ONLY subdomains with exact protocol
12
+ *
13
+ * Important matching rules:
14
+ * - Protocol is always matched exactly - if you need both HTTP and HTTPS, include both patterns
15
+ * - Wildcard patterns (e.g., 'https://*.example.com') will ONLY match subdomains, NOT the root domain
16
+ * - To match both subdomains AND the root domain, include both patterns:
17
+ * ['https://*.example.com', 'https://example.com']
18
+ *
9
19
  * @returns A function that tests if an origin matches any of the patterns
10
20
  *
11
21
  * @example
12
22
  * ```typescript
13
23
  * const allowedOrigins = [
14
- * 'https://example.com',
15
- * '*.trusted-domain.com' // Won't match https://evil-domain.com, only subdomains
24
+ * // Exact matches - protocol required
25
+ * 'https://example.com', // matches only https://example.com
26
+ * 'http://example.com', // matches only http://example.com
27
+ *
28
+ * // Wildcard subdomain matches - protocol required
29
+ * 'https://*.example.com', // matches https://app.example.com, https://api.example.com
30
+ * // does NOT match https://example.com
31
+ *
32
+ * // To match both HTTP and HTTPS, include both
33
+ * 'https://*.staging.com', // matches https://app.staging.com
34
+ * 'http://*.staging.com', // matches http://app.staging.com
35
+ *
36
+ * // To match both subdomains and root domain, include both
37
+ * 'https://*.production.com', // matches https://app.production.com
38
+ * 'https://production.com', // matches https://production.com
16
39
  * ];
17
40
  *
18
41
  * const matcher = createOriginMatcher(allowedOrigins);
19
42
  *
20
- * // Exact match
21
- * console.log(matcher('https://example.com')); // true
22
- * console.log(matcher('http://example.com')); // false
43
+ * // Exact matches
44
+ * matcher('https://example.com'); // true
45
+ * matcher('http://example.com'); // true
23
46
  *
24
- * // Wildcard subdomain matches
25
- * console.log(matcher('https://app.trusted-domain.com')); // true
26
- * console.log(matcher('https://staging.trusted-domain.com')); // true
27
- * console.log(matcher('https://trusted-domain.com')); // false, because it's not a subdomain
28
- * console.log(matcher('https://evil-domain.com')); // false
47
+ * // Subdomain matches (protocol specific)
48
+ * matcher('https://app.example.com'); // true
49
+ * matcher('http://app.example.com'); // false - wrong protocol
50
+ *
51
+ * // Root domain with wildcard pattern
52
+ * matcher('https://example.com'); // false - wildcards don't match root
53
+ * matcher('https://production.com'); // true - matched by exact pattern
29
54
  * ```
30
55
  */
31
56
  declare function experimental_createOriginMatcher(origins: string[]): (origin: string) => boolean;
@@ -38,15 +63,31 @@ declare function experimental_createOriginMatcher(origins: string[]): (origin: s
38
63
  * @returns A function that will mutate the Response object by applying the CORS headers
39
64
  * @example
40
65
  * ```ts
41
- * const cors = createCORSHandler({
42
- * origins: ['https://example.com', "*.allows-subdomains.com", "http://localhost:3000"],
43
- * });
66
+ * const cors = experimental_createCORSTransform([
67
+ * // Exact matches
68
+ * 'https://example.com',
69
+ * 'http://localhost:3000',
70
+ *
71
+ * // Wildcard subdomain matches
72
+ * 'https://*.myapp.com', // matches https://dashboard.myapp.com
73
+ * 'http://*.myapp.com', // matches http://dashboard.myapp.com
74
+ *
75
+ * // Match both subdomain and root domain
76
+ * 'https://*.staging.com', // matches https://app.staging.com
77
+ * 'https://staging.com' // matches https://staging.com
78
+ * ]);
44
79
  *
45
- * const handler = createKaitoHandler({
46
- * // ...
47
- * transform: async (request, response) => {
48
- * cors(request, response);
49
- * }
80
+ * const router = create({
81
+ * before: async req => {
82
+ * if (req.method === 'OPTIONS') {
83
+ * // Return early to skip the router. This response still gets passed to `.transform()`
84
+ * // So our CORS headers will still be applied
85
+ * return new Response(null, {status: 204});
86
+ * }
87
+ * },
88
+ * transform: async (request, response) => {
89
+ * cors(request, response);
90
+ * }
50
91
  * });
51
92
  * ```
52
93
  */
@@ -1,31 +1,56 @@
1
1
  /**
2
2
  * Creates a function that matches origins against a predefined set of patterns, supporting wildcards.
3
- * The matcher handles both exact matches and wildcard subdomain patterns (e.g., '*.example.com').
3
+ * The matcher handles both exact matches and wildcard subdomain patterns.
4
4
  *
5
5
  * **⚠️ This API is experimental and may change or even be removed in the future. ⚠️**
6
6
  *
7
7
  * @param origins Array of origin patterns to match against.
8
- * Patterns can be exact origins (e.g., 'https://example.com') or wildcard patterns (e.g., '*.example.com') that match subdomains.
8
+ * Each pattern MUST include the protocol (http:// or https://).
9
+ * Two types of patterns are supported:
10
+ * 1. Exact matches (e.g., 'https://example.com') - matches only the exact domain with exact protocol
11
+ * 2. Wildcard subdomain patterns (e.g., 'https://*.example.com') - matches ONLY subdomains with exact protocol
12
+ *
13
+ * Important matching rules:
14
+ * - Protocol is always matched exactly - if you need both HTTP and HTTPS, include both patterns
15
+ * - Wildcard patterns (e.g., 'https://*.example.com') will ONLY match subdomains, NOT the root domain
16
+ * - To match both subdomains AND the root domain, include both patterns:
17
+ * ['https://*.example.com', 'https://example.com']
18
+ *
9
19
  * @returns A function that tests if an origin matches any of the patterns
10
20
  *
11
21
  * @example
12
22
  * ```typescript
13
23
  * const allowedOrigins = [
14
- * 'https://example.com',
15
- * '*.trusted-domain.com' // Won't match https://evil-domain.com, only subdomains
24
+ * // Exact matches - protocol required
25
+ * 'https://example.com', // matches only https://example.com
26
+ * 'http://example.com', // matches only http://example.com
27
+ *
28
+ * // Wildcard subdomain matches - protocol required
29
+ * 'https://*.example.com', // matches https://app.example.com, https://api.example.com
30
+ * // does NOT match https://example.com
31
+ *
32
+ * // To match both HTTP and HTTPS, include both
33
+ * 'https://*.staging.com', // matches https://app.staging.com
34
+ * 'http://*.staging.com', // matches http://app.staging.com
35
+ *
36
+ * // To match both subdomains and root domain, include both
37
+ * 'https://*.production.com', // matches https://app.production.com
38
+ * 'https://production.com', // matches https://production.com
16
39
  * ];
17
40
  *
18
41
  * const matcher = createOriginMatcher(allowedOrigins);
19
42
  *
20
- * // Exact match
21
- * console.log(matcher('https://example.com')); // true
22
- * console.log(matcher('http://example.com')); // false
43
+ * // Exact matches
44
+ * matcher('https://example.com'); // true
45
+ * matcher('http://example.com'); // true
23
46
  *
24
- * // Wildcard subdomain matches
25
- * console.log(matcher('https://app.trusted-domain.com')); // true
26
- * console.log(matcher('https://staging.trusted-domain.com')); // true
27
- * console.log(matcher('https://trusted-domain.com')); // false, because it's not a subdomain
28
- * console.log(matcher('https://evil-domain.com')); // false
47
+ * // Subdomain matches (protocol specific)
48
+ * matcher('https://app.example.com'); // true
49
+ * matcher('http://app.example.com'); // false - wrong protocol
50
+ *
51
+ * // Root domain with wildcard pattern
52
+ * matcher('https://example.com'); // false - wildcards don't match root
53
+ * matcher('https://production.com'); // true - matched by exact pattern
29
54
  * ```
30
55
  */
31
56
  declare function experimental_createOriginMatcher(origins: string[]): (origin: string) => boolean;
@@ -38,15 +63,31 @@ declare function experimental_createOriginMatcher(origins: string[]): (origin: s
38
63
  * @returns A function that will mutate the Response object by applying the CORS headers
39
64
  * @example
40
65
  * ```ts
41
- * const cors = createCORSHandler({
42
- * origins: ['https://example.com', "*.allows-subdomains.com", "http://localhost:3000"],
43
- * });
66
+ * const cors = experimental_createCORSTransform([
67
+ * // Exact matches
68
+ * 'https://example.com',
69
+ * 'http://localhost:3000',
70
+ *
71
+ * // Wildcard subdomain matches
72
+ * 'https://*.myapp.com', // matches https://dashboard.myapp.com
73
+ * 'http://*.myapp.com', // matches http://dashboard.myapp.com
74
+ *
75
+ * // Match both subdomain and root domain
76
+ * 'https://*.staging.com', // matches https://app.staging.com
77
+ * 'https://staging.com' // matches https://staging.com
78
+ * ]);
44
79
  *
45
- * const handler = createKaitoHandler({
46
- * // ...
47
- * transform: async (request, response) => {
48
- * cors(request, response);
49
- * }
80
+ * const router = create({
81
+ * before: async req => {
82
+ * if (req.method === 'OPTIONS') {
83
+ * // Return early to skip the router. This response still gets passed to `.transform()`
84
+ * // So our CORS headers will still be applied
85
+ * return new Response(null, {status: 204});
86
+ * }
87
+ * },
88
+ * transform: async (request, response) => {
89
+ * cors(request, response);
90
+ * }
50
91
  * });
51
92
  * ```
52
93
  */
package/dist/cors/cors.js CHANGED
@@ -3,13 +3,20 @@ function experimental_createOriginMatcher(origins) {
3
3
  if (origins.length === 0) {
4
4
  return () => false;
5
5
  }
6
+ const escapedCharsRegex = /[.+?^${}()|[\]\\]/g;
6
7
  const source = origins.map((origin) => {
7
- if (origin.startsWith("*.")) {
8
- const escapedDomain = origin.slice(2).replace(/[.+?^${}()|[\]\\]/g, "\\$&");
9
- return `^(?:https?://)[^.]+\\.${escapedDomain}$`;
8
+ if (origin.includes("://*.")) {
9
+ const parts = origin.split("://");
10
+ if (parts.length !== 2) {
11
+ throw new Error(`Invalid origin pattern: ${origin}. Must include protocol (e.g., https://*.example.com)`);
12
+ }
13
+ const [protocol, rest] = parts;
14
+ const domain = rest.slice(2).replace(escapedCharsRegex, "\\$&");
15
+ const pattern = `^${protocol.replace(escapedCharsRegex, "\\$&")}:\\/\\/[^.]+\\.${domain}$`;
16
+ return pattern;
10
17
  } else {
11
- const escapedOrigin = origin.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
12
- return `^${escapedOrigin}$`;
18
+ const pattern = `^${origin.replace(escapedCharsRegex, "\\$&")}$`;
19
+ return pattern;
13
20
  }
14
21
  }).join("|");
15
22
  const regex = new RegExp(source);
@@ -21,7 +28,7 @@ function experimental_createCORSTransform(origins) {
21
28
  const origin = request.headers.get("Origin");
22
29
  if (origin && matcher(origin)) {
23
30
  response.headers.set("Access-Control-Allow-Origin", origin);
24
- response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
31
+ response.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS");
25
32
  response.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
26
33
  response.headers.set("Access-Control-Max-Age", "86400");
27
34
  response.headers.set("Access-Control-Allow-Credentials", "true");
package/dist/index.cjs CHANGED
@@ -178,7 +178,10 @@ var Router = class _Router {
178
178
  merge = (pathPrefix, other) => {
179
179
  const newRoutes = [...other.state.routes].map((route) => ({
180
180
  ...route,
181
- path: `${pathPrefix}${route.path}`
181
+ // handle pathPrefix = / & route.path = / case causing //
182
+ // we intentionally are replacing on the joining path and not the pathPrefix, in case of
183
+ // /named -> merged to -> / causing /named/ not /named
184
+ path: `${pathPrefix}${route.path === "/" ? "" : route.path}`
182
185
  }));
183
186
  return new _Router({
184
187
  ...this.state,
@@ -366,7 +369,7 @@ var Router = class _Router {
366
369
  const item = {
367
370
  description: route.openapi?.description ?? "Successful response",
368
371
  responses: {
369
- default: {
372
+ 200: {
370
373
  description: route.openapi?.description ?? "Successful response",
371
374
  content
372
375
  }
package/dist/index.d.cts CHANGED
@@ -148,7 +148,7 @@ type Route<ContextTo, Result, Path extends string, AdditionalParams extends Reco
148
148
  };
149
149
  type AnyRoute = Route<any, any, any, any, any, any, any>;
150
150
 
151
- type PrefixRoutesPathInner<R extends AnyRoute, Prefix extends `/${string}`> = R extends Route<infer ContextTo, infer Result, infer Path, infer AdditionalParams, infer Method, infer Query, infer BodyOutput> ? Route<ContextTo, Result, `${Prefix}${Path}`, AdditionalParams, Method, Query, BodyOutput> : never;
151
+ type PrefixRoutesPathInner<R extends AnyRoute, Prefix extends `/${string}`> = R extends Route<infer ContextTo, infer Result, infer Path, infer AdditionalParams, infer Method, infer Query, infer BodyOutput> ? Route<ContextTo, Result, `${Prefix}${Path extends '/' ? '' : Path}`, AdditionalParams, Method, Query, BodyOutput> : never;
152
152
  type PrefixRoutesPath<Prefix extends `/${string}`, R extends AnyRoute> = R extends R ? PrefixRoutesPathInner<R, Prefix> : never;
153
153
  type RouterState<ContextFrom, ContextTo, RequiredParams extends Record<string, unknown>, Routes extends AnyRoute> = {
154
154
  routes: Set<Routes>;
package/dist/index.d.ts CHANGED
@@ -148,7 +148,7 @@ type Route<ContextTo, Result, Path extends string, AdditionalParams extends Reco
148
148
  };
149
149
  type AnyRoute = Route<any, any, any, any, any, any, any>;
150
150
 
151
- type PrefixRoutesPathInner<R extends AnyRoute, Prefix extends `/${string}`> = R extends Route<infer ContextTo, infer Result, infer Path, infer AdditionalParams, infer Method, infer Query, infer BodyOutput> ? Route<ContextTo, Result, `${Prefix}${Path}`, AdditionalParams, Method, Query, BodyOutput> : never;
151
+ type PrefixRoutesPathInner<R extends AnyRoute, Prefix extends `/${string}`> = R extends Route<infer ContextTo, infer Result, infer Path, infer AdditionalParams, infer Method, infer Query, infer BodyOutput> ? Route<ContextTo, Result, `${Prefix}${Path extends '/' ? '' : Path}`, AdditionalParams, Method, Query, BodyOutput> : never;
152
152
  type PrefixRoutesPath<Prefix extends `/${string}`, R extends AnyRoute> = R extends R ? PrefixRoutesPathInner<R, Prefix> : never;
153
153
  type RouterState<ContextFrom, ContextTo, RequiredParams extends Record<string, unknown>, Routes extends AnyRoute> = {
154
154
  routes: Set<Routes>;
package/dist/index.js CHANGED
@@ -148,7 +148,10 @@ var Router = class _Router {
148
148
  merge = (pathPrefix, other) => {
149
149
  const newRoutes = [...other.state.routes].map((route) => ({
150
150
  ...route,
151
- path: `${pathPrefix}${route.path}`
151
+ // handle pathPrefix = / & route.path = / case causing //
152
+ // we intentionally are replacing on the joining path and not the pathPrefix, in case of
153
+ // /named -> merged to -> / causing /named/ not /named
154
+ path: `${pathPrefix}${route.path === "/" ? "" : route.path}`
152
155
  }));
153
156
  return new _Router({
154
157
  ...this.state,
@@ -336,7 +339,7 @@ var Router = class _Router {
336
339
  const item = {
337
340
  description: route.openapi?.description ?? "Successful response",
338
341
  responses: {
339
- default: {
342
+ 200: {
340
343
  description: route.openapi?.description ?? "Successful response",
341
344
  content
342
345
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaito-http/core",
3
- "version": "4.0.0-beta.6",
3
+ "version": "4.0.0-beta.8",
4
4
  "author": "Alistair Smith <hi@alistair.sh>",
5
5
  "repository": "https://github.com/kaito-http/kaito",
6
6
  "devDependencies": {