@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.
- package/dist/adapters/node.d.mts +0 -1
- package/dist/client.d.mts +293 -287
- package/dist/client.mjs +1 -0
- package/dist/core.d.mts +23 -24
- package/dist/core.mjs +6 -6
- package/dist/error.d.mts +53 -53
- package/dist/keys.d.mts +0 -1
- package/dist/mutex.d.mts +14 -14
- package/dist/provider/apple.d.mts +34 -35
- package/dist/provider/code.d.mts +75 -85
- package/dist/provider/code.mjs +83 -0
- package/dist/provider/discord.d.mts +49 -50
- package/dist/provider/facebook.d.mts +49 -50
- package/dist/provider/github.d.mts +50 -51
- package/dist/provider/gitlab.d.mts +34 -35
- package/dist/provider/google.d.mts +49 -50
- package/dist/provider/linkedin.d.mts +47 -48
- package/dist/provider/magiclink.d.mts +28 -38
- package/dist/provider/magiclink.mjs +57 -0
- package/dist/provider/microsoft.d.mts +67 -68
- package/dist/provider/oauth2.d.mts +75 -76
- package/dist/provider/oauth2.mjs +57 -0
- package/dist/provider/passkey.d.mts +20 -21
- package/dist/provider/password.d.mts +174 -202
- package/dist/provider/provider.d.mts +107 -109
- package/dist/provider/reddit.d.mts +33 -34
- package/dist/provider/slack.d.mts +34 -35
- package/dist/provider/spotify.d.mts +34 -35
- package/dist/provider/totp.d.mts +43 -44
- package/dist/provider/twitch.d.mts +33 -34
- package/dist/provider/vercel.d.mts +65 -66
- package/dist/revocation.d.mts +29 -30
- package/dist/router/context.d.mts +21 -0
- package/dist/router/context.mjs +193 -0
- package/dist/router/cookies.d.mts +8 -0
- package/dist/router/cookies.mjs +13 -0
- package/dist/router/index.d.mts +21 -0
- package/dist/router/index.mjs +107 -0
- package/dist/router/matcher.d.mts +15 -0
- package/dist/router/matcher.mjs +76 -0
- package/dist/router/middleware/cors.d.mts +15 -0
- package/dist/router/middleware/cors.mjs +114 -0
- package/dist/router/safe-request.d.mts +52 -0
- package/dist/router/safe-request.mjs +160 -0
- package/dist/router/types.d.mts +67 -0
- package/dist/router/types.mjs +1 -0
- package/dist/router/variables.d.mts +12 -0
- package/dist/router/variables.mjs +20 -0
- package/dist/storage/memory.d.mts +11 -12
- package/dist/storage/storage.d.mts +110 -110
- package/dist/storage/turso.d.mts +0 -1
- package/dist/storage/unstorage.d.mts +0 -1
- package/dist/subject.d.mts +0 -1
- package/dist/themes/theme.d.mts +101 -101
- package/dist/toolkit/client.d.mts +56 -57
- package/dist/toolkit/providers/facebook.d.mts +0 -1
- package/dist/toolkit/providers/github.d.mts +0 -1
- package/dist/toolkit/providers/google.d.mts +0 -1
- package/dist/toolkit/storage.d.mts +8 -8
- package/dist/ui/base.d.mts +0 -1
- package/dist/ui/code.d.mts +5 -6
- package/dist/ui/form.d.mts +6 -7
- package/dist/ui/icon.d.mts +0 -1
- package/dist/ui/magiclink.d.mts +5 -6
- package/dist/ui/passkey.d.mts +0 -1
- package/dist/ui/password.d.mts +2 -3
- package/dist/ui/select.d.mts +0 -1
- package/dist/ui/totp.d.mts +0 -1
- package/dist/util.d.mts +1 -2
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
/**
|
package/dist/revocation.d.mts
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 };
|