@draftlab/auth 0.14.0 → 0.15.1

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.
Files changed (70) hide show
  1. package/dist/adapters/node.d.mts +0 -1
  2. package/dist/client.d.mts +293 -287
  3. package/dist/client.mjs +1 -0
  4. package/dist/core.d.mts +23 -24
  5. package/dist/core.mjs +6 -6
  6. package/dist/error.d.mts +53 -53
  7. package/dist/keys.d.mts +0 -1
  8. package/dist/mutex.d.mts +14 -14
  9. package/dist/provider/apple.d.mts +34 -35
  10. package/dist/provider/code.d.mts +75 -85
  11. package/dist/provider/code.mjs +83 -0
  12. package/dist/provider/discord.d.mts +49 -50
  13. package/dist/provider/facebook.d.mts +49 -50
  14. package/dist/provider/github.d.mts +50 -51
  15. package/dist/provider/gitlab.d.mts +34 -35
  16. package/dist/provider/google.d.mts +49 -50
  17. package/dist/provider/linkedin.d.mts +47 -48
  18. package/dist/provider/magiclink.d.mts +28 -38
  19. package/dist/provider/magiclink.mjs +57 -0
  20. package/dist/provider/microsoft.d.mts +67 -68
  21. package/dist/provider/oauth2.d.mts +75 -76
  22. package/dist/provider/oauth2.mjs +57 -0
  23. package/dist/provider/passkey.d.mts +20 -21
  24. package/dist/provider/password.d.mts +174 -202
  25. package/dist/provider/provider.d.mts +107 -109
  26. package/dist/provider/reddit.d.mts +33 -34
  27. package/dist/provider/slack.d.mts +34 -35
  28. package/dist/provider/spotify.d.mts +34 -35
  29. package/dist/provider/totp.d.mts +43 -44
  30. package/dist/provider/twitch.d.mts +33 -34
  31. package/dist/provider/vercel.d.mts +65 -66
  32. package/dist/revocation.d.mts +29 -30
  33. package/dist/router/context.d.mts +21 -0
  34. package/dist/router/context.mjs +193 -0
  35. package/dist/router/cookies.d.mts +8 -0
  36. package/dist/router/cookies.mjs +13 -0
  37. package/dist/router/index.d.mts +21 -0
  38. package/dist/router/index.mjs +107 -0
  39. package/dist/router/matcher.d.mts +15 -0
  40. package/dist/router/matcher.mjs +76 -0
  41. package/dist/router/middleware/cors.d.mts +15 -0
  42. package/dist/router/middleware/cors.mjs +114 -0
  43. package/dist/router/safe-request.d.mts +52 -0
  44. package/dist/router/safe-request.mjs +160 -0
  45. package/dist/router/types.d.mts +67 -0
  46. package/dist/router/types.mjs +1 -0
  47. package/dist/router/variables.d.mts +12 -0
  48. package/dist/router/variables.mjs +20 -0
  49. package/dist/storage/memory.d.mts +11 -12
  50. package/dist/storage/storage.d.mts +110 -110
  51. package/dist/storage/turso.d.mts +0 -1
  52. package/dist/storage/unstorage.d.mts +0 -1
  53. package/dist/subject.d.mts +0 -1
  54. package/dist/themes/theme.d.mts +101 -101
  55. package/dist/toolkit/client.d.mts +56 -57
  56. package/dist/toolkit/providers/facebook.d.mts +0 -1
  57. package/dist/toolkit/providers/github.d.mts +0 -1
  58. package/dist/toolkit/providers/google.d.mts +0 -1
  59. package/dist/toolkit/storage.d.mts +8 -8
  60. package/dist/ui/base.d.mts +0 -1
  61. package/dist/ui/code.d.mts +5 -6
  62. package/dist/ui/form.d.mts +6 -7
  63. package/dist/ui/icon.d.mts +0 -1
  64. package/dist/ui/magiclink.d.mts +5 -6
  65. package/dist/ui/passkey.d.mts +0 -1
  66. package/dist/ui/password.d.mts +2 -3
  67. package/dist/ui/select.d.mts +0 -1
  68. package/dist/ui/totp.d.mts +0 -1
  69. package/dist/util.d.mts +1 -2
  70. package/package.json +6 -7
@@ -2,50 +2,49 @@ import { Provider } from "./provider.mjs";
2
2
  import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
3
3
 
4
4
  //#region src/provider/twitch.d.ts
5
-
6
5
  /**
7
6
  * Configuration options for Twitch OAuth 2.0 provider.
8
7
  * Extends the base OAuth 2.0 configuration with Twitch-specific documentation.
9
8
  */
10
9
  interface TwitchConfig extends Oauth2WrappedConfig {
11
10
  /**
12
- * Twitch application client ID.
13
- * Get this from your Twitch Console at https://dev.twitch.tv/console
14
- *
15
- * @example
16
- * ```ts
17
- * {
18
- * clientID: "abcdef123456"
19
- * }
20
- * ```
21
- */
11
+ * Twitch application client ID.
12
+ * Get this from your Twitch Console at https://dev.twitch.tv/console
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * {
17
+ * clientID: "abcdef123456"
18
+ * }
19
+ * ```
20
+ */
22
21
  readonly clientID: string;
23
22
  /**
24
- * Twitch application client secret.
25
- * Keep this secure and never expose it to client-side code.
26
- *
27
- * @example
28
- * ```ts
29
- * {
30
- * clientSecret: process.env.TWITCH_CLIENT_SECRET
31
- * }
32
- * ```
33
- */
23
+ * Twitch application client secret.
24
+ * Keep this secure and never expose it to client-side code.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * {
29
+ * clientSecret: process.env.TWITCH_CLIENT_SECRET
30
+ * }
31
+ * ```
32
+ */
34
33
  readonly clientSecret: string;
35
34
  /**
36
- * Twitch OAuth scopes to request access for.
37
- * Determines what data and actions your app can access.
38
- *
39
- * @example
40
- * ```ts
41
- * {
42
- * scopes: [
43
- * "user:read:email", // Access user email
44
- * "user:read:subscriptions" // View subscriptions
45
- * ]
46
- * }
47
- * ```
48
- */
35
+ * Twitch OAuth scopes to request access for.
36
+ * Determines what data and actions your app can access.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * {
41
+ * scopes: [
42
+ * "user:read:email", // Access user email
43
+ * "user:read:subscriptions" // View subscriptions
44
+ * ]
45
+ * }
46
+ * ```
47
+ */
49
48
  readonly scopes: string[];
50
49
  }
51
50
  /**
@@ -2,84 +2,83 @@ import { Provider } from "./provider.mjs";
2
2
  import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
3
3
 
4
4
  //#region src/provider/vercel.d.ts
5
-
6
5
  /**
7
6
  * Configuration options for Vercel OAuth 2.0 + OpenID Connect provider.
8
7
  * Extends the base OAuth 2.0 configuration with Vercel-specific documentation.
9
8
  */
10
9
  interface VercelConfig extends Oauth2WrappedConfig {
11
10
  /**
12
- * Vercel OAuth App client ID.
13
- * Found in your Vercel App settings under the Authentication tab.
14
- *
15
- * To create an app:
16
- * 1. Go to Team Settings → Apps → Create
17
- * 2. Configure app details and callback URLs
18
- * 3. Copy the Client ID from the Authentication tab
19
- *
20
- * @example
21
- * ```ts
22
- * {
23
- * clientID: "oac_abc123xyz789" // Vercel OAuth App Client ID
24
- * }
25
- * ```
26
- */
11
+ * Vercel OAuth App client ID.
12
+ * Found in your Vercel App settings under the Authentication tab.
13
+ *
14
+ * To create an app:
15
+ * 1. Go to Team Settings → Apps → Create
16
+ * 2. Configure app details and callback URLs
17
+ * 3. Copy the Client ID from the Authentication tab
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * {
22
+ * clientID: "oac_abc123xyz789" // Vercel OAuth App Client ID
23
+ * }
24
+ * ```
25
+ */
27
26
  readonly clientID: string;
28
27
  /**
29
- * Vercel OAuth App client secret.
30
- * Generated in your Vercel App settings under the Authentication tab.
31
- * Keep this secure and never expose it to client-side code.
32
- *
33
- * To generate:
34
- * 1. Go to your app's Authentication tab
35
- * 2. Click "Generate Client Secret"
36
- * 3. Copy and store securely (shown only once)
37
- *
38
- * @example
39
- * ```ts
40
- * {
41
- * clientSecret: process.env.VERCEL_CLIENT_SECRET
42
- * }
43
- * ```
44
- */
28
+ * Vercel OAuth App client secret.
29
+ * Generated in your Vercel App settings under the Authentication tab.
30
+ * Keep this secure and never expose it to client-side code.
31
+ *
32
+ * To generate:
33
+ * 1. Go to your app's Authentication tab
34
+ * 2. Click "Generate Client Secret"
35
+ * 3. Copy and store securely (shown only once)
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * {
40
+ * clientSecret: process.env.VERCEL_CLIENT_SECRET
41
+ * }
42
+ * ```
43
+ */
45
44
  readonly clientSecret: string;
46
45
  /**
47
- * OpenID Connect scopes to request.
48
- * Controls what user information is included in the ID Token.
49
- *
50
- * Available scopes (must be enabled in Vercel App dashboard first):
51
- * - `openid`: Required for ID Token issuance
52
- * - `email`: User's email address
53
- * - `profile`: Name, username, and avatar
54
- * - `offline_access`: Refresh token for long-lived access (optional)
55
- *
56
- * **Important**: Enable scopes in: Vercel App → Permissions page
57
- *
58
- * @example
59
- * ```ts
60
- * {
61
- * // Basic scopes (usually sufficient)
62
- * scopes: ["openid", "email", "profile"]
63
- *
64
- * // With refresh token support (enable offline_access in dashboard first)
65
- * scopes: ["openid", "email", "profile", "offline_access"]
66
- * }
67
- * ```
68
- */
46
+ * OpenID Connect scopes to request.
47
+ * Controls what user information is included in the ID Token.
48
+ *
49
+ * Available scopes (must be enabled in Vercel App dashboard first):
50
+ * - `openid`: Required for ID Token issuance
51
+ * - `email`: User's email address
52
+ * - `profile`: Name, username, and avatar
53
+ * - `offline_access`: Refresh token for long-lived access (optional)
54
+ *
55
+ * **Important**: Enable scopes in: Vercel App → Permissions page
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * {
60
+ * // Basic scopes (usually sufficient)
61
+ * scopes: ["openid", "email", "profile"]
62
+ *
63
+ * // With refresh token support (enable offline_access in dashboard first)
64
+ * scopes: ["openid", "email", "profile", "offline_access"]
65
+ * }
66
+ * ```
67
+ */
69
68
  readonly scopes: string[];
70
69
  /**
71
- * Additional query parameters for Vercel OAuth authorization.
72
- * Useful for customizing the authorization flow.
73
- *
74
- * @example
75
- * ```ts
76
- * {
77
- * query: {
78
- * prompt: "consent" // Force consent screen every time
79
- * }
80
- * }
81
- * ```
82
- */
70
+ * Additional query parameters for Vercel OAuth authorization.
71
+ * Useful for customizing the authorization flow.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * {
76
+ * query: {
77
+ * prompt: "consent" // Force consent screen every time
78
+ * }
79
+ * }
80
+ * ```
81
+ */
83
82
  readonly query?: Record<string, string>;
84
83
  }
85
84
  /**
@@ -1,7 +1,6 @@
1
1
  import { StorageAdapter } from "./storage/storage.mjs";
2
2
 
3
3
  //#region src/revocation.d.ts
4
-
5
4
  /**
6
5
  * Data stored for a revoked token.
7
6
  * Tracks when the token was revoked and when it naturally expires.
@@ -18,37 +17,37 @@ interface RevocationRecord {
18
17
  */
19
18
  declare const Revocation: {
20
19
  /**
21
- * Revokes a token, preventing it from being used even if not yet expired.
22
- *
23
- * @param storage - Storage adapter to use
24
- * @param token - The token to revoke (access or refresh token)
25
- * @param expiresAt - When the token naturally expires (milliseconds since epoch)
26
- * @returns Promise that resolves when revocation is stored
27
- *
28
- * @example
29
- * ```ts
30
- * // Revoke a refresh token on logout
31
- * await Revocation.revoke(storage, refreshToken, expiresAt)
32
- * ```
33
- */
20
+ * Revokes a token, preventing it from being used even if not yet expired.
21
+ *
22
+ * @param storage - Storage adapter to use
23
+ * @param token - The token to revoke (access or refresh token)
24
+ * @param expiresAt - When the token naturally expires (milliseconds since epoch)
25
+ * @returns Promise that resolves when revocation is stored
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * // Revoke a refresh token on logout
30
+ * await Revocation.revoke(storage, refreshToken, expiresAt)
31
+ * ```
32
+ */
34
33
  readonly revoke: (storage: StorageAdapter, token: string, expiresAt: number) => Promise<void>;
35
34
  /**
36
- * Checks if a token has been revoked.
37
- * Returns false if token is not in revocation list (never revoked or already expired).
38
- *
39
- * @param storage - Storage adapter to use
40
- * @param token - The token to check
41
- * @returns Promise resolving to true if token is revoked, false otherwise
42
- *
43
- * @example
44
- * ```ts
45
- * // Check if token was revoked before using it
46
- * const isRevoked = await Revocation.isRevoked(storage, accessToken)
47
- * if (isRevoked) {
48
- * throw new InvalidAccessTokenError()
49
- * }
50
- * ```
51
- */
35
+ * Checks if a token has been revoked.
36
+ * Returns false if token is not in revocation list (never revoked or already expired).
37
+ *
38
+ * @param storage - Storage adapter to use
39
+ * @param token - The token to check
40
+ * @returns Promise resolving to true if token is revoked, false otherwise
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * // Check if token was revoked before using it
45
+ * const isRevoked = await Revocation.isRevoked(storage, accessToken)
46
+ * if (isRevoked) {
47
+ * throw new InvalidAccessTokenError()
48
+ * }
49
+ * ```
50
+ */
52
51
  readonly isRevoked: (storage: StorageAdapter, token: string) => Promise<boolean>;
53
52
  };
54
53
  //#endregion
@@ -0,0 +1,21 @@
1
+ import { RouterContext, VariableMap } from "./types.mjs";
2
+
3
+ //#region src/router/context.d.ts
4
+ declare class ContextBuilder<TVariables extends VariableMap = VariableMap> {
5
+ private readonly request;
6
+ private readonly matchedParams;
7
+ private readonly searchParams;
8
+ private readonly cookies;
9
+ private readonly variableManager;
10
+ private readonly responseHeaders;
11
+ private status;
12
+ private finalized;
13
+ private cachedFormData;
14
+ private formDataPromise;
15
+ private bodyText;
16
+ constructor(request: Request, matchedParams: Record<string, string>, initialVariables?: Partial<TVariables>);
17
+ build<TParams extends Record<string, string>>(): RouterContext<TParams, TVariables>;
18
+ newResponse(body?: BodyInit, init?: ResponseInit): Response;
19
+ }
20
+ //#endregion
21
+ export { ContextBuilder };
@@ -0,0 +1,193 @@
1
+ import { ContextVariableManager } from "./variables.mjs";
2
+
3
+ //#region src/router/context.ts
4
+ const parseCookies = (request) => {
5
+ const cookies = /* @__PURE__ */ new Map();
6
+ const cookieHeader = request.headers.get("cookie");
7
+ if (!cookieHeader) return cookies;
8
+ try {
9
+ for (const cookie of cookieHeader.split(";")) {
10
+ const trimmedCookie = cookie.trim();
11
+ if (!trimmedCookie) continue;
12
+ const [name, ...valueParts] = trimmedCookie.split("=");
13
+ if (!name || name.trim() === "") continue;
14
+ const trimmedName = name.trim();
15
+ if (!/^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/.test(trimmedName)) {
16
+ console.warn(`Invalid cookie name: ${trimmedName}`);
17
+ continue;
18
+ }
19
+ const value = valueParts.join("=");
20
+ try {
21
+ cookies.set(trimmedName, decodeURIComponent(value));
22
+ } catch {
23
+ cookies.set(trimmedName, value);
24
+ }
25
+ }
26
+ } catch (error) {
27
+ console.error("Failed to parse cookies:", error);
28
+ }
29
+ return cookies;
30
+ };
31
+ const serializeCookie = (name, value, options = {}) => {
32
+ if (!/^[!#$%&'*+\-.0-9A-Z^_`a-z|~]+$/.test(name)) throw new Error(`Invalid cookie name: ${name}`);
33
+ const parts = [`${name}=${encodeURIComponent(value)}`];
34
+ if (options.domain) parts.push(`Domain=${options.domain}`);
35
+ if (options.path) parts.push(`Path=${options.path}`);
36
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
37
+ if (options.maxAge) parts.push(`Max-Age=${options.maxAge}`);
38
+ if (options.httpOnly) parts.push("HttpOnly");
39
+ if (options.secure) parts.push("Secure");
40
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
41
+ return parts.join("; ");
42
+ };
43
+ const validateRedirectStatus = (status) => {
44
+ if (![
45
+ 300,
46
+ 301,
47
+ 302,
48
+ 303,
49
+ 307,
50
+ 308
51
+ ].includes(status)) throw new Error(`Invalid redirect status code: ${status}.`);
52
+ return true;
53
+ };
54
+ var ContextBuilder = class {
55
+ request;
56
+ matchedParams;
57
+ searchParams;
58
+ cookies;
59
+ variableManager;
60
+ responseHeaders = new Headers();
61
+ status = 200;
62
+ finalized = false;
63
+ cachedFormData = null;
64
+ formDataPromise = null;
65
+ bodyText = null;
66
+ constructor(request, matchedParams, initialVariables) {
67
+ if (!request || typeof request !== "object" || !request.url || !request.method) throw new Error("Invalid request object provided to ContextBuilder.");
68
+ this.request = request;
69
+ this.matchedParams = matchedParams;
70
+ this.searchParams = new URL(request.url).searchParams;
71
+ this.cookies = parseCookies(request);
72
+ this.variableManager = new ContextVariableManager(initialVariables);
73
+ Object.freeze(this.matchedParams);
74
+ }
75
+ build() {
76
+ return {
77
+ request: this.request,
78
+ params: this.matchedParams,
79
+ searchParams: this.searchParams,
80
+ query: (key) => this.searchParams.get(key) ?? void 0,
81
+ header: (key) => this.request.headers.get(key) ?? void 0,
82
+ cookie: (key) => this.cookies.get(key),
83
+ formData: async () => {
84
+ if (this.cachedFormData) return this.cachedFormData;
85
+ if (this.formDataPromise) try {
86
+ return await this.formDataPromise;
87
+ } catch {
88
+ this.formDataPromise = null;
89
+ }
90
+ this.formDataPromise = (async () => {
91
+ try {
92
+ const formData = await this.request.formData();
93
+ this.cachedFormData = formData;
94
+ return formData;
95
+ } catch (error) {
96
+ this.formDataPromise = null;
97
+ throw new Error(`Failed to read form data: ${error instanceof Error ? error.message : "Unknown error"}`);
98
+ }
99
+ })();
100
+ return this.formDataPromise;
101
+ },
102
+ parseJson: async () => {
103
+ try {
104
+ if (this.bodyText !== null) {
105
+ if (!this.bodyText) throw new Error("Request body is empty.");
106
+ return JSON.parse(this.bodyText);
107
+ }
108
+ const text = await this.request.text();
109
+ this.bodyText = text;
110
+ if (!text) throw new Error("Request body is empty.");
111
+ return JSON.parse(text);
112
+ } catch (error) {
113
+ throw new Error(`Failed to parse JSON: ${error instanceof Error ? error.message : "Unknown error"}`);
114
+ }
115
+ },
116
+ redirect: (url, status = 302) => {
117
+ validateRedirectStatus(status);
118
+ const location = url instanceof URL ? url.toString() : url;
119
+ return this.newResponse(void 0, {
120
+ status,
121
+ headers: { location }
122
+ });
123
+ },
124
+ json: (data, init) => {
125
+ const finalInit = {
126
+ ...init,
127
+ headers: {
128
+ "content-type": "application/json; charset=utf-8",
129
+ ...init?.headers
130
+ }
131
+ };
132
+ return this.newResponse(JSON.stringify(data), finalInit);
133
+ },
134
+ text: (data, init) => {
135
+ const finalInit = {
136
+ ...init,
137
+ headers: {
138
+ "content-type": "text/plain; charset=utf-8",
139
+ ...init?.headers
140
+ }
141
+ };
142
+ return this.newResponse(data, finalInit);
143
+ },
144
+ setCookie: (name, value, options) => {
145
+ try {
146
+ const cookie = serializeCookie(name, value, options);
147
+ this.responseHeaders.append("Set-Cookie", cookie);
148
+ } catch (error) {
149
+ console.error(`Failed to set cookie "${name}":`, error);
150
+ }
151
+ },
152
+ deleteCookie: (name, options) => {
153
+ try {
154
+ const cookie = serializeCookie(name, "", {
155
+ ...options,
156
+ expires: /* @__PURE__ */ new Date(0)
157
+ });
158
+ this.responseHeaders.append("Set-Cookie", cookie);
159
+ } catch (error) {
160
+ console.error(`Failed to delete cookie "${name}":`, error);
161
+ }
162
+ },
163
+ newResponse: (body, init) => this.newResponse(body, init),
164
+ set: (key, value) => {
165
+ this.variableManager.set(key, value);
166
+ },
167
+ get: (key) => {
168
+ return this.variableManager.get(key);
169
+ },
170
+ has: (key) => {
171
+ return this.variableManager.has(key);
172
+ }
173
+ };
174
+ }
175
+ newResponse(body, init) {
176
+ if (this.finalized) throw new Error("Response already finalized");
177
+ const finalHeaders = new Headers(this.responseHeaders);
178
+ if (init?.headers) new Headers(init.headers).forEach((value, key) => {
179
+ if (key.toLowerCase() === "set-cookie") finalHeaders.append(key, value);
180
+ else finalHeaders.set(key, value);
181
+ });
182
+ const response = new Response(body, {
183
+ status: init?.status || this.status,
184
+ statusText: init?.statusText,
185
+ headers: finalHeaders
186
+ });
187
+ this.finalized = true;
188
+ return response;
189
+ }
190
+ };
191
+
192
+ //#endregion
193
+ export { ContextBuilder };
@@ -0,0 +1,8 @@
1
+ import { CookieOptions, RouterContext, VariableMap } from "./types.mjs";
2
+
3
+ //#region src/router/cookies.d.ts
4
+ declare const getCookie: <TVariables extends VariableMap = VariableMap>(ctx: RouterContext<Record<string, string>, TVariables>, name: string) => string | undefined;
5
+ declare const setCookie: <TVariables extends VariableMap = VariableMap>(ctx: RouterContext<Record<string, string>, TVariables>, name: string, value: string, options?: CookieOptions) => void;
6
+ declare const deleteCookie: <TVariables extends VariableMap = VariableMap>(ctx: RouterContext<Record<string, string>, TVariables>, name: string, options?: Pick<CookieOptions, "domain" | "path">) => void;
7
+ //#endregion
8
+ export { deleteCookie, getCookie, setCookie };
@@ -0,0 +1,13 @@
1
+ //#region src/router/cookies.ts
2
+ const getCookie = (ctx, name) => {
3
+ return ctx.cookie(name);
4
+ };
5
+ const setCookie = (ctx, name, value, options) => {
6
+ ctx.setCookie(name, value, options);
7
+ };
8
+ const deleteCookie = (ctx, name, options) => {
9
+ ctx.deleteCookie(name, options);
10
+ };
11
+
12
+ //#endregion
13
+ export { deleteCookie, getCookie, setCookie };
@@ -0,0 +1,21 @@
1
+ import { AnyHandler, ErrorHandler, ExtractParams, GlobalMiddleware, RouterEnvironment, RouterOptions } from "./types.mjs";
2
+
3
+ //#region src/router/index.d.ts
4
+ declare class Router<TEnvironment extends RouterEnvironment = RouterEnvironment> {
5
+ private readonly routes;
6
+ private readonly matcher;
7
+ private readonly globalMiddleware;
8
+ private errorHandler?;
9
+ constructor(routerOptions?: RouterOptions);
10
+ private addRoute;
11
+ get<TPath extends string>(path: TPath, handler: AnyHandler<ExtractParams<TPath>, TEnvironment["Variables"]>): this;
12
+ post<TPath extends string>(path: TPath, handler: AnyHandler<ExtractParams<TPath>, TEnvironment["Variables"]>): this;
13
+ use(middleware: GlobalMiddleware<TEnvironment["Variables"]>): this;
14
+ onError(handler: ErrorHandler<TEnvironment["Variables"]>): this;
15
+ mount<TMountedEnvironment extends RouterEnvironment>(path: string, router: Router<TMountedEnvironment>): this;
16
+ handle(request: Request, initialVariables?: Partial<TEnvironment["Variables"]>): Promise<Response>;
17
+ get fetch(): (request: Request) => Promise<Response>;
18
+ private createErrorResponse;
19
+ }
20
+ //#endregion
21
+ export { Router };
@@ -0,0 +1,107 @@
1
+ import { ContextBuilder } from "./context.mjs";
2
+ import { RouteMatcher } from "./matcher.mjs";
3
+ import { makeSafeRequest } from "./safe-request.mjs";
4
+
5
+ //#region src/router/index.ts
6
+ var Router = class {
7
+ routes = /* @__PURE__ */ new Map();
8
+ matcher;
9
+ globalMiddleware = [];
10
+ errorHandler;
11
+ constructor(routerOptions = {}) {
12
+ this.matcher = new RouteMatcher(routerOptions);
13
+ }
14
+ addRoute(method, path, handler) {
15
+ const { handler: mainHandler, middleware = [] } = typeof handler === "function" ? { handler } : handler;
16
+ if (typeof mainHandler !== "function") throw new Error(`Handler for ${method} ${path} must be a function.`);
17
+ const route = {
18
+ method,
19
+ pattern: path,
20
+ handler: mainHandler,
21
+ middleware: [...this.globalMiddleware, ...middleware],
22
+ compiled: this.matcher.compile(path)
23
+ };
24
+ const methodRoutes = this.routes.get(method) ?? [];
25
+ if (methodRoutes.some((r) => r.pattern === path)) console.warn(`Route already exists: ${method} ${path}. Overwriting.`);
26
+ const newRoutes = [...methodRoutes.filter((r) => r.pattern !== path), route];
27
+ const sortedPatterns = this.matcher.sortRoutesBySpecificity(newRoutes.map((r) => r.pattern));
28
+ newRoutes.sort((a, b) => sortedPatterns.indexOf(a.pattern) - sortedPatterns.indexOf(b.pattern));
29
+ this.routes.set(method, newRoutes);
30
+ return this;
31
+ }
32
+ get(path, handler) {
33
+ return this.addRoute("GET", path, handler);
34
+ }
35
+ post(path, handler) {
36
+ return this.addRoute("POST", path, handler);
37
+ }
38
+ use(middleware) {
39
+ this.globalMiddleware.push(middleware);
40
+ return this;
41
+ }
42
+ onError(handler) {
43
+ this.errorHandler = handler;
44
+ return this;
45
+ }
46
+ mount(path, router) {
47
+ const normalizedPath = path.endsWith("/") ? path.slice(0, -1) : path;
48
+ for (const [method, routes] of router.routes) for (const route of routes) this.addRoute(method, `${normalizedPath}${route.pattern}`, {
49
+ handler: route.handler,
50
+ middleware: route.middleware
51
+ });
52
+ return this;
53
+ }
54
+ async handle(request, initialVariables) {
55
+ const safeRequest = await makeSafeRequest(request);
56
+ try {
57
+ const url = new URL(safeRequest.url);
58
+ const method = safeRequest.method.toUpperCase();
59
+ const pathname = this.matcher.normalizePath(url.pathname);
60
+ const methodRoutes = this.routes.get(method);
61
+ if (!methodRoutes) return this.createErrorResponse("Not Found", 404);
62
+ for (const route of methodRoutes) {
63
+ const match = this.matcher.match(route.pattern, pathname);
64
+ if (match) {
65
+ const context = new ContextBuilder(safeRequest, match.params, initialVariables).build();
66
+ const allMiddleware = [...this.globalMiddleware, ...route.middleware];
67
+ const run = async (index, ctx) => {
68
+ if (index < allMiddleware.length) {
69
+ const middleware = allMiddleware[index];
70
+ if (middleware) return middleware(ctx, () => run(index + 1, ctx));
71
+ }
72
+ return route.handler(ctx);
73
+ };
74
+ return await run(0, context);
75
+ }
76
+ }
77
+ return this.createErrorResponse(`Route not found: ${method} ${pathname}`, 404);
78
+ } catch (error) {
79
+ console.error("Router handle error:", error);
80
+ if (this.errorHandler) try {
81
+ const errorContext = new ContextBuilder(safeRequest, {}, initialVariables).build();
82
+ return await this.errorHandler(error, errorContext);
83
+ } catch (handlerError) {
84
+ console.error("Error handler failed:", handlerError);
85
+ }
86
+ const message = error instanceof Error ? error.message : "Internal Server Error";
87
+ return this.createErrorResponse(message, 500);
88
+ }
89
+ }
90
+ get fetch() {
91
+ return (request) => {
92
+ return this.handle(request);
93
+ };
94
+ }
95
+ createErrorResponse(message, status) {
96
+ return new Response(JSON.stringify({
97
+ error: message,
98
+ status
99
+ }), {
100
+ status,
101
+ headers: { "Content-Type": "application/json" }
102
+ });
103
+ }
104
+ };
105
+
106
+ //#endregion
107
+ export { Router };