@fresho/router 0.1.0 → 0.3.2
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/README.md +24 -0
- package/dist/auth/index.d.mts +327 -0
- package/dist/auth/index.d.mts.map +1 -0
- package/dist/auth/index.mjs +279 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/index.d.mts +57 -99
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +112 -54
- package/dist/index.mjs.map +1 -1
- package/dist/jwt-DHM_nSwL.d.mts +124 -0
- package/dist/jwt-DHM_nSwL.d.mts.map +1 -0
- package/dist/jwt-Dab9kOC2.mjs +208 -0
- package/dist/jwt-Dab9kOC2.mjs.map +1 -0
- package/dist/middleware/index.d.mts +2 -99
- package/dist/middleware/index.d.mts.map +1 -1
- package/dist/middleware/index.mjs +11 -150
- package/dist/middleware/index.mjs.map +1 -1
- package/dist/{middleware-CqtYOc6D.d.mts → middleware-Bxq0LHLS.d.mts} +1 -1
- package/dist/{middleware-CqtYOc6D.d.mts.map → middleware-Bxq0LHLS.d.mts.map} +1 -1
- package/dist/{middleware-BpFh0XrR.mjs → middleware-XMbIevSv.mjs} +1 -1
- package/dist/{middleware-BpFh0XrR.mjs.map → middleware-XMbIevSv.mjs.map} +1 -1
- package/docs/authentication.md +29 -2
- package/docs/middleware.md +10 -1
- package/package.json +6 -2
package/README.md
CHANGED
|
@@ -214,6 +214,30 @@ Built-in middleware: `cors`, `errorHandler`, `logger`, `rateLimit`, `requestId`,
|
|
|
214
214
|
|
|
215
215
|
See **[Middleware Documentation](docs/middleware.md)** for detailed usage.
|
|
216
216
|
|
|
217
|
+
## Authentication Utilities
|
|
218
|
+
|
|
219
|
+
For standalone auth utilities (JWT signing/verification, Basic auth, OAuth), import from `@fresho/router/auth`:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { jwtSign, jwtVerify, parseBasicAuth } from "@fresho/router/auth";
|
|
223
|
+
|
|
224
|
+
// Sign a JWT
|
|
225
|
+
const token = await jwtSign({ uid: "user-123" }, secret, { expiresIn: "1h" });
|
|
226
|
+
|
|
227
|
+
// Verify a JWT
|
|
228
|
+
const payload = await jwtVerify(token, secret);
|
|
229
|
+
|
|
230
|
+
// Parse Basic auth header
|
|
231
|
+
const creds = parseBasicAuth(request.headers.get("Authorization"));
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Available utilities:
|
|
235
|
+
- **JWT**: `jwtSign`, `jwtVerify`
|
|
236
|
+
- **Basic Auth**: `parseBasicAuth`, `encodeBasicAuth`, `extractBasicAuthToken`
|
|
237
|
+
- **OAuth**: `encodeOAuthState`, `decodeOAuthState`, `exchangeCode`, `refreshAccessToken`, `revokeToken`, `buildAuthorizationUrl`, `OAUTH_PROVIDERS`
|
|
238
|
+
|
|
239
|
+
See **[Authentication Documentation](docs/authentication.md)** for detailed usage.
|
|
240
|
+
|
|
217
241
|
## HTTP Client
|
|
218
242
|
|
|
219
243
|
Generate a typed client for your API:
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import { a as VerifyJwtOptions, i as SignJwtOptions, n as JwtPayload, o as jwtSign, r as JwtSecret, s as jwtVerify, t as JwtAlgorithm } from "../jwt-DHM_nSwL.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/basic.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @fileoverview Basic HTTP authentication utilities.
|
|
7
|
+
*
|
|
8
|
+
* Provides functions for parsing and encoding Basic authentication credentials.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { parseBasicAuth, encodeBasicAuth } from '@fresho/router/auth';
|
|
13
|
+
*
|
|
14
|
+
* // Parse credentials from Authorization header
|
|
15
|
+
* const credentials = parseBasicAuth('Basic dXNlcm5hbWU6cGFzc3dvcmQ=');
|
|
16
|
+
* if (credentials) {
|
|
17
|
+
* console.log(credentials.username); // 'username'
|
|
18
|
+
* console.log(credentials.password); // 'password'
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* // Create an Authorization header value
|
|
22
|
+
* const header = encodeBasicAuth('username', 'password');
|
|
23
|
+
* // 'Basic dXNlcm5hbWU6cGFzc3dvcmQ='
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
/** Parsed Basic authentication credentials. */
|
|
27
|
+
interface BasicCredentials {
|
|
28
|
+
/** The username from the credentials. */
|
|
29
|
+
username: string;
|
|
30
|
+
/** The password from the credentials. */
|
|
31
|
+
password: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Parses Basic authentication credentials from an Authorization header value.
|
|
35
|
+
*
|
|
36
|
+
* @param authHeader - The full Authorization header value (e.g., 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
|
|
37
|
+
* @returns The parsed credentials, or null if parsing fails
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* const header = request.headers.get('Authorization');
|
|
42
|
+
* const credentials = parseBasicAuth(header);
|
|
43
|
+
* if (credentials) {
|
|
44
|
+
* // Verify credentials
|
|
45
|
+
* const isValid = await verifyPassword(credentials.username, credentials.password);
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function parseBasicAuth(authHeader: string | null): BasicCredentials | null;
|
|
50
|
+
/**
|
|
51
|
+
* Encodes credentials into a Basic authentication header value.
|
|
52
|
+
*
|
|
53
|
+
* @param username - The username
|
|
54
|
+
* @param password - The password
|
|
55
|
+
* @returns The Authorization header value (e.g., 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=')
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const authHeader = encodeBasicAuth('admin', 'secret');
|
|
60
|
+
* fetch('/api/endpoint', {
|
|
61
|
+
* headers: { Authorization: authHeader }
|
|
62
|
+
* });
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
declare function encodeBasicAuth(username: string, password: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Extracts the raw base64-encoded credentials from an Authorization header.
|
|
68
|
+
*
|
|
69
|
+
* @param authHeader - The full Authorization header value
|
|
70
|
+
* @returns The base64 string, or null if not a Basic auth header
|
|
71
|
+
*/
|
|
72
|
+
declare function extractBasicAuthToken(authHeader: string | null): string | null;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/auth/oauth.d.ts
|
|
75
|
+
/**
|
|
76
|
+
* @fileoverview OAuth 2.0 utilities for @fresho/router.
|
|
77
|
+
*
|
|
78
|
+
* Provides reusable primitives for implementing OAuth 2.0 Authorization Code flows.
|
|
79
|
+
* This implementation is designed as a confidential client suitable for server-side
|
|
80
|
+
* applications that can securely store client secrets.
|
|
81
|
+
*
|
|
82
|
+
* ## Design Decisions
|
|
83
|
+
*
|
|
84
|
+
* ### Why Not PKCE?
|
|
85
|
+
*
|
|
86
|
+
* PKCE (Proof Key for Code Exchange, RFC 7636) is a security extension designed to
|
|
87
|
+
* protect against authorization code interception attacks. However:
|
|
88
|
+
*
|
|
89
|
+
* - PKCE is **required** only for public clients (mobile apps, SPAs without backend)
|
|
90
|
+
* - PKCE is **optional** for confidential clients that can securely store secrets
|
|
91
|
+
* - These utilities assume a confidential client with a `client_secret`
|
|
92
|
+
* - The `client_secret` provides equivalent protection during token exchange
|
|
93
|
+
*
|
|
94
|
+
* ### HMAC-Signed State Parameter
|
|
95
|
+
*
|
|
96
|
+
* Instead of PKCE, we provide HMAC-signed state utilities that:
|
|
97
|
+
*
|
|
98
|
+
* 1. **Prevent CSRF attacks** - The signature is verified on callback
|
|
99
|
+
* 2. **Carry custom data** - Embed arbitrary data (e.g., user ID) in the state
|
|
100
|
+
* 3. **Include a nonce** - Random bytes prevent replay attacks
|
|
101
|
+
* 4. **Use Web Crypto API** - Works in Cloudflare Workers, Deno, and browsers
|
|
102
|
+
*
|
|
103
|
+
* ## Provider Compatibility
|
|
104
|
+
*
|
|
105
|
+
* This implementation pattern works with ~60-70% of OAuth 2.0 providers:
|
|
106
|
+
*
|
|
107
|
+
* | Provider | PKCE | Auth Method | Compatible |
|
|
108
|
+
* |------------|----------|-------------|------------|
|
|
109
|
+
* | Google | Optional | Body | ✅ Yes |
|
|
110
|
+
* | GitHub | None | Basic Auth | ✅ Minor tweak |
|
|
111
|
+
* | Slack | None | Body | ✅ Yes |
|
|
112
|
+
* | LinkedIn | None | Body | ✅ Yes |
|
|
113
|
+
* | Spotify | Optional | Basic Auth | ✅ Yes |
|
|
114
|
+
* | Discord | Optional | Body | ✅ Yes |
|
|
115
|
+
* | Dropbox | None | Basic Auth | ✅ Minor tweak |
|
|
116
|
+
* | Twitter | Required | - | ❌ Needs PKCE |
|
|
117
|
+
* | TikTok | Required | - | ❌ Needs PKCE |
|
|
118
|
+
*
|
|
119
|
+
* @see https://tools.ietf.org/html/rfc6749 - OAuth 2.0 Authorization Framework
|
|
120
|
+
* @see https://tools.ietf.org/html/rfc7636 - PKCE Extension
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```typescript
|
|
124
|
+
* import { encodeOAuthState, decodeOAuthState, exchangeCode } from '@fresho/router/middleware';
|
|
125
|
+
*
|
|
126
|
+
* // In your authorize route:
|
|
127
|
+
* const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);
|
|
128
|
+
* const authUrl = `https://provider.com/oauth/authorize?state=${state}&...`;
|
|
129
|
+
*
|
|
130
|
+
* // In your callback route:
|
|
131
|
+
* const data = await decodeOAuthState(state, SECRET);
|
|
132
|
+
* if (data) {
|
|
133
|
+
* const tokens = await exchangeCode(code, { ... });
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
/** OAuth 2.0 token response as per RFC 6749. */
|
|
138
|
+
interface OAuthTokenResponse {
|
|
139
|
+
/** The access token issued by the authorization server. */
|
|
140
|
+
access_token: string;
|
|
141
|
+
/** The refresh token (optional, returned with offline access). */
|
|
142
|
+
refresh_token?: string;
|
|
143
|
+
/** The type of token (typically "Bearer"). */
|
|
144
|
+
token_type: string;
|
|
145
|
+
/** The lifetime in seconds of the access token. */
|
|
146
|
+
expires_in: number;
|
|
147
|
+
/** The scopes granted by the authorization server. */
|
|
148
|
+
scope?: string;
|
|
149
|
+
/** OpenID Connect ID token (if openid scope was requested). */
|
|
150
|
+
id_token?: string;
|
|
151
|
+
}
|
|
152
|
+
/** OAuth 2.0 provider configuration. */
|
|
153
|
+
interface OAuthProvider {
|
|
154
|
+
/** Authorization endpoint URL. */
|
|
155
|
+
authorizationUrl: string;
|
|
156
|
+
/** Token endpoint URL. */
|
|
157
|
+
tokenUrl: string;
|
|
158
|
+
/** Token revocation endpoint URL (optional). */
|
|
159
|
+
revokeUrl?: string;
|
|
160
|
+
/** User info endpoint URL (optional). */
|
|
161
|
+
userInfoUrl?: string;
|
|
162
|
+
}
|
|
163
|
+
/** Common OAuth 2.0 providers with their endpoints. */
|
|
164
|
+
declare const OAUTH_PROVIDERS: Record<string, OAuthProvider>;
|
|
165
|
+
/** Token endpoint authentication method. */
|
|
166
|
+
type TokenAuthMethod = 'body' | 'basic';
|
|
167
|
+
/** Configuration for token exchange. */
|
|
168
|
+
interface ExchangeCodeOptions {
|
|
169
|
+
/** Token endpoint URL. */
|
|
170
|
+
tokenUrl: string;
|
|
171
|
+
/** OAuth client ID. */
|
|
172
|
+
clientId: string;
|
|
173
|
+
/** OAuth client secret. */
|
|
174
|
+
clientSecret: string;
|
|
175
|
+
/** Redirect URI used in authorization request. */
|
|
176
|
+
redirectUri: string;
|
|
177
|
+
/** Authentication method (default: 'body'). */
|
|
178
|
+
authMethod?: TokenAuthMethod;
|
|
179
|
+
}
|
|
180
|
+
/** Configuration for token revocation. */
|
|
181
|
+
interface RevokeTokenOptions {
|
|
182
|
+
/** Revocation endpoint URL. */
|
|
183
|
+
revokeUrl: string;
|
|
184
|
+
/** Token to revoke. */
|
|
185
|
+
token: string;
|
|
186
|
+
/** Token type hint (optional, e.g., 'refresh_token' or 'access_token'). */
|
|
187
|
+
tokenTypeHint?: string;
|
|
188
|
+
/** OAuth client ID (for providers that require it). */
|
|
189
|
+
clientId?: string;
|
|
190
|
+
/** OAuth client secret (for providers that require it). */
|
|
191
|
+
clientSecret?: string;
|
|
192
|
+
/** Authentication method (default: 'body'). */
|
|
193
|
+
authMethod?: TokenAuthMethod;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Encodes arbitrary data into an HMAC-signed state parameter.
|
|
197
|
+
*
|
|
198
|
+
* The state is base64url-encoded and includes:
|
|
199
|
+
* - The JSON-serialized payload
|
|
200
|
+
* - A random nonce (12 bytes) for replay protection
|
|
201
|
+
* - An HMAC-SHA256 signature (truncated to 16 hex chars)
|
|
202
|
+
*
|
|
203
|
+
* @param data - Arbitrary data to encode in the state
|
|
204
|
+
* @param secret - Secret key for HMAC signing
|
|
205
|
+
* @returns Base64url-encoded state string
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const state = await encodeOAuthState({ uid: 'user-123', returnTo: '/dashboard' }, SECRET);
|
|
210
|
+
* // Use in OAuth authorization URL
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
declare function encodeOAuthState(data: Record<string, unknown>, secret: string): Promise<string>;
|
|
214
|
+
/**
|
|
215
|
+
* Decodes and verifies an HMAC-signed state parameter.
|
|
216
|
+
*
|
|
217
|
+
* @param state - Base64url-encoded state string
|
|
218
|
+
* @param secret - Secret key for HMAC verification
|
|
219
|
+
* @returns The decoded data, or null if verification fails
|
|
220
|
+
*
|
|
221
|
+
* @example
|
|
222
|
+
* ```typescript
|
|
223
|
+
* const data = await decodeOAuthState(state, SECRET);
|
|
224
|
+
* if (data) {
|
|
225
|
+
* console.log(data.uid); // 'user-123'
|
|
226
|
+
* }
|
|
227
|
+
* ```
|
|
228
|
+
*/
|
|
229
|
+
declare function decodeOAuthState<T extends Record<string, unknown> = Record<string, unknown>>(state: string, secret: string): Promise<T | null>;
|
|
230
|
+
/**
|
|
231
|
+
* Exchanges an authorization code for tokens.
|
|
232
|
+
*
|
|
233
|
+
* @param code - Authorization code from OAuth callback
|
|
234
|
+
* @param options - Token exchange configuration
|
|
235
|
+
* @returns Token response from the OAuth provider
|
|
236
|
+
* @throws Error if token exchange fails
|
|
237
|
+
*
|
|
238
|
+
* @example
|
|
239
|
+
* ```typescript
|
|
240
|
+
* const tokens = await exchangeCode(code, {
|
|
241
|
+
* tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,
|
|
242
|
+
* clientId: env.CLIENT_ID,
|
|
243
|
+
* clientSecret: env.CLIENT_SECRET,
|
|
244
|
+
* redirectUri: 'https://example.com/callback',
|
|
245
|
+
* });
|
|
246
|
+
* ```
|
|
247
|
+
*/
|
|
248
|
+
declare function exchangeCode(code: string, options: ExchangeCodeOptions): Promise<OAuthTokenResponse>;
|
|
249
|
+
/**
|
|
250
|
+
* Refreshes an access token using a refresh token.
|
|
251
|
+
*
|
|
252
|
+
* @param refreshToken - The refresh token
|
|
253
|
+
* @param options - Token exchange configuration (same as exchangeCode)
|
|
254
|
+
* @returns New token response from the OAuth provider
|
|
255
|
+
* @throws Error if token refresh fails
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* ```typescript
|
|
259
|
+
* const tokens = await refreshAccessToken(refreshToken, {
|
|
260
|
+
* tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,
|
|
261
|
+
* clientId: env.CLIENT_ID,
|
|
262
|
+
* clientSecret: env.CLIENT_SECRET,
|
|
263
|
+
* redirectUri: 'https://example.com/callback',
|
|
264
|
+
* });
|
|
265
|
+
* ```
|
|
266
|
+
*/
|
|
267
|
+
declare function refreshAccessToken(refreshToken: string, options: Omit<ExchangeCodeOptions, 'redirectUri'>): Promise<OAuthTokenResponse>;
|
|
268
|
+
/**
|
|
269
|
+
* Revokes an OAuth token.
|
|
270
|
+
*
|
|
271
|
+
* @param options - Token revocation configuration
|
|
272
|
+
* @returns True if revocation succeeded
|
|
273
|
+
* @throws Error if revocation fails
|
|
274
|
+
*
|
|
275
|
+
* @example
|
|
276
|
+
* ```typescript
|
|
277
|
+
* await revokeToken({
|
|
278
|
+
* revokeUrl: OAUTH_PROVIDERS.google.revokeUrl!,
|
|
279
|
+
* token: refreshToken,
|
|
280
|
+
* tokenTypeHint: 'refresh_token',
|
|
281
|
+
* });
|
|
282
|
+
* ```
|
|
283
|
+
*/
|
|
284
|
+
declare function revokeToken(options: RevokeTokenOptions): Promise<boolean>;
|
|
285
|
+
/** Options for building an authorization URL. */
|
|
286
|
+
interface AuthorizationUrlOptions {
|
|
287
|
+
/** Authorization endpoint URL. */
|
|
288
|
+
authorizationUrl: string;
|
|
289
|
+
/** OAuth client ID. */
|
|
290
|
+
clientId: string;
|
|
291
|
+
/** Redirect URI for OAuth callback. */
|
|
292
|
+
redirectUri: string;
|
|
293
|
+
/** Requested scopes. */
|
|
294
|
+
scopes: string[];
|
|
295
|
+
/** State parameter (use encodeOAuthState to generate). */
|
|
296
|
+
state: string;
|
|
297
|
+
/** Response type (default: 'code'). */
|
|
298
|
+
responseType?: string;
|
|
299
|
+
/** Additional provider-specific parameters. */
|
|
300
|
+
extraParams?: Record<string, string>;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Builds an OAuth authorization URL.
|
|
304
|
+
*
|
|
305
|
+
* @param options - Authorization URL configuration
|
|
306
|
+
* @returns The complete authorization URL
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```typescript
|
|
310
|
+
* const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);
|
|
311
|
+
* const authUrl = buildAuthorizationUrl({
|
|
312
|
+
* authorizationUrl: OAUTH_PROVIDERS.google.authorizationUrl,
|
|
313
|
+
* clientId: env.CLIENT_ID,
|
|
314
|
+
* redirectUri: 'https://example.com/callback',
|
|
315
|
+
* scopes: ['openid', 'email', 'profile'],
|
|
316
|
+
* state,
|
|
317
|
+
* extraParams: {
|
|
318
|
+
* access_type: 'offline',
|
|
319
|
+
* prompt: 'consent',
|
|
320
|
+
* },
|
|
321
|
+
* });
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
declare function buildAuthorizationUrl(options: AuthorizationUrlOptions): string;
|
|
325
|
+
//#endregion
|
|
326
|
+
export { type AuthorizationUrlOptions, type BasicCredentials, type ExchangeCodeOptions, type JwtAlgorithm, type JwtPayload, type JwtSecret, OAUTH_PROVIDERS, type OAuthProvider, type OAuthTokenResponse, type RevokeTokenOptions, type SignJwtOptions, type TokenAuthMethod, type VerifyJwtOptions, buildAuthorizationUrl, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
|
|
327
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/auth/basic.ts","../../src/auth/oauth.ts"],"sourcesContent":[],"mappings":";;;;;;;AA2BA;AA2BA;AAsCA;AAUA;;;;AClCA;AAgBA;AAYA;AA8CA;AAGA;AAcA;AAqCA;AAmDA;;;;;AAGU,UD/NO,gBAAA,CC+NP;EA6DY;EAEX,QAAA,EAAA,MAAA;EACA;EAAR,QAAA,EAAA,MAAA;;AAqDH;;;;;;AAqDA;AAkCA;AAuCA;;;;;;;;iBDvbgB,cAAA,6BAA2C;;;;;;;;;;;;;;;;iBAsC3C,eAAA;;;;;;;iBAUA,qBAAA;;;;;;AA3EhB;AA2BA;AAsCA;AAUA;;;;AClCA;AAgBA;AAYA;AA8CA;AAGA;AAcA;AAqCA;AAmDA;;;;;;AAgEA;;;;;AAwDA;;;;;;AAqDA;AAkCA;AAuCA;;;;;;;;;;;;;;;;;;;;;;;;;;;UAzaiB,kBAAA;;;;;;;;;;;;;;;UAgBA,aAAA;;;;;;;;;;;cAYJ,iBAAiB,eAAe;;KA8CjC,eAAA;;UAGK,mBAAA;;;;;;;;;;eAUF;;;UAIE,kBAAA;;;;;;;;;;;;eAYF;;;;;;;;;;;;;;;;;;;;iBAyBO,gBAAA,OACd,0CAEL;;;;;;;;;;;;;;;;iBAgDmB,2BAA2B,0BAA0B,yDAGxE,QAAQ;;;;;;;;;;;;;;;;;;;iBA6DW,YAAA,wBAEX,sBACR,QAAQ;;;;;;;;;;;;;;;;;;;iBAqDW,kBAAA,gCAEX,KAAK,sCACb,QAAQ;;;;;;;;;;;;;;;;;iBAkDW,WAAA,UAAqB,qBAAqB;;UAkC/C,uBAAA;;;;;;;;;;;;;;gBAcD;;;;;;;;;;;;;;;;;;;;;;;;iBAyBA,qBAAA,UAA+B"}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { a as parseBasicAuth, i as extractBasicAuthToken, n as jwtVerify, r as encodeBasicAuth, t as jwtSign } from "../jwt-Dab9kOC2.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/auth/oauth.ts
|
|
4
|
+
/** Common OAuth 2.0 providers with their endpoints. */
|
|
5
|
+
const OAUTH_PROVIDERS = {
|
|
6
|
+
google: {
|
|
7
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
8
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
9
|
+
revokeUrl: "https://oauth2.googleapis.com/revoke",
|
|
10
|
+
userInfoUrl: "https://www.googleapis.com/oauth2/v3/userinfo"
|
|
11
|
+
},
|
|
12
|
+
github: {
|
|
13
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
14
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
15
|
+
revokeUrl: void 0,
|
|
16
|
+
userInfoUrl: "https://api.github.com/user"
|
|
17
|
+
},
|
|
18
|
+
discord: {
|
|
19
|
+
authorizationUrl: "https://discord.com/oauth2/authorize",
|
|
20
|
+
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
21
|
+
revokeUrl: "https://discord.com/api/oauth2/token/revoke",
|
|
22
|
+
userInfoUrl: "https://discord.com/api/users/@me"
|
|
23
|
+
},
|
|
24
|
+
slack: {
|
|
25
|
+
authorizationUrl: "https://slack.com/openid/connect/authorize",
|
|
26
|
+
tokenUrl: "https://slack.com/api/openid.connect.token",
|
|
27
|
+
revokeUrl: void 0,
|
|
28
|
+
userInfoUrl: void 0
|
|
29
|
+
},
|
|
30
|
+
linkedin: {
|
|
31
|
+
authorizationUrl: "https://www.linkedin.com/oauth/v2/authorization",
|
|
32
|
+
tokenUrl: "https://www.linkedin.com/oauth/v2/accessToken",
|
|
33
|
+
revokeUrl: void 0,
|
|
34
|
+
userInfoUrl: "https://api.linkedin.com/v2/userinfo"
|
|
35
|
+
},
|
|
36
|
+
spotify: {
|
|
37
|
+
authorizationUrl: "https://accounts.spotify.com/authorize",
|
|
38
|
+
tokenUrl: "https://accounts.spotify.com/api/token",
|
|
39
|
+
revokeUrl: void 0,
|
|
40
|
+
userInfoUrl: "https://api.spotify.com/v1/me"
|
|
41
|
+
},
|
|
42
|
+
dropbox: {
|
|
43
|
+
authorizationUrl: "https://www.dropbox.com/oauth2/authorize",
|
|
44
|
+
tokenUrl: "https://api.dropboxapi.com/oauth2/token",
|
|
45
|
+
revokeUrl: "https://api.dropboxapi.com/2/auth/token/revoke",
|
|
46
|
+
userInfoUrl: void 0
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Encodes arbitrary data into an HMAC-signed state parameter.
|
|
51
|
+
*
|
|
52
|
+
* The state is base64url-encoded and includes:
|
|
53
|
+
* - The JSON-serialized payload
|
|
54
|
+
* - A random nonce (12 bytes) for replay protection
|
|
55
|
+
* - An HMAC-SHA256 signature (truncated to 16 hex chars)
|
|
56
|
+
*
|
|
57
|
+
* @param data - Arbitrary data to encode in the state
|
|
58
|
+
* @param secret - Secret key for HMAC signing
|
|
59
|
+
* @returns Base64url-encoded state string
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```typescript
|
|
63
|
+
* const state = await encodeOAuthState({ uid: 'user-123', returnTo: '/dashboard' }, SECRET);
|
|
64
|
+
* // Use in OAuth authorization URL
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
async function encodeOAuthState(data, secret) {
|
|
68
|
+
const nonceBytes = new Uint8Array(12);
|
|
69
|
+
crypto.getRandomValues(nonceBytes);
|
|
70
|
+
const nonce = btoa(String.fromCharCode(...nonceBytes)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
71
|
+
const payload = JSON.stringify({
|
|
72
|
+
...data,
|
|
73
|
+
_nonce: nonce
|
|
74
|
+
});
|
|
75
|
+
const encoder = new TextEncoder();
|
|
76
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
77
|
+
name: "HMAC",
|
|
78
|
+
hash: "SHA-256"
|
|
79
|
+
}, false, ["sign"]);
|
|
80
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
81
|
+
const signatureHex = Array.from(new Uint8Array(signature)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16);
|
|
82
|
+
const stateData = JSON.stringify({
|
|
83
|
+
payload,
|
|
84
|
+
sig: signatureHex
|
|
85
|
+
});
|
|
86
|
+
return btoa(stateData).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Decodes and verifies an HMAC-signed state parameter.
|
|
90
|
+
*
|
|
91
|
+
* @param state - Base64url-encoded state string
|
|
92
|
+
* @param secret - Secret key for HMAC verification
|
|
93
|
+
* @returns The decoded data, or null if verification fails
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```typescript
|
|
97
|
+
* const data = await decodeOAuthState(state, SECRET);
|
|
98
|
+
* if (data) {
|
|
99
|
+
* console.log(data.uid); // 'user-123'
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
async function decodeOAuthState(state, secret) {
|
|
104
|
+
try {
|
|
105
|
+
const base64 = state.replace(/-/g, "+").replace(/_/g, "/");
|
|
106
|
+
const padded = base64.padEnd(base64.length + (4 - base64.length % 4) % 4, "=");
|
|
107
|
+
const { payload, sig } = JSON.parse(atob(padded));
|
|
108
|
+
const encoder = new TextEncoder();
|
|
109
|
+
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
110
|
+
name: "HMAC",
|
|
111
|
+
hash: "SHA-256"
|
|
112
|
+
}, false, ["sign"]);
|
|
113
|
+
const expectedSignature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
114
|
+
if (sig !== Array.from(new Uint8Array(expectedSignature)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16)) return null;
|
|
115
|
+
const payloadData = JSON.parse(payload);
|
|
116
|
+
delete payloadData._nonce;
|
|
117
|
+
return payloadData;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Exchanges an authorization code for tokens.
|
|
124
|
+
*
|
|
125
|
+
* @param code - Authorization code from OAuth callback
|
|
126
|
+
* @param options - Token exchange configuration
|
|
127
|
+
* @returns Token response from the OAuth provider
|
|
128
|
+
* @throws Error if token exchange fails
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const tokens = await exchangeCode(code, {
|
|
133
|
+
* tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,
|
|
134
|
+
* clientId: env.CLIENT_ID,
|
|
135
|
+
* clientSecret: env.CLIENT_SECRET,
|
|
136
|
+
* redirectUri: 'https://example.com/callback',
|
|
137
|
+
* });
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
async function exchangeCode(code, options) {
|
|
141
|
+
const { tokenUrl, clientId, clientSecret, redirectUri, authMethod = "body" } = options;
|
|
142
|
+
const body = new URLSearchParams({
|
|
143
|
+
grant_type: "authorization_code",
|
|
144
|
+
code,
|
|
145
|
+
redirect_uri: redirectUri
|
|
146
|
+
});
|
|
147
|
+
const headers = {
|
|
148
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
149
|
+
Accept: "application/json"
|
|
150
|
+
};
|
|
151
|
+
if (authMethod === "basic") headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
152
|
+
else {
|
|
153
|
+
body.set("client_id", clientId);
|
|
154
|
+
body.set("client_secret", clientSecret);
|
|
155
|
+
}
|
|
156
|
+
const response = await fetch(tokenUrl, {
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers,
|
|
159
|
+
body: body.toString()
|
|
160
|
+
});
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
const errorText = await response.text();
|
|
163
|
+
throw new Error(`Token exchange failed: ${response.status} ${errorText}`);
|
|
164
|
+
}
|
|
165
|
+
return response.json();
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Refreshes an access token using a refresh token.
|
|
169
|
+
*
|
|
170
|
+
* @param refreshToken - The refresh token
|
|
171
|
+
* @param options - Token exchange configuration (same as exchangeCode)
|
|
172
|
+
* @returns New token response from the OAuth provider
|
|
173
|
+
* @throws Error if token refresh fails
|
|
174
|
+
*
|
|
175
|
+
* @example
|
|
176
|
+
* ```typescript
|
|
177
|
+
* const tokens = await refreshAccessToken(refreshToken, {
|
|
178
|
+
* tokenUrl: OAUTH_PROVIDERS.google.tokenUrl,
|
|
179
|
+
* clientId: env.CLIENT_ID,
|
|
180
|
+
* clientSecret: env.CLIENT_SECRET,
|
|
181
|
+
* redirectUri: 'https://example.com/callback',
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
async function refreshAccessToken(refreshToken, options) {
|
|
186
|
+
const { tokenUrl, clientId, clientSecret, authMethod = "body" } = options;
|
|
187
|
+
const body = new URLSearchParams({
|
|
188
|
+
grant_type: "refresh_token",
|
|
189
|
+
refresh_token: refreshToken
|
|
190
|
+
});
|
|
191
|
+
const headers = {
|
|
192
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
193
|
+
Accept: "application/json"
|
|
194
|
+
};
|
|
195
|
+
if (authMethod === "basic") headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
196
|
+
else {
|
|
197
|
+
body.set("client_id", clientId);
|
|
198
|
+
body.set("client_secret", clientSecret);
|
|
199
|
+
}
|
|
200
|
+
const response = await fetch(tokenUrl, {
|
|
201
|
+
method: "POST",
|
|
202
|
+
headers,
|
|
203
|
+
body: body.toString()
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const errorText = await response.text();
|
|
207
|
+
throw new Error(`Token refresh failed: ${response.status} ${errorText}`);
|
|
208
|
+
}
|
|
209
|
+
return response.json();
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Revokes an OAuth token.
|
|
213
|
+
*
|
|
214
|
+
* @param options - Token revocation configuration
|
|
215
|
+
* @returns True if revocation succeeded
|
|
216
|
+
* @throws Error if revocation fails
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```typescript
|
|
220
|
+
* await revokeToken({
|
|
221
|
+
* revokeUrl: OAUTH_PROVIDERS.google.revokeUrl!,
|
|
222
|
+
* token: refreshToken,
|
|
223
|
+
* tokenTypeHint: 'refresh_token',
|
|
224
|
+
* });
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
async function revokeToken(options) {
|
|
228
|
+
const { revokeUrl, token, tokenTypeHint, clientId, clientSecret, authMethod = "body" } = options;
|
|
229
|
+
const body = new URLSearchParams({ token });
|
|
230
|
+
if (tokenTypeHint) body.set("token_type_hint", tokenTypeHint);
|
|
231
|
+
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
|
|
232
|
+
if (authMethod === "basic" && clientId && clientSecret) headers.Authorization = `Basic ${btoa(`${clientId}:${clientSecret}`)}`;
|
|
233
|
+
else if (clientId && clientSecret) {
|
|
234
|
+
body.set("client_id", clientId);
|
|
235
|
+
body.set("client_secret", clientSecret);
|
|
236
|
+
}
|
|
237
|
+
return (await fetch(revokeUrl, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers,
|
|
240
|
+
body: body.toString()
|
|
241
|
+
})).ok;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Builds an OAuth authorization URL.
|
|
245
|
+
*
|
|
246
|
+
* @param options - Authorization URL configuration
|
|
247
|
+
* @returns The complete authorization URL
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```typescript
|
|
251
|
+
* const state = await encodeOAuthState({ uid: 'user-123' }, SECRET);
|
|
252
|
+
* const authUrl = buildAuthorizationUrl({
|
|
253
|
+
* authorizationUrl: OAUTH_PROVIDERS.google.authorizationUrl,
|
|
254
|
+
* clientId: env.CLIENT_ID,
|
|
255
|
+
* redirectUri: 'https://example.com/callback',
|
|
256
|
+
* scopes: ['openid', 'email', 'profile'],
|
|
257
|
+
* state,
|
|
258
|
+
* extraParams: {
|
|
259
|
+
* access_type: 'offline',
|
|
260
|
+
* prompt: 'consent',
|
|
261
|
+
* },
|
|
262
|
+
* });
|
|
263
|
+
* ```
|
|
264
|
+
*/
|
|
265
|
+
function buildAuthorizationUrl(options) {
|
|
266
|
+
const { authorizationUrl, clientId, redirectUri, scopes, state, responseType = "code", extraParams = {} } = options;
|
|
267
|
+
return `${authorizationUrl}?${new URLSearchParams({
|
|
268
|
+
response_type: responseType,
|
|
269
|
+
client_id: clientId,
|
|
270
|
+
redirect_uri: redirectUri,
|
|
271
|
+
scope: scopes.join(" "),
|
|
272
|
+
state,
|
|
273
|
+
...extraParams
|
|
274
|
+
}).toString()}`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
//#endregion
|
|
278
|
+
export { OAUTH_PROVIDERS, buildAuthorizationUrl, decodeOAuthState, encodeBasicAuth, encodeOAuthState, exchangeCode, extractBasicAuthToken, jwtSign, jwtVerify, parseBasicAuth, refreshAccessToken, revokeToken };
|
|
279
|
+
//# sourceMappingURL=index.mjs.map
|