@draftlab/auth 0.4.1 → 0.5.0

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 (90) hide show
  1. package/dist/adapters/{node.js → node.mjs} +2 -4
  2. package/dist/{allow.js → allow.mjs} +1 -1
  3. package/dist/{client.d.ts → client.d.mts} +2 -2
  4. package/dist/{client.js → client.mjs} +55 -10
  5. package/dist/{core.d.ts → core.d.mts} +10 -10
  6. package/dist/{core.js → core.mjs} +72 -55
  7. package/dist/index.d.mts +2 -0
  8. package/dist/index.mjs +3 -0
  9. package/dist/{keys.d.ts → keys.d.mts} +1 -1
  10. package/dist/{keys.js → keys.mjs} +6 -8
  11. package/dist/{pkce.js → pkce.mjs} +5 -10
  12. package/dist/plugin/{builder.d.ts → builder.d.mts} +1 -1
  13. package/dist/plugin/{manager.d.ts → manager.d.mts} +2 -2
  14. package/dist/plugin/{manager.js → manager.mjs} +1 -1
  15. package/dist/plugin/{plugin.d.ts → plugin.d.mts} +1 -1
  16. package/dist/plugin/{types.d.ts → types.d.mts} +1 -1
  17. package/dist/provider/{code.d.ts → code.d.mts} +1 -1
  18. package/dist/provider/{code.js → code.mjs} +2 -3
  19. package/dist/provider/{discord.d.ts → discord.d.mts} +2 -2
  20. package/dist/provider/{discord.js → discord.mjs} +59 -1
  21. package/dist/provider/{facebook.d.ts → facebook.d.mts} +2 -2
  22. package/dist/provider/{facebook.js → facebook.mjs} +57 -1
  23. package/dist/provider/{github.d.ts → github.d.mts} +2 -2
  24. package/dist/provider/{github.js → github.mjs} +79 -1
  25. package/dist/provider/{google.d.ts → google.d.mts} +2 -2
  26. package/dist/provider/{google.js → google.mjs} +45 -1
  27. package/dist/provider/{linkedin.d.ts → linkedin.d.mts} +2 -2
  28. package/dist/provider/{linkedin.js → linkedin.mjs} +57 -1
  29. package/dist/provider/{magiclink.d.ts → magiclink.d.mts} +1 -1
  30. package/dist/provider/{magiclink.js → magiclink.mjs} +4 -6
  31. package/dist/provider/{microsoft.d.ts → microsoft.d.mts} +2 -2
  32. package/dist/provider/{microsoft.js → microsoft.mjs} +68 -1
  33. package/dist/provider/{oauth2.d.ts → oauth2.d.mts} +1 -1
  34. package/dist/provider/{oauth2.js → oauth2.mjs} +4 -4
  35. package/dist/provider/{passkey.d.ts → passkey.d.mts} +1 -1
  36. package/dist/provider/{passkey.js → passkey.mjs} +8 -13
  37. package/dist/provider/{password.d.ts → password.d.mts} +1 -1
  38. package/dist/provider/{password.js → password.mjs} +31 -44
  39. package/dist/provider/{provider.d.ts → provider.d.mts} +1 -1
  40. package/dist/provider/{totp.d.ts → totp.d.mts} +1 -1
  41. package/dist/provider/{totp.js → totp.mjs} +51 -14
  42. package/dist/{random.js → random.mjs} +1 -2
  43. package/dist/storage/{memory.d.ts → memory.d.mts} +1 -1
  44. package/dist/storage/{memory.js → memory.mjs} +3 -5
  45. package/dist/storage/{storage.d.ts → storage.d.mts} +27 -10
  46. package/dist/storage/storage.mjs +104 -0
  47. package/dist/storage/{turso.d.ts → turso.d.mts} +1 -1
  48. package/dist/storage/{turso.js → turso.mjs} +1 -1
  49. package/dist/storage/{unstorage.d.ts → unstorage.d.mts} +1 -1
  50. package/dist/storage/{unstorage.js → unstorage.mjs} +11 -4
  51. package/dist/{subject.d.ts → subject.d.mts} +1 -1
  52. package/dist/ui/{base.d.ts → base.d.mts} +1 -1
  53. package/dist/ui/{base.js → base.mjs} +1 -1
  54. package/dist/ui/{code.d.ts → code.d.mts} +1 -1
  55. package/dist/ui/{code.js → code.mjs} +3 -4
  56. package/dist/ui/{magiclink.d.ts → magiclink.d.mts} +1 -1
  57. package/dist/ui/{magiclink.js → magiclink.mjs} +3 -4
  58. package/dist/ui/{passkey.d.ts → passkey.d.mts} +1 -1
  59. package/dist/ui/{passkey.js → passkey.mjs} +2 -2
  60. package/dist/ui/{password.d.ts → password.d.mts} +1 -1
  61. package/dist/ui/{password.js → password.mjs} +3 -4
  62. package/dist/ui/{select.d.ts → select.d.mts} +1 -1
  63. package/dist/ui/{select.js → select.mjs} +2 -2
  64. package/dist/ui/{totp.d.ts → totp.d.mts} +1 -1
  65. package/dist/ui/{totp.js → totp.mjs} +2 -2
  66. package/dist/{util.js → util.mjs} +2 -5
  67. package/package.json +17 -16
  68. package/dist/index.d.ts +0 -2
  69. package/dist/index.js +0 -3
  70. package/dist/storage/storage.js +0 -62
  71. /package/dist/adapters/{node.d.ts → node.d.mts} +0 -0
  72. /package/dist/{allow.d.ts → allow.d.mts} +0 -0
  73. /package/dist/{error.d.ts → error.d.mts} +0 -0
  74. /package/dist/{error.js → error.mjs} +0 -0
  75. /package/dist/{pkce.d.ts → pkce.d.mts} +0 -0
  76. /package/dist/plugin/{builder.js → builder.mjs} +0 -0
  77. /package/dist/plugin/{plugin.js → plugin.mjs} +0 -0
  78. /package/dist/plugin/{types.js → types.mjs} +0 -0
  79. /package/dist/provider/{provider.js → provider.mjs} +0 -0
  80. /package/dist/{random.d.ts → random.d.mts} +0 -0
  81. /package/dist/{subject.js → subject.mjs} +0 -0
  82. /package/dist/themes/{theme.d.ts → theme.d.mts} +0 -0
  83. /package/dist/themes/{theme.js → theme.mjs} +0 -0
  84. /package/dist/{types.d.ts → types.d.mts} +0 -0
  85. /package/dist/{types.js → types.mjs} +0 -0
  86. /package/dist/ui/{form.d.ts → form.d.mts} +0 -0
  87. /package/dist/ui/{form.js → form.mjs} +0 -0
  88. /package/dist/ui/{icon.d.ts → icon.d.mts} +0 -0
  89. /package/dist/ui/{icon.js → icon.mjs} +0 -0
  90. /package/dist/{util.d.ts → util.d.mts} +0 -0
@@ -5,8 +5,7 @@ import { Readable } from "node:stream";
5
5
  * Converts Node.js IncomingMessage to Web Standards Request
6
6
  */
7
7
  const nodeRequestAdapter = (req) => {
8
- const host = req.headers.host || "localhost";
9
- const sanitizedHost = host.split(",")[0]?.trim();
8
+ const sanitizedHost = (req.headers.host || "localhost").split(",")[0]?.trim();
10
9
  const url = new URL(req.url || "/", `http://${sanitizedHost}`);
11
10
  const headers = new Headers();
12
11
  for (const [key, value] of Object.entries(req.headers)) if (value !== void 0) if (Array.isArray(value)) for (const v of value) headers.append(key, v);
@@ -49,8 +48,7 @@ const nodeResponseAdapter = async (response, res) => {
49
48
  const createNodeHandler = (fetchHandler) => {
50
49
  return (req, res) => {
51
50
  try {
52
- const request = nodeRequestAdapter(req);
53
- fetchHandler(request).then((response) => nodeResponseAdapter(response, res)).catch((error) => {
51
+ fetchHandler(nodeRequestAdapter(req)).then((response) => nodeResponseAdapter(response, res)).catch((error) => {
54
52
  console.error("Handler error:", error instanceof Error ? error.message : "Unknown error");
55
53
  if (!res.headersSent) {
56
54
  res.statusCode = 500;
@@ -1,4 +1,4 @@
1
- import { isDomainMatch } from "./util.js";
1
+ import { isDomainMatch } from "./util.mjs";
2
2
 
3
3
  //#region src/allow.ts
4
4
  /**
@@ -1,5 +1,5 @@
1
- import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.js";
2
- import { SubjectSchema } from "./subject.js";
1
+ import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.mjs";
2
+ import { SubjectSchema } from "./subject.mjs";
3
3
  import { StandardSchemaV1 } from "@standard-schema/spec";
4
4
 
5
5
  //#region src/client.d.ts
@@ -1,9 +1,58 @@
1
- import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.js";
2
- import { generatePKCE } from "./pkce.js";
1
+ import { InvalidAccessTokenError, InvalidAuthorizationCodeError, InvalidRefreshTokenError, InvalidSubjectError } from "./error.mjs";
2
+ import { generatePKCE } from "./pkce.mjs";
3
3
  import { createLocalJWKSet, errors, jwtVerify } from "jose";
4
4
 
5
5
  //#region src/client.ts
6
6
  /**
7
+ * Draft Auth client for OAuth 2.0 authentication.
8
+ *
9
+ * ## Quick Start
10
+ *
11
+ * First, create a client.
12
+ *
13
+ * ```ts title="client.ts"
14
+ * import { createClient } from "@draftlab/auth/client"
15
+ *
16
+ * const client = createClient({
17
+ * clientID: "my-client",
18
+ * issuer: "https://auth.myserver.com"
19
+ * })
20
+ * ```
21
+ *
22
+ * Start the OAuth flow by calling `authorize`.
23
+ *
24
+ * ```ts
25
+ * const result = await client.authorize(
26
+ * "https://myapp.com/callback",
27
+ * "code"
28
+ * )
29
+ * if (result.success) {
30
+ * window.location.href = result.data.url
31
+ * }
32
+ * ```
33
+ *
34
+ * When the user completes the flow, exchange the code for tokens.
35
+ *
36
+ * ```ts
37
+ * const result = await client.exchange(code, redirectUri)
38
+ * if (result.success) {
39
+ * const { access, refresh } = result.data
40
+ * // Store tokens securely
41
+ * }
42
+ * ```
43
+ *
44
+ * Verify tokens to get user information.
45
+ *
46
+ * ```ts
47
+ * const result = await client.verify(subjects, accessToken)
48
+ * if (result.success) {
49
+ * // Access user properties: result.data.subject.properties
50
+ * }
51
+ * ```
52
+ *
53
+ * @packageDocumentation
54
+ */
55
+ /**
7
56
  * Create a Draft Auth client.
8
57
  *
9
58
  * @param input - Client configuration
@@ -34,8 +83,7 @@ const createClient = (input) => {
34
83
  const wk = await getIssuer();
35
84
  const cached = jwksCache.get(issuer);
36
85
  if (cached) return cached;
37
- const keyset = await f(wk.jwks_uri).then((r) => r.json());
38
- const result = createLocalJWKSet(keyset);
86
+ const result = createLocalJWKSet(await f(wk.jwks_uri).then((r) => r.json()));
39
87
  jwksCache.set(issuer, result);
40
88
  return result;
41
89
  };
@@ -72,8 +120,7 @@ const createClient = (input) => {
72
120
  },
73
121
  async exchange(code, redirectURI, verifier) {
74
122
  try {
75
- const wk = await getIssuer();
76
- const response = await f(wk.token_endpoint, {
123
+ const response = await f((await getIssuer()).token_endpoint, {
77
124
  method: "POST",
78
125
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
79
126
  body: new URLSearchParams({
@@ -124,8 +171,7 @@ const createClient = (input) => {
124
171
  data: {}
125
172
  };
126
173
  } catch {}
127
- const wk = await getIssuer();
128
- const response = await f(wk.token_endpoint, {
174
+ const response = await f((await getIssuer()).token_endpoint, {
129
175
  method: "POST",
130
176
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
131
177
  body: new URLSearchParams({
@@ -155,8 +201,7 @@ const createClient = (input) => {
155
201
  },
156
202
  async verify(subjects, token, options) {
157
203
  try {
158
- const jwks = await getJWKS();
159
- const jwtResult = await jwtVerify(token, jwks, { issuer });
204
+ const jwtResult = await jwtVerify(token, await getJWKS(), { issuer });
160
205
  const validated = await subjects[jwtResult.payload.type]?.["~standard"].validate(jwtResult.payload.properties);
161
206
  if (!validated?.issues && jwtResult.payload.mode === "access") return {
162
207
  success: true,
@@ -1,11 +1,11 @@
1
- import { AllowCheckInput } from "./allow.js";
2
- import { UnknownStateError } from "./error.js";
3
- import { Prettify } from "./util.js";
4
- import { SubjectPayload, SubjectSchema } from "./subject.js";
5
- import { StorageAdapter } from "./storage/storage.js";
6
- import { Plugin } from "./plugin/types.js";
7
- import { Provider } from "./provider/provider.js";
8
- import { Theme } from "./themes/theme.js";
1
+ import { AllowCheckInput } from "./allow.mjs";
2
+ import { UnknownStateError } from "./error.mjs";
3
+ import { Prettify } from "./util.mjs";
4
+ import { SubjectPayload, SubjectSchema } from "./subject.mjs";
5
+ import { StorageAdapter } from "./storage/storage.mjs";
6
+ import { Plugin } from "./plugin/types.mjs";
7
+ import { Provider } from "./provider/provider.mjs";
8
+ import { Theme } from "./themes/theme.mjs";
9
9
  import { Router } from "@draftlab/auth-router";
10
10
 
11
11
  //#region src/core.d.ts
@@ -13,11 +13,11 @@ import { Router } from "@draftlab/auth-router";
13
13
  /**
14
14
  * Sets the subject payload in the JWT token and returns the response.
15
15
  */
16
- interface OnSuccessResponder<T extends {
16
+ interface OnSuccessResponder<T$1 extends {
17
17
  type: string;
18
18
  properties: unknown;
19
19
  }> {
20
- subject<Type extends T["type"]>(type: Type, properties: Extract<T, {
20
+ subject<Type extends T$1["type"]>(type: Type, properties: Extract<T$1, {
21
21
  type: Type;
22
22
  }>["properties"], opts?: {
23
23
  ttl?: {
@@ -1,13 +1,13 @@
1
- import { getRelativeUrl, lazy } from "./util.js";
2
- import { defaultAllowCheck } from "./allow.js";
3
- import { MissingParameterError, OauthError, UnauthorizedClientError, UnknownStateError } from "./error.js";
4
- import { validatePKCE } from "./pkce.js";
5
- import { generateSecureToken } from "./random.js";
6
- import { Storage } from "./storage/storage.js";
7
- import { encryptionKeys, signingKeys } from "./keys.js";
8
- import { PluginManager } from "./plugin/manager.js";
9
- import { setTheme } from "./themes/theme.js";
10
- import { Select } from "./ui/select.js";
1
+ import { getRelativeUrl, lazy } from "./util.mjs";
2
+ import { defaultAllowCheck } from "./allow.mjs";
3
+ import { MissingParameterError, OauthError, UnauthorizedClientError, UnknownStateError } from "./error.mjs";
4
+ import { validatePKCE } from "./pkce.mjs";
5
+ import { generateSecureToken } from "./random.mjs";
6
+ import { Storage } from "./storage/storage.mjs";
7
+ import { encryptionKeys, signingKeys } from "./keys.mjs";
8
+ import { PluginManager } from "./plugin/manager.mjs";
9
+ import { setTheme } from "./themes/theme.mjs";
10
+ import { Select } from "./ui/select.mjs";
11
11
  import { CompactEncrypt, SignJWT, compactDecrypt } from "jose";
12
12
  import { Router } from "@draftlab/auth-router";
13
13
  import { deleteCookie, getCookie, setCookie } from "@draftlab/auth-router/cookies";
@@ -15,6 +15,33 @@ import { cors } from "@draftlab/auth-router/middleware/cors";
15
15
 
16
16
  //#region src/core.ts
17
17
  /**
18
+ * Core issuer implementation.
19
+ */
20
+ /**
21
+ * Performs an operation with guaranteed minimum execution time.
22
+ * Adds random jitter to prevent timing-based attacks even if operation completes quickly.
23
+ *
24
+ * Used for validating sensitive data where timing differences could leak information
25
+ * (e.g., authorization codes, refresh tokens).
26
+ *
27
+ * @param fn - Async function to execute
28
+ * @param minTimeMs - Minimum execution time in milliseconds (default: 100ms)
29
+ * @returns Result of the function, guaranteed to take at least minTimeMs
30
+ */
31
+ const normalizeTimingAsync = async (fn, minTimeMs = 100) => {
32
+ const startTime = performance.now();
33
+ const result = await fn();
34
+ const elapsed = performance.now() - startTime;
35
+ const remainingTime = Math.max(0, minTimeMs - elapsed);
36
+ if (remainingTime > 0) {
37
+ const jitterBuffer = new Uint32Array(1);
38
+ crypto.getRandomValues(jitterBuffer);
39
+ const totalDelay = remainingTime + (jitterBuffer[0] ?? 0) / 4294967295 * 20;
40
+ await new Promise((resolve) => setTimeout(resolve, totalDelay));
41
+ }
42
+ return result;
43
+ };
44
+ /**
18
45
  * Determines if the incoming request is using HTTPS protocol.
19
46
  * Checks multiple proxy headers to handle load balancers and reverse proxies.
20
47
  *
@@ -60,8 +87,7 @@ const issuer = (input) => {
60
87
  const issuer$1 = (ctx) => {
61
88
  const baseUrl = new URL(getRelativeUrl(ctx, "/"));
62
89
  if (input.basePath) {
63
- const normalizedBasePath = input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`;
64
- baseUrl.pathname = normalizedBasePath.replace(/\/$/, "");
90
+ baseUrl.pathname = (input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`).replace(/\/$/, "");
65
91
  return baseUrl.href;
66
92
  }
67
93
  return baseUrl.origin;
@@ -90,12 +116,9 @@ const issuer = (input) => {
90
116
  */
91
117
  const resolveSubject = async (type, properties) => {
92
118
  const jsonString = JSON.stringify(properties);
93
- const encoder = new TextEncoder();
94
- const data = encoder.encode(jsonString);
119
+ const data = new TextEncoder().encode(jsonString);
95
120
  const hashBuffer = await crypto.subtle.digest("SHA-256", data);
96
- const hashArray = Array.from(new Uint8Array(hashBuffer));
97
- const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
98
- return `${type}:${hashHex.slice(0, 16)}`;
121
+ return `${type}:${Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16)}`;
99
122
  };
100
123
  /**
101
124
  * Generates access and refresh tokens for OAuth 2.0.
@@ -128,23 +151,21 @@ const issuer = (input) => {
128
151
  if (!signingKeyData) throw new Error("Signing key not available");
129
152
  const now = Math.floor(Date.now() / 1e3);
130
153
  if (!value.clientID.trim()) throw new Error("Invalid audience: client ID cannot be empty");
131
- const accessPayload = {
132
- type: value.type,
133
- properties: value.properties,
134
- sub: value.subject,
135
- aud: value.clientID,
136
- iss: issuer$1(ctx),
137
- exp: now + value.ttl.access,
138
- iat: now,
139
- mode: "access"
140
- };
141
- const access = await new SignJWT(accessPayload).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
142
- alg: signingKeyData.alg,
143
- kid: signingKeyData.id,
144
- typ: "JWT"
145
- }).sign(signingKeyData.private);
146
154
  return {
147
- access,
155
+ access: await new SignJWT({
156
+ type: value.type,
157
+ properties: value.properties,
158
+ sub: value.subject,
159
+ aud: value.clientID,
160
+ iss: issuer$1(ctx),
161
+ exp: now + value.ttl.access,
162
+ iat: now,
163
+ mode: "access"
164
+ }).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
165
+ alg: signingKeyData.alg,
166
+ kid: signingKeyData.id,
167
+ typ: "JWT"
168
+ }).sign(signingKeyData.private),
148
169
  refresh: [value.subject, refreshToken].join(":"),
149
170
  expiresIn: Math.floor(now + value.ttl.access - Date.now() / 1e3)
150
171
  };
@@ -227,8 +248,7 @@ const issuer = (input) => {
227
248
  },
228
249
  async set(ctx, key, maxAge, value) {
229
250
  const isHttps = isHttpsRequest(ctx);
230
- const encryptedValue = await encrypt(value);
231
- setCookie(ctx, key, encryptedValue, {
251
+ setCookie(ctx, key, await encrypt(value), {
232
252
  maxAge,
233
253
  httpOnly: true,
234
254
  secure: isHttps,
@@ -238,13 +258,12 @@ const issuer = (input) => {
238
258
  },
239
259
  async get(ctx, key) {
240
260
  const raw = getCookie(ctx, key);
241
- if (!raw) return void 0;
261
+ if (!raw) return;
242
262
  try {
243
- const decrypted = await decrypt(raw);
244
- return decrypted;
263
+ return await decrypt(raw);
245
264
  } catch {
246
265
  deleteCookie(ctx, key, { path: input.basePath || "/" });
247
- return void 0;
266
+ return;
248
267
  }
249
268
  },
250
269
  async unset(ctx, key) {
@@ -281,8 +300,7 @@ const issuer = (input) => {
281
300
  credentials: false
282
301
  })],
283
302
  handler: async (c) => {
284
- const signingKeys$1 = await allSigning();
285
- const jwksDocument = { keys: signingKeys$1.map((keyInfo) => ({
303
+ const jwksDocument = { keys: (await allSigning()).map((keyInfo) => ({
286
304
  ...keyInfo.jwk,
287
305
  alg: keyInfo.alg,
288
306
  exp: keyInfo.expired ? Math.floor(keyInfo.expired.getTime() / 1e3) : void 0
@@ -326,19 +344,20 @@ const issuer = (input) => {
326
344
  return c.json(error$1.toJSON(), { status: 400 });
327
345
  }
328
346
  const key = ["oauth:code", code.toString()];
329
- const payload = await Storage.get(storage, key);
330
- if (!payload) {
347
+ const { isValid, payload } = await normalizeTimingAsync(async () => {
348
+ const data = await Storage.get(storage, key);
349
+ const redirectUri = form.get("redirect_uri");
350
+ const clientId = form.get("client_id");
351
+ const valid = !!(data && data.redirectURI === redirectUri && data.clientID === clientId);
352
+ return {
353
+ isValid: valid,
354
+ payload: valid ? data : void 0
355
+ };
356
+ });
357
+ if (!isValid || !payload) {
331
358
  const error$1 = new OauthError("invalid_grant", "Authorization code has been used or expired");
332
359
  return c.json(error$1.toJSON(), { status: 400 });
333
360
  }
334
- if (payload.redirectURI !== form.get("redirect_uri")) {
335
- const error$1 = new OauthError("invalid_redirect_uri", "Redirect URI mismatch");
336
- return c.json(error$1.toJSON(), { status: 400 });
337
- }
338
- if (payload.clientID !== form.get("client_id")) {
339
- const error$1 = new OauthError("unauthorized_client", "Client is not authorized to use this authorization code");
340
- return c.json(error$1.toJSON(), { status: 400 });
341
- }
342
361
  if (payload.pkce) {
343
362
  const codeVerifier = form.get("code_verifier")?.toString();
344
363
  if (!codeVerifier) {
@@ -398,7 +417,6 @@ const issuer = (input) => {
398
417
  payload.properties = refreshResult.properties;
399
418
  if (refreshResult.subject) payload.subject = refreshResult.subject;
400
419
  if (refreshResult.scopes) payload.scopes = refreshResult.scopes;
401
- payload.properties = refreshResult.properties;
402
420
  } catch {
403
421
  return c.json({
404
422
  error: "server_error",
@@ -449,14 +467,13 @@ const issuer = (input) => {
449
467
  const audience = c.query("audience");
450
468
  const code_challenge = c.query("code_challenge");
451
469
  const code_challenge_method = c.query("code_challenge_method");
452
- const scope = c.query("scope");
453
470
  const authorization = {
454
471
  response_type,
455
472
  redirect_uri,
456
473
  state,
457
474
  client_id,
458
475
  audience,
459
- scope,
476
+ scope: c.query("scope"),
460
477
  ...code_challenge && code_challenge_method && { pkce: {
461
478
  challenge: code_challenge,
462
479
  method: code_challenge_method
@@ -472,7 +489,7 @@ const issuer = (input) => {
472
489
  redirectURI: redirect_uri,
473
490
  audience
474
491
  }, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
475
- await auth.set(c, "authorization", 3600 * 24, authorization);
492
+ await auth.set(c, "authorization", 900, authorization);
476
493
  if (provider) return c.redirect(`${provider}/authorize`);
477
494
  const availableProviders = Object.keys(input.providers);
478
495
  if (availableProviders.length === 1) return c.redirect(`${availableProviders[0]}/authorize`);
@@ -0,0 +1,2 @@
1
+ import { issuer } from "./core.mjs";
2
+ export { issuer };
package/dist/index.mjs ADDED
@@ -0,0 +1,3 @@
1
+ import { issuer } from "./core.mjs";
2
+
3
+ export { issuer };
@@ -1,4 +1,4 @@
1
- import { StorageAdapter } from "./storage/storage.js";
1
+ import { StorageAdapter } from "./storage/storage.mjs";
2
2
  import { CryptoKey, JWK } from "jose";
3
3
 
4
4
  //#region src/keys.d.ts
@@ -1,5 +1,5 @@
1
- import { generateSecureToken } from "./random.js";
2
- import { Storage } from "./storage/storage.js";
1
+ import { generateSecureToken } from "./random.mjs";
2
+ import { Storage } from "./storage/storage.mjs";
3
3
  import { exportJWK, exportPKCS8, exportSPKI, generateKeyPair, importPKCS8, importSPKI } from "jose";
4
4
 
5
5
  //#region src/keys.ts
@@ -63,7 +63,7 @@ const signingKeys = async (storage) => {
63
63
  const jwk = await exportJWK(key.publicKey);
64
64
  jwk.kid = serialized.id;
65
65
  jwk.use = "sig";
66
- const newKeyPair = {
66
+ return [{
67
67
  id: serialized.id,
68
68
  alg: signingAlg,
69
69
  created: new Date(serialized.created),
@@ -71,8 +71,7 @@ const signingKeys = async (storage) => {
71
71
  public: key.publicKey,
72
72
  private: key.privateKey,
73
73
  jwk
74
- };
75
- return [newKeyPair, ...results];
74
+ }, ...results];
76
75
  };
77
76
  /**
78
77
  * Loads or generates encryption keys for token encryption operations.
@@ -124,7 +123,7 @@ const encryptionKeys = async (storage) => {
124
123
  await Storage.set(storage, ["encryption:key", serialized.id], serialized);
125
124
  const jwk = await exportJWK(key.publicKey);
126
125
  jwk.kid = serialized.id;
127
- const newKeyPair = {
126
+ return [{
128
127
  id: serialized.id,
129
128
  alg: encryptionAlg,
130
129
  created: new Date(serialized.created),
@@ -132,8 +131,7 @@ const encryptionKeys = async (storage) => {
132
131
  public: key.publicKey,
133
132
  private: key.privateKey,
134
133
  jwk
135
- };
136
- return [newKeyPair, ...results];
134
+ }, ...results];
137
135
  };
138
136
 
139
137
  //#endregion
@@ -57,8 +57,7 @@ const generateVerifier = (length) => {
57
57
  */
58
58
  const generateChallenge = async (verifier, method) => {
59
59
  if (method === "plain") return verifier;
60
- const encoder = new TextEncoder();
61
- const data = encoder.encode(verifier);
60
+ const data = new TextEncoder().encode(verifier);
62
61
  const hash = await crypto.subtle.digest("SHA-256", data);
63
62
  return base64url.encode(new Uint8Array(hash));
64
63
  };
@@ -89,10 +88,9 @@ const generatePKCE = async (length = 48) => {
89
88
  const verifier = generateVerifier(length);
90
89
  if (verifier.length < 43 || verifier.length > 128) throw new Error("Generated verifier does not meet requirements");
91
90
  if (!/^[A-Za-z0-9_-]+$/.test(verifier)) throw new Error("Generated verifier is not valid base64url format");
92
- const challenge = await generateChallenge(verifier, "S256");
93
91
  return {
94
92
  verifier,
95
- challenge,
93
+ challenge: await generateChallenge(verifier, "S256"),
96
94
  method: "S256"
97
95
  };
98
96
  };
@@ -129,19 +127,16 @@ const validatePKCE = async (verifier, challenge, method = "S256") => {
129
127
  let hasEarlyFailure = false;
130
128
  const normalizedVerifier = String(verifier || "");
131
129
  const normalizedChallenge = String(challenge || "");
132
- const validations = [
130
+ hasEarlyFailure = ![
133
131
  typeof verifier === "string" && typeof challenge === "string" && verifier && challenge,
134
132
  normalizedVerifier.length >= 43 && normalizedVerifier.length <= 128,
135
133
  normalizedChallenge.length >= 43 && normalizedChallenge.length <= 128,
136
134
  /^[A-Za-z0-9_-]+$/.test(normalizedVerifier),
137
135
  /^[A-Za-z0-9_-]+$/.test(normalizedChallenge)
138
- ];
139
- hasEarlyFailure = !validations.every(Boolean);
136
+ ].every(Boolean);
140
137
  const verifierToUse = hasEarlyFailure ? "dummyverifier_".repeat(6) : normalizedVerifier;
141
138
  try {
142
- const generatedChallenge = await generateChallenge(verifierToUse, method);
143
- const challengeToCompare = hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge;
144
- const comparisonResult = timingSafeCompare(generatedChallenge, challengeToCompare);
139
+ const comparisonResult = timingSafeCompare(await generateChallenge(verifierToUse, method), hasEarlyFailure ? "dummychallenge_".repeat(6) : normalizedChallenge);
145
140
  isValid = !hasEarlyFailure && comparisonResult;
146
141
  } catch {
147
142
  isValid = false;
@@ -1,4 +1,4 @@
1
- import { PluginBuilder } from "./plugin.js";
1
+ import { PluginBuilder } from "./plugin.mjs";
2
2
 
3
3
  //#region src/plugin/builder.d.ts
4
4
 
@@ -1,5 +1,5 @@
1
- import { StorageAdapter } from "../storage/storage.js";
2
- import { Plugin } from "./types.js";
1
+ import { StorageAdapter } from "../storage/storage.mjs";
2
+ import { Plugin } from "./types.mjs";
3
3
  import { Router } from "@draftlab/auth-router";
4
4
 
5
5
  //#region src/plugin/manager.d.ts
@@ -1,4 +1,4 @@
1
- import { PluginError } from "./types.js";
1
+ import { PluginError } from "./types.mjs";
2
2
 
3
3
  //#region src/plugin/manager.ts
4
4
  var PluginManager = class {
@@ -1,4 +1,4 @@
1
- import { Plugin, PluginRouteHandler } from "./types.js";
1
+ import { Plugin, PluginRouteHandler } from "./types.mjs";
2
2
 
3
3
  //#region src/plugin/plugin.d.ts
4
4
 
@@ -1,4 +1,4 @@
1
- import { StorageAdapter } from "../storage/storage.js";
1
+ import { StorageAdapter } from "../storage/storage.mjs";
2
2
  import { RouterContext } from "@draftlab/auth-router/types";
3
3
 
4
4
  //#region src/plugin/types.d.ts
@@ -1,4 +1,4 @@
1
- import { Provider } from "./provider.js";
1
+ import { Provider } from "./provider.mjs";
2
2
 
3
3
  //#region src/provider/code.d.ts
4
4
 
@@ -1,4 +1,4 @@
1
- import { generateUnbiasedDigits, timingSafeCompare } from "../random.js";
1
+ import { generateUnbiasedDigits, timingSafeCompare } from "../random.mjs";
2
2
 
3
3
  //#region src/provider/code.ts
4
4
  /**
@@ -130,8 +130,7 @@ const CodeProvider = (config) => {
130
130
  const action = formData.get("action")?.toString();
131
131
  if (action === "request" || action === "resend") {
132
132
  const code = generateCode();
133
- const formEntries = Object.fromEntries(formData);
134
- const { action: _,...claims } = formEntries;
133
+ const { action: _, ...claims } = Object.fromEntries(formData);
135
134
  const sendError = await config.sendCode(claims, code);
136
135
  if (sendError) return transition(c, { type: "start" }, formData, sendError);
137
136
  return transition(c, {
@@ -1,5 +1,5 @@
1
- import { Provider } from "./provider.js";
2
- import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.js";
1
+ import { Provider } from "./provider.mjs";
2
+ import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
3
3
 
4
4
  //#region src/provider/discord.d.ts
5
5
 
@@ -1,7 +1,65 @@
1
- import { Oauth2Provider } from "./oauth2.js";
1
+ import { Oauth2Provider } from "./oauth2.mjs";
2
2
 
3
3
  //#region src/provider/discord.ts
4
4
  /**
5
+ * Discord OAuth 2.0 authentication provider for Draft Auth.
6
+ * Provides access tokens for calling Discord APIs on behalf of users.
7
+ *
8
+ * ## Quick Setup
9
+ *
10
+ * ```ts
11
+ * import { DiscordProvider } from "@draftlab/auth/provider/discord"
12
+ *
13
+ * export default issuer({
14
+ * providers: {
15
+ * discord: DiscordProvider({
16
+ * clientID: process.env.DISCORD_CLIENT_ID,
17
+ * clientSecret: process.env.DISCORD_CLIENT_SECRET,
18
+ * scopes: ["identify", "email", "guilds"]
19
+ * })
20
+ * }
21
+ * })
22
+ * ```
23
+ *
24
+ * ## Common Scopes
25
+ *
26
+ * - `identify` - Access to user's basic account information
27
+ * - `email` - Access to user's email address
28
+ * - `guilds` - Access to user's guilds (servers)
29
+ * - `guilds.join` - Ability to join user to guilds
30
+ * - `gdm.join` - Ability to join user to group DMs
31
+ * - `connections` - Access to user's connections (Steam, YouTube, etc.)
32
+ * - `guilds.members.read` - Read guild member information
33
+ * - `bot` - For bot applications (requires additional setup)
34
+ *
35
+ * ## User Data Access
36
+ *
37
+ * ```ts
38
+ * success: async (ctx, value) => {
39
+ * if (value.provider === "discord") {
40
+ * const accessToken = value.tokenset.access
41
+ *
42
+ * // Fetch user information
43
+ * const userResponse = await fetch('https://discord.com/api/users/@me', {
44
+ * headers: { Authorization: `Bearer ${accessToken}` }
45
+ * })
46
+ * const user = await userResponse.json()
47
+ *
48
+ * // Fetch user guilds (requires guilds scope)
49
+ * const guildsResponse = await fetch('https://discord.com/api/users/@me/guilds', {
50
+ * headers: { Authorization: `Bearer ${accessToken}` }
51
+ * })
52
+ * const guilds = await guildsResponse.json()
53
+ *
54
+ * // User info: user.username + user.discriminator
55
+ * // Avatar: `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`
56
+ * }
57
+ * }
58
+ * ```
59
+ *
60
+ * @packageDocumentation
61
+ */
62
+ /**
5
63
  * Creates a Discord OAuth 2.0 authentication provider.
6
64
  * Use this when you need access tokens to call Discord APIs on behalf of the user.
7
65
  *
@@ -1,5 +1,5 @@
1
- import { Provider } from "./provider.js";
2
- import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.js";
1
+ import { Provider } from "./provider.mjs";
2
+ import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
3
3
 
4
4
  //#region src/provider/facebook.d.ts
5
5