@draftlab/auth 0.4.1 → 0.6.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 (104) 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} +47 -4
  4. package/dist/{client.js → client.mjs} +81 -10
  5. package/dist/{core.d.ts → core.d.mts} +10 -10
  6. package/dist/{core.js → core.mjs} +104 -56
  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/apple.d.mts +105 -0
  18. package/dist/provider/apple.mjs +151 -0
  19. package/dist/provider/{code.d.ts → code.d.mts} +1 -1
  20. package/dist/provider/{code.js → code.mjs} +2 -3
  21. package/dist/provider/{discord.d.ts → discord.d.mts} +2 -2
  22. package/dist/provider/{discord.js → discord.mjs} +59 -1
  23. package/dist/provider/{facebook.d.ts → facebook.d.mts} +2 -2
  24. package/dist/provider/{facebook.js → facebook.mjs} +57 -1
  25. package/dist/provider/{github.d.ts → github.d.mts} +2 -2
  26. package/dist/provider/{github.js → github.mjs} +79 -1
  27. package/dist/provider/gitlab.d.mts +100 -0
  28. package/dist/provider/gitlab.mjs +128 -0
  29. package/dist/provider/{google.d.ts → google.d.mts} +2 -2
  30. package/dist/provider/{google.js → google.mjs} +45 -1
  31. package/dist/provider/{linkedin.d.ts → linkedin.d.mts} +2 -2
  32. package/dist/provider/{linkedin.js → linkedin.mjs} +57 -1
  33. package/dist/provider/{magiclink.d.ts → magiclink.d.mts} +1 -1
  34. package/dist/provider/{magiclink.js → magiclink.mjs} +4 -6
  35. package/dist/provider/{microsoft.d.ts → microsoft.d.mts} +2 -2
  36. package/dist/provider/{microsoft.js → microsoft.mjs} +68 -1
  37. package/dist/provider/{oauth2.d.ts → oauth2.d.mts} +1 -1
  38. package/dist/provider/{oauth2.js → oauth2.mjs} +4 -4
  39. package/dist/provider/{passkey.d.ts → passkey.d.mts} +1 -1
  40. package/dist/provider/{passkey.js → passkey.mjs} +8 -13
  41. package/dist/provider/{password.d.ts → password.d.mts} +1 -1
  42. package/dist/provider/{password.js → password.mjs} +31 -44
  43. package/dist/provider/{provider.d.ts → provider.d.mts} +1 -1
  44. package/dist/provider/reddit.d.mts +101 -0
  45. package/dist/provider/reddit.mjs +114 -0
  46. package/dist/provider/slack.d.mts +108 -0
  47. package/dist/provider/slack.mjs +125 -0
  48. package/dist/provider/spotify.d.mts +107 -0
  49. package/dist/provider/spotify.mjs +122 -0
  50. package/dist/provider/{totp.d.ts → totp.d.mts} +1 -1
  51. package/dist/provider/{totp.js → totp.mjs} +51 -14
  52. package/dist/provider/twitch.d.mts +102 -0
  53. package/dist/provider/twitch.mjs +118 -0
  54. package/dist/{random.js → random.mjs} +1 -2
  55. package/dist/revocation.d.mts +55 -0
  56. package/dist/revocation.mjs +63 -0
  57. package/dist/storage/{memory.d.ts → memory.d.mts} +1 -1
  58. package/dist/storage/{memory.js → memory.mjs} +3 -5
  59. package/dist/storage/{storage.d.ts → storage.d.mts} +27 -10
  60. package/dist/storage/storage.mjs +104 -0
  61. package/dist/storage/{turso.d.ts → turso.d.mts} +1 -1
  62. package/dist/storage/{turso.js → turso.mjs} +1 -1
  63. package/dist/storage/{unstorage.d.ts → unstorage.d.mts} +1 -1
  64. package/dist/storage/{unstorage.js → unstorage.mjs} +11 -4
  65. package/dist/{subject.d.ts → subject.d.mts} +1 -1
  66. package/dist/ui/{base.d.ts → base.d.mts} +1 -1
  67. package/dist/ui/{base.js → base.mjs} +1 -1
  68. package/dist/ui/{code.d.ts → code.d.mts} +1 -1
  69. package/dist/ui/{code.js → code.mjs} +3 -4
  70. package/dist/ui/{magiclink.d.ts → magiclink.d.mts} +1 -1
  71. package/dist/ui/{magiclink.js → magiclink.mjs} +3 -4
  72. package/dist/ui/{passkey.d.ts → passkey.d.mts} +1 -1
  73. package/dist/ui/{passkey.js → passkey.mjs} +2 -2
  74. package/dist/ui/{password.d.ts → password.d.mts} +1 -1
  75. package/dist/ui/{password.js → password.mjs} +3 -4
  76. package/dist/ui/{select.d.ts → select.d.mts} +1 -1
  77. package/dist/ui/{select.js → select.mjs} +2 -2
  78. package/dist/ui/{totp.d.ts → totp.d.mts} +1 -1
  79. package/dist/ui/{totp.js → totp.mjs} +2 -2
  80. package/dist/{util.js → util.mjs} +2 -5
  81. package/package.json +17 -16
  82. package/dist/index.d.ts +0 -2
  83. package/dist/index.js +0 -3
  84. package/dist/storage/storage.js +0 -62
  85. /package/dist/adapters/{node.d.ts → node.d.mts} +0 -0
  86. /package/dist/{allow.d.ts → allow.d.mts} +0 -0
  87. /package/dist/{error.d.ts → error.d.mts} +0 -0
  88. /package/dist/{error.js → error.mjs} +0 -0
  89. /package/dist/{pkce.d.ts → pkce.d.mts} +0 -0
  90. /package/dist/plugin/{builder.js → builder.mjs} +0 -0
  91. /package/dist/plugin/{plugin.js → plugin.mjs} +0 -0
  92. /package/dist/plugin/{types.js → types.mjs} +0 -0
  93. /package/dist/provider/{provider.js → provider.mjs} +0 -0
  94. /package/dist/{random.d.ts → random.d.mts} +0 -0
  95. /package/dist/{subject.js → subject.mjs} +0 -0
  96. /package/dist/themes/{theme.d.ts → theme.d.mts} +0 -0
  97. /package/dist/themes/{theme.js → theme.mjs} +0 -0
  98. /package/dist/{types.d.ts → types.d.mts} +0 -0
  99. /package/dist/{types.js → types.mjs} +0 -0
  100. /package/dist/ui/{form.d.ts → form.d.mts} +0 -0
  101. /package/dist/ui/{form.js → form.mjs} +0 -0
  102. /package/dist/ui/{icon.d.ts → icon.d.mts} +0 -0
  103. /package/dist/ui/{icon.js → icon.mjs} +0 -0
  104. /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
@@ -252,8 +252,24 @@ interface VerifyResult<T extends SubjectSchema> {
252
252
  } }[keyof T];
253
253
  }
254
254
  /**
255
- * Options for UserInfo requests.
255
+ * Options for token revocation.
256
256
  */
257
+ interface RevokeOptions {
258
+ /**
259
+ * Optional hint about the token type.
260
+ * Can be "access_token" or "refresh_token".
261
+ *
262
+ * Helps the server optimize token lookup.
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * {
267
+ * tokenTypeHint: "refresh_token"
268
+ * }
269
+ * ```
270
+ */
271
+ tokenTypeHint?: "access_token" | "refresh_token";
272
+ }
257
273
  /**
258
274
  * Draft Auth client with OAuth 2.0 operations.
259
275
  */
@@ -392,6 +408,33 @@ interface Client {
392
408
  * ```
393
409
  */
394
410
  verify<T extends SubjectSchema>(subjects: T, token: string, options?: VerifyOptions): Promise<Result<VerifyResult<T>, InvalidRefreshTokenError | InvalidAccessTokenError | InvalidSubjectError>>;
411
+ /**
412
+ * Revoke a token (access or refresh token).
413
+ *
414
+ * Once revoked, the token cannot be used to access resources or refresh.
415
+ * Useful for implementing logout functionality.
416
+ *
417
+ * @param token - The token to revoke
418
+ * @param opts - Additional revocation options
419
+ * @returns Empty result on success
420
+ *
421
+ * @example Logout with refresh token revocation
422
+ * ```ts
423
+ * const result = await client.revoke(refreshToken, {
424
+ * tokenTypeHint: "refresh_token"
425
+ * })
426
+ *
427
+ * if (result.success) {
428
+ * // Token revoked successfully, user is logged out
429
+ * clearStoredTokens()
430
+ * redirectToHome()
431
+ * } else {
432
+ * // Revocation failed, but still clear tokens on client
433
+ * clearStoredTokens()
434
+ * }
435
+ * ```
436
+ */
437
+ revoke(token: string, opts?: RevokeOptions): Promise<Result<void>>;
395
438
  }
396
439
  /**
397
440
  * Create a Draft Auth client.
@@ -409,4 +452,4 @@ interface Client {
409
452
  */
410
453
  declare const createClient: (input: ClientInput) => Client;
411
454
  //#endregion
412
- export { AuthorizeOptions, AuthorizeResult, Challenge, Client, ClientInput, RefreshOptions, Result, Tokens, VerifyOptions, VerifyResult, WellKnown, createClient };
455
+ export { AuthorizeOptions, AuthorizeResult, Challenge, Client, ClientInput, RefreshOptions, Result, RevokeOptions, Tokens, VerifyOptions, VerifyResult, WellKnown, createClient };
@@ -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,
@@ -200,6 +245,32 @@ const createClient = (input) => {
200
245
  error: new InvalidAccessTokenError()
201
246
  };
202
247
  }
248
+ },
249
+ async revoke(token, opts) {
250
+ try {
251
+ const wk = await getIssuer();
252
+ const body = new URLSearchParams({
253
+ token,
254
+ ...opts?.tokenTypeHint ? { token_type_hint: opts.tokenTypeHint } : {}
255
+ });
256
+ if ((await f(wk.token_endpoint.replace("/token", "/revoke"), {
257
+ method: "POST",
258
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
259
+ body: body.toString()
260
+ })).ok) return {
261
+ success: true,
262
+ data: void 0
263
+ };
264
+ return {
265
+ success: false,
266
+ error: /* @__PURE__ */ new Error("Failed to revoke token")
267
+ };
268
+ } catch (error) {
269
+ return {
270
+ success: false,
271
+ error
272
+ };
273
+ }
203
274
  }
204
275
  };
205
276
  return client;
@@ -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,14 @@
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 { Revocation } from "./revocation.mjs";
10
+ import { setTheme } from "./themes/theme.mjs";
11
+ import { Select } from "./ui/select.mjs";
11
12
  import { CompactEncrypt, SignJWT, compactDecrypt } from "jose";
12
13
  import { Router } from "@draftlab/auth-router";
13
14
  import { deleteCookie, getCookie, setCookie } from "@draftlab/auth-router/cookies";
@@ -15,6 +16,33 @@ import { cors } from "@draftlab/auth-router/middleware/cors";
15
16
 
16
17
  //#region src/core.ts
17
18
  /**
19
+ * Core issuer implementation.
20
+ */
21
+ /**
22
+ * Performs an operation with guaranteed minimum execution time.
23
+ * Adds random jitter to prevent timing-based attacks even if operation completes quickly.
24
+ *
25
+ * Used for validating sensitive data where timing differences could leak information
26
+ * (e.g., authorization codes, refresh tokens).
27
+ *
28
+ * @param fn - Async function to execute
29
+ * @param minTimeMs - Minimum execution time in milliseconds (default: 100ms)
30
+ * @returns Result of the function, guaranteed to take at least minTimeMs
31
+ */
32
+ const normalizeTimingAsync = async (fn, minTimeMs = 100) => {
33
+ const startTime = performance.now();
34
+ const result = await fn();
35
+ const elapsed = performance.now() - startTime;
36
+ const remainingTime = Math.max(0, minTimeMs - elapsed);
37
+ if (remainingTime > 0) {
38
+ const jitterBuffer = new Uint32Array(1);
39
+ crypto.getRandomValues(jitterBuffer);
40
+ const totalDelay = remainingTime + (jitterBuffer[0] ?? 0) / 4294967295 * 20;
41
+ await new Promise((resolve) => setTimeout(resolve, totalDelay));
42
+ }
43
+ return result;
44
+ };
45
+ /**
18
46
  * Determines if the incoming request is using HTTPS protocol.
19
47
  * Checks multiple proxy headers to handle load balancers and reverse proxies.
20
48
  *
@@ -60,8 +88,7 @@ const issuer = (input) => {
60
88
  const issuer$1 = (ctx) => {
61
89
  const baseUrl = new URL(getRelativeUrl(ctx, "/"));
62
90
  if (input.basePath) {
63
- const normalizedBasePath = input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`;
64
- baseUrl.pathname = normalizedBasePath.replace(/\/$/, "");
91
+ baseUrl.pathname = (input.basePath.startsWith("/") ? input.basePath : `/${input.basePath}`).replace(/\/$/, "");
65
92
  return baseUrl.href;
66
93
  }
67
94
  return baseUrl.origin;
@@ -90,12 +117,9 @@ const issuer = (input) => {
90
117
  */
91
118
  const resolveSubject = async (type, properties) => {
92
119
  const jsonString = JSON.stringify(properties);
93
- const encoder = new TextEncoder();
94
- const data = encoder.encode(jsonString);
120
+ const data = new TextEncoder().encode(jsonString);
95
121
  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)}`;
122
+ return `${type}:${Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 16)}`;
99
123
  };
100
124
  /**
101
125
  * Generates access and refresh tokens for OAuth 2.0.
@@ -128,23 +152,21 @@ const issuer = (input) => {
128
152
  if (!signingKeyData) throw new Error("Signing key not available");
129
153
  const now = Math.floor(Date.now() / 1e3);
130
154
  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
155
  return {
147
- access,
156
+ access: await new SignJWT({
157
+ type: value.type,
158
+ properties: value.properties,
159
+ sub: value.subject,
160
+ aud: value.clientID,
161
+ iss: issuer$1(ctx),
162
+ exp: now + value.ttl.access,
163
+ iat: now,
164
+ mode: "access"
165
+ }).setExpirationTime(Math.floor(now + value.ttl.access)).setProtectedHeader({
166
+ alg: signingKeyData.alg,
167
+ kid: signingKeyData.id,
168
+ typ: "JWT"
169
+ }).sign(signingKeyData.private),
148
170
  refresh: [value.subject, refreshToken].join(":"),
149
171
  expiresIn: Math.floor(now + value.ttl.access - Date.now() / 1e3)
150
172
  };
@@ -227,8 +249,7 @@ const issuer = (input) => {
227
249
  },
228
250
  async set(ctx, key, maxAge, value) {
229
251
  const isHttps = isHttpsRequest(ctx);
230
- const encryptedValue = await encrypt(value);
231
- setCookie(ctx, key, encryptedValue, {
252
+ setCookie(ctx, key, await encrypt(value), {
232
253
  maxAge,
233
254
  httpOnly: true,
234
255
  secure: isHttps,
@@ -238,13 +259,12 @@ const issuer = (input) => {
238
259
  },
239
260
  async get(ctx, key) {
240
261
  const raw = getCookie(ctx, key);
241
- if (!raw) return void 0;
262
+ if (!raw) return;
242
263
  try {
243
- const decrypted = await decrypt(raw);
244
- return decrypted;
264
+ return await decrypt(raw);
245
265
  } catch {
246
266
  deleteCookie(ctx, key, { path: input.basePath || "/" });
247
- return void 0;
267
+ return;
248
268
  }
249
269
  },
250
270
  async unset(ctx, key) {
@@ -281,8 +301,7 @@ const issuer = (input) => {
281
301
  credentials: false
282
302
  })],
283
303
  handler: async (c) => {
284
- const signingKeys$1 = await allSigning();
285
- const jwksDocument = { keys: signingKeys$1.map((keyInfo) => ({
304
+ const jwksDocument = { keys: (await allSigning()).map((keyInfo) => ({
286
305
  ...keyInfo.jwk,
287
306
  alg: keyInfo.alg,
288
307
  exp: keyInfo.expired ? Math.floor(keyInfo.expired.getTime() / 1e3) : void 0
@@ -326,19 +345,20 @@ const issuer = (input) => {
326
345
  return c.json(error$1.toJSON(), { status: 400 });
327
346
  }
328
347
  const key = ["oauth:code", code.toString()];
329
- const payload = await Storage.get(storage, key);
330
- if (!payload) {
348
+ const { isValid, payload } = await normalizeTimingAsync(async () => {
349
+ const data = await Storage.get(storage, key);
350
+ const redirectUri = form.get("redirect_uri");
351
+ const clientId = form.get("client_id");
352
+ const valid = !!(data && data.redirectURI === redirectUri && data.clientID === clientId);
353
+ return {
354
+ isValid: valid,
355
+ payload: valid ? data : void 0
356
+ };
357
+ });
358
+ if (!isValid || !payload) {
331
359
  const error$1 = new OauthError("invalid_grant", "Authorization code has been used or expired");
332
360
  return c.json(error$1.toJSON(), { status: 400 });
333
361
  }
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
362
  if (payload.pkce) {
343
363
  const codeVerifier = form.get("code_verifier")?.toString();
344
364
  if (!codeVerifier) {
@@ -365,7 +385,12 @@ const issuer = (input) => {
365
385
  const error$1 = new OauthError("invalid_request", "Missing refresh_token");
366
386
  return c.json(error$1.toJSON(), { status: 400 });
367
387
  }
368
- const splits = refreshToken.toString().split(":");
388
+ const refreshTokenStr = refreshToken.toString();
389
+ if (await Revocation.isRevoked(storage, refreshTokenStr)) {
390
+ const error$1 = new OauthError("invalid_grant", "Refresh token has been revoked");
391
+ return c.json(error$1.toJSON(), { status: 400 });
392
+ }
393
+ const splits = refreshTokenStr.split(":");
369
394
  const token = splits.pop();
370
395
  if (!token) throw new Error("Invalid refresh token format");
371
396
  const subject = splits.join(":");
@@ -398,7 +423,6 @@ const issuer = (input) => {
398
423
  payload.properties = refreshResult.properties;
399
424
  if (refreshResult.subject) payload.subject = refreshResult.subject;
400
425
  if (refreshResult.scopes) payload.scopes = refreshResult.scopes;
401
- payload.properties = refreshResult.properties;
402
426
  } catch {
403
427
  return c.json({
404
428
  error: "server_error",
@@ -440,6 +464,31 @@ const issuer = (input) => {
440
464
  }, { status: 400 });
441
465
  }
442
466
  });
467
+ app.post("/revoke", {
468
+ middleware: [cors({
469
+ origin: "*",
470
+ allowHeaders: ["Content-Type"],
471
+ allowMethods: ["POST"],
472
+ credentials: false
473
+ })],
474
+ handler: async (c) => {
475
+ const token = (await c.formData()).get("token")?.toString();
476
+ if (!token) {
477
+ const error$1 = new OauthError("invalid_request", "Missing token parameter");
478
+ return c.json(error$1.toJSON(), { status: 400 });
479
+ }
480
+ try {
481
+ const expiresAt = Date.now() + ttlRefresh * 1e3;
482
+ await Revocation.revoke(storage, token, expiresAt);
483
+ return c.json({});
484
+ } catch (_err) {
485
+ return c.json({
486
+ error: "server_error",
487
+ error_description: "Token revocation failed"
488
+ }, { status: 500 });
489
+ }
490
+ }
491
+ });
443
492
  app.get("/authorize", async (c) => {
444
493
  const provider = c.query("provider");
445
494
  const response_type = c.query("response_type");
@@ -449,14 +498,13 @@ const issuer = (input) => {
449
498
  const audience = c.query("audience");
450
499
  const code_challenge = c.query("code_challenge");
451
500
  const code_challenge_method = c.query("code_challenge_method");
452
- const scope = c.query("scope");
453
501
  const authorization = {
454
502
  response_type,
455
503
  redirect_uri,
456
504
  state,
457
505
  client_id,
458
506
  audience,
459
- scope,
507
+ scope: c.query("scope"),
460
508
  ...code_challenge && code_challenge_method && { pkce: {
461
509
  challenge: code_challenge,
462
510
  method: code_challenge_method
@@ -472,7 +520,7 @@ const issuer = (input) => {
472
520
  redirectURI: redirect_uri,
473
521
  audience
474
522
  }, c.request)) throw new UnauthorizedClientError(client_id, redirect_uri);
475
- await auth.set(c, "authorization", 3600 * 24, authorization);
523
+ await auth.set(c, "authorization", 900, authorization);
476
524
  if (provider) return c.redirect(`${provider}/authorize`);
477
525
  const availableProviders = Object.keys(input.providers);
478
526
  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