@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.
- package/dist/adapters/{node.js → node.mjs} +2 -4
- package/dist/{allow.js → allow.mjs} +1 -1
- package/dist/{client.d.ts → client.d.mts} +47 -4
- package/dist/{client.js → client.mjs} +81 -10
- package/dist/{core.d.ts → core.d.mts} +10 -10
- package/dist/{core.js → core.mjs} +104 -56
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +3 -0
- package/dist/{keys.d.ts → keys.d.mts} +1 -1
- package/dist/{keys.js → keys.mjs} +6 -8
- package/dist/{pkce.js → pkce.mjs} +5 -10
- package/dist/plugin/{builder.d.ts → builder.d.mts} +1 -1
- package/dist/plugin/{manager.d.ts → manager.d.mts} +2 -2
- package/dist/plugin/{manager.js → manager.mjs} +1 -1
- package/dist/plugin/{plugin.d.ts → plugin.d.mts} +1 -1
- package/dist/plugin/{types.d.ts → types.d.mts} +1 -1
- package/dist/provider/apple.d.mts +105 -0
- package/dist/provider/apple.mjs +151 -0
- package/dist/provider/{code.d.ts → code.d.mts} +1 -1
- package/dist/provider/{code.js → code.mjs} +2 -3
- package/dist/provider/{discord.d.ts → discord.d.mts} +2 -2
- package/dist/provider/{discord.js → discord.mjs} +59 -1
- package/dist/provider/{facebook.d.ts → facebook.d.mts} +2 -2
- package/dist/provider/{facebook.js → facebook.mjs} +57 -1
- package/dist/provider/{github.d.ts → github.d.mts} +2 -2
- package/dist/provider/{github.js → github.mjs} +79 -1
- package/dist/provider/gitlab.d.mts +100 -0
- package/dist/provider/gitlab.mjs +128 -0
- package/dist/provider/{google.d.ts → google.d.mts} +2 -2
- package/dist/provider/{google.js → google.mjs} +45 -1
- package/dist/provider/{linkedin.d.ts → linkedin.d.mts} +2 -2
- package/dist/provider/{linkedin.js → linkedin.mjs} +57 -1
- package/dist/provider/{magiclink.d.ts → magiclink.d.mts} +1 -1
- package/dist/provider/{magiclink.js → magiclink.mjs} +4 -6
- package/dist/provider/{microsoft.d.ts → microsoft.d.mts} +2 -2
- package/dist/provider/{microsoft.js → microsoft.mjs} +68 -1
- package/dist/provider/{oauth2.d.ts → oauth2.d.mts} +1 -1
- package/dist/provider/{oauth2.js → oauth2.mjs} +4 -4
- package/dist/provider/{passkey.d.ts → passkey.d.mts} +1 -1
- package/dist/provider/{passkey.js → passkey.mjs} +8 -13
- package/dist/provider/{password.d.ts → password.d.mts} +1 -1
- package/dist/provider/{password.js → password.mjs} +31 -44
- package/dist/provider/{provider.d.ts → provider.d.mts} +1 -1
- package/dist/provider/reddit.d.mts +101 -0
- package/dist/provider/reddit.mjs +114 -0
- package/dist/provider/slack.d.mts +108 -0
- package/dist/provider/slack.mjs +125 -0
- package/dist/provider/spotify.d.mts +107 -0
- package/dist/provider/spotify.mjs +122 -0
- package/dist/provider/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/provider/{totp.js → totp.mjs} +51 -14
- package/dist/provider/twitch.d.mts +102 -0
- package/dist/provider/twitch.mjs +118 -0
- package/dist/{random.js → random.mjs} +1 -2
- package/dist/revocation.d.mts +55 -0
- package/dist/revocation.mjs +63 -0
- package/dist/storage/{memory.d.ts → memory.d.mts} +1 -1
- package/dist/storage/{memory.js → memory.mjs} +3 -5
- package/dist/storage/{storage.d.ts → storage.d.mts} +27 -10
- package/dist/storage/storage.mjs +104 -0
- package/dist/storage/{turso.d.ts → turso.d.mts} +1 -1
- package/dist/storage/{turso.js → turso.mjs} +1 -1
- package/dist/storage/{unstorage.d.ts → unstorage.d.mts} +1 -1
- package/dist/storage/{unstorage.js → unstorage.mjs} +11 -4
- package/dist/{subject.d.ts → subject.d.mts} +1 -1
- package/dist/ui/{base.d.ts → base.d.mts} +1 -1
- package/dist/ui/{base.js → base.mjs} +1 -1
- package/dist/ui/{code.d.ts → code.d.mts} +1 -1
- package/dist/ui/{code.js → code.mjs} +3 -4
- package/dist/ui/{magiclink.d.ts → magiclink.d.mts} +1 -1
- package/dist/ui/{magiclink.js → magiclink.mjs} +3 -4
- package/dist/ui/{passkey.d.ts → passkey.d.mts} +1 -1
- package/dist/ui/{passkey.js → passkey.mjs} +2 -2
- package/dist/ui/{password.d.ts → password.d.mts} +1 -1
- package/dist/ui/{password.js → password.mjs} +3 -4
- package/dist/ui/{select.d.ts → select.d.mts} +1 -1
- package/dist/ui/{select.js → select.mjs} +2 -2
- package/dist/ui/{totp.d.ts → totp.d.mts} +1 -1
- package/dist/ui/{totp.js → totp.mjs} +2 -2
- package/dist/{util.js → util.mjs} +2 -5
- package/package.json +17 -16
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3
- package/dist/storage/storage.js +0 -62
- /package/dist/adapters/{node.d.ts → node.d.mts} +0 -0
- /package/dist/{allow.d.ts → allow.d.mts} +0 -0
- /package/dist/{error.d.ts → error.d.mts} +0 -0
- /package/dist/{error.js → error.mjs} +0 -0
- /package/dist/{pkce.d.ts → pkce.d.mts} +0 -0
- /package/dist/plugin/{builder.js → builder.mjs} +0 -0
- /package/dist/plugin/{plugin.js → plugin.mjs} +0 -0
- /package/dist/plugin/{types.js → types.mjs} +0 -0
- /package/dist/provider/{provider.js → provider.mjs} +0 -0
- /package/dist/{random.d.ts → random.d.mts} +0 -0
- /package/dist/{subject.js → subject.mjs} +0 -0
- /package/dist/themes/{theme.d.ts → theme.d.mts} +0 -0
- /package/dist/themes/{theme.js → theme.mjs} +0 -0
- /package/dist/{types.d.ts → types.d.mts} +0 -0
- /package/dist/{types.js → types.mjs} +0 -0
- /package/dist/ui/{form.d.ts → form.d.mts} +0 -0
- /package/dist/ui/{form.js → form.mjs} +0 -0
- /package/dist/ui/{icon.d.ts → icon.d.mts} +0 -0
- /package/dist/ui/{icon.js → icon.mjs} +0 -0
- /package/dist/{util.d.ts → util.d.mts} +0 -0
|
@@ -1,8 +1,50 @@
|
|
|
1
|
-
import { generateSecureToken } from "../random.
|
|
2
|
-
import { Storage } from "../storage/storage.
|
|
1
|
+
import { generateSecureToken } from "../random.mjs";
|
|
2
|
+
import { Storage } from "../storage/storage.mjs";
|
|
3
3
|
import { Secret, TOTP } from "otpauth";
|
|
4
4
|
|
|
5
5
|
//#region src/provider/totp.ts
|
|
6
|
+
/**
|
|
7
|
+
* Configures a provider that supports TOTP (Time-based One-Time Password) authentication.
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { TOTPProvider } from "@draftlab/auth/provider/totp"
|
|
11
|
+
*
|
|
12
|
+
* export default issuer({
|
|
13
|
+
* providers: {
|
|
14
|
+
* totp: TOTPProvider({
|
|
15
|
+
* issuer: "My Application",
|
|
16
|
+
* setup: async (req, qrCode, secret, backupCodes) => {
|
|
17
|
+
* return new Response(renderSetupPage(qrCode, secret, backupCodes))
|
|
18
|
+
* },
|
|
19
|
+
* verify: async (req, error) => {
|
|
20
|
+
* return new Response(renderVerifyPage(error))
|
|
21
|
+
* },
|
|
22
|
+
* recovery: async (req, error) => {
|
|
23
|
+
* return new Response(renderRecoveryPage(error))
|
|
24
|
+
* }
|
|
25
|
+
* })
|
|
26
|
+
* },
|
|
27
|
+
* // ...
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* TOTPProvider implements Time-based One-Time Password authentication.
|
|
32
|
+
* It provides secure TOTP token generation and verification with backup recovery codes.
|
|
33
|
+
*
|
|
34
|
+
* The provider requires configuration of:
|
|
35
|
+
* - Issuer name for authenticator apps
|
|
36
|
+
* - UI handlers for setup, verification, and recovery flows
|
|
37
|
+
* - Optional TOTP parameters (algorithm, digits, period)
|
|
38
|
+
*
|
|
39
|
+
* It automatically manages:
|
|
40
|
+
* - Secure secret generation
|
|
41
|
+
* - QR code URL generation for authenticator apps
|
|
42
|
+
* - Token validation with timing attack protection
|
|
43
|
+
* - Recovery codes generation and one-time usage
|
|
44
|
+
* - Storage of TOTP configuration and backup codes
|
|
45
|
+
*
|
|
46
|
+
* @packageDocumentation
|
|
47
|
+
*/
|
|
6
48
|
const totpKey = (userId) => [
|
|
7
49
|
"totp",
|
|
8
50
|
"user",
|
|
@@ -72,16 +114,14 @@ const TOTPProvider = (config) => {
|
|
|
72
114
|
const secret = new Secret({ size: 20 });
|
|
73
115
|
const label = config.generateLabel ? await config.generateLabel(email) : email;
|
|
74
116
|
const backupCodes = generateBackupCodes(backupCodesCount);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const totpData$1 = {
|
|
117
|
+
const qrCodeUrl$1 = createTOTPInstance(secret.base32, label).toString();
|
|
118
|
+
await saveTOTPData(email, {
|
|
78
119
|
secret: secret.base32,
|
|
79
120
|
enabled: false,
|
|
80
121
|
backupCodes,
|
|
81
122
|
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
82
123
|
label
|
|
83
|
-
};
|
|
84
|
-
await saveTOTPData(email, totpData$1);
|
|
124
|
+
});
|
|
85
125
|
return ctx.forward(c, await config.register(c.request, qrCodeUrl$1, secret.base32, backupCodes, void 0, email));
|
|
86
126
|
}
|
|
87
127
|
const token = formData.get("token")?.toString();
|
|
@@ -89,11 +129,10 @@ const TOTPProvider = (config) => {
|
|
|
89
129
|
const totpData = await getTOTPData(email);
|
|
90
130
|
if (!totpData) return ctx.forward(c, await config.register(c.request, "", "", [], "TOTP setup session not found"));
|
|
91
131
|
const totp = createTOTPInstance(totpData.secret, totpData.label || email);
|
|
92
|
-
|
|
132
|
+
if (totp.validate({
|
|
93
133
|
token,
|
|
94
134
|
window
|
|
95
|
-
})
|
|
96
|
-
if (delta !== null) {
|
|
135
|
+
}) !== null) {
|
|
97
136
|
totpData.enabled = true;
|
|
98
137
|
await saveTOTPData(email, totpData);
|
|
99
138
|
return ctx.success(c, {
|
|
@@ -114,12 +153,10 @@ const TOTPProvider = (config) => {
|
|
|
114
153
|
if (!email || !token) return ctx.forward(c, await config.authorize(c.request, "Email and verification code are required"));
|
|
115
154
|
const totpData = await getTOTPData(email);
|
|
116
155
|
if (!totpData || !totpData.enabled) return ctx.forward(c, await config.authorize(c.request, "TOTP is not set up for this email"));
|
|
117
|
-
|
|
118
|
-
const delta = totp.validate({
|
|
156
|
+
if (createTOTPInstance(totpData.secret, totpData.label || email).validate({
|
|
119
157
|
token,
|
|
120
158
|
window
|
|
121
|
-
})
|
|
122
|
-
if (delta !== null) return ctx.success(c, {
|
|
159
|
+
}) !== null) return ctx.success(c, {
|
|
123
160
|
email,
|
|
124
161
|
method: "totp"
|
|
125
162
|
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Provider } from "./provider.mjs";
|
|
2
|
+
import { Oauth2UserData, Oauth2WrappedConfig } from "./oauth2.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/provider/twitch.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configuration options for Twitch OAuth 2.0 provider.
|
|
8
|
+
* Extends the base OAuth 2.0 configuration with Twitch-specific documentation.
|
|
9
|
+
*/
|
|
10
|
+
interface TwitchConfig extends Oauth2WrappedConfig {
|
|
11
|
+
/**
|
|
12
|
+
* Twitch application client ID.
|
|
13
|
+
* Get this from your Twitch Console at https://dev.twitch.tv/console
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* {
|
|
18
|
+
* clientID: "abcdef123456"
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
readonly clientID: string;
|
|
23
|
+
/**
|
|
24
|
+
* Twitch application client secret.
|
|
25
|
+
* Keep this secure and never expose it to client-side code.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* {
|
|
30
|
+
* clientSecret: process.env.TWITCH_CLIENT_SECRET
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
readonly clientSecret: string;
|
|
35
|
+
/**
|
|
36
|
+
* Twitch OAuth scopes to request access for.
|
|
37
|
+
* Determines what data and actions your app can access.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* {
|
|
42
|
+
* scopes: [
|
|
43
|
+
* "user:read:email", // Access user email
|
|
44
|
+
* "user:read:subscriptions" // View subscriptions
|
|
45
|
+
* ]
|
|
46
|
+
* }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
readonly scopes: string[];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Creates a Twitch OAuth 2.0 authentication provider.
|
|
53
|
+
* Allows users to authenticate using their Twitch accounts.
|
|
54
|
+
*
|
|
55
|
+
* @param config - Twitch OAuth 2.0 configuration
|
|
56
|
+
* @returns OAuth 2.0 provider configured for Twitch
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```ts
|
|
60
|
+
* // Basic Twitch authentication
|
|
61
|
+
* const basicTwitch = TwitchProvider({
|
|
62
|
+
* clientID: process.env.TWITCH_CLIENT_ID,
|
|
63
|
+
* clientSecret: process.env.TWITCH_CLIENT_SECRET
|
|
64
|
+
* })
|
|
65
|
+
*
|
|
66
|
+
* // Twitch with email scope
|
|
67
|
+
* const twitchWithEmail = TwitchProvider({
|
|
68
|
+
* clientID: process.env.TWITCH_CLIENT_ID,
|
|
69
|
+
* clientSecret: process.env.TWITCH_CLIENT_SECRET,
|
|
70
|
+
* scopes: ["user:read:email"]
|
|
71
|
+
* })
|
|
72
|
+
*
|
|
73
|
+
* // Using the access token to fetch user data
|
|
74
|
+
* export default issuer({
|
|
75
|
+
* providers: { twitch: twitchWithEmail },
|
|
76
|
+
* success: async (ctx, value) => {
|
|
77
|
+
* if (value.provider === "twitch") {
|
|
78
|
+
* const token = value.tokenset.access
|
|
79
|
+
*
|
|
80
|
+
* const userRes = await fetch('https://api.twitch.tv/helix/users', {
|
|
81
|
+
* headers: {
|
|
82
|
+
* 'Authorization': `Bearer ${token}`,
|
|
83
|
+
* 'Client-ID': process.env.TWITCH_CLIENT_ID
|
|
84
|
+
* }
|
|
85
|
+
* })
|
|
86
|
+
* const { data } = await userRes.json()
|
|
87
|
+
* const user = data[0]
|
|
88
|
+
*
|
|
89
|
+
* return ctx.subject("user", {
|
|
90
|
+
* twitchId: user.id,
|
|
91
|
+
* login: user.login,
|
|
92
|
+
* email: user.email,
|
|
93
|
+
* displayName: user.display_name
|
|
94
|
+
* })
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* })
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
declare const TwitchProvider: (config: TwitchConfig) => Provider<Oauth2UserData>;
|
|
101
|
+
//#endregion
|
|
102
|
+
export { TwitchConfig, TwitchProvider };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Oauth2Provider } from "./oauth2.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/provider/twitch.ts
|
|
4
|
+
/**
|
|
5
|
+
* Twitch authentication provider for Draft Auth.
|
|
6
|
+
* Implements OAuth 2.0 flow for authenticating users with their Twitch accounts.
|
|
7
|
+
*
|
|
8
|
+
* ## Quick Setup
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { TwitchProvider } from "@draftlab/auth/provider/twitch"
|
|
12
|
+
*
|
|
13
|
+
* export default issuer({
|
|
14
|
+
* providers: {
|
|
15
|
+
* twitch: TwitchProvider({
|
|
16
|
+
* clientID: process.env.TWITCH_CLIENT_ID,
|
|
17
|
+
* clientSecret: process.env.TWITCH_CLIENT_SECRET,
|
|
18
|
+
* scopes: ["user:read:email"]
|
|
19
|
+
* })
|
|
20
|
+
* }
|
|
21
|
+
* })
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* ## Common Scopes
|
|
25
|
+
*
|
|
26
|
+
* - `user:read:email` - Access user's email address
|
|
27
|
+
* - `user:read:subscriptions` - View user subscriptions
|
|
28
|
+
* - `user:read:follows` - View user's follows
|
|
29
|
+
* - `channel:read:subscriptions` - View channel subscribers
|
|
30
|
+
* - `analytics:read:games` - View game analytics
|
|
31
|
+
* - `bits:read` - View bits information
|
|
32
|
+
*
|
|
33
|
+
* ## User Data Access
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* success: async (ctx, value) => {
|
|
37
|
+
* if (value.provider === "twitch") {
|
|
38
|
+
* const accessToken = value.tokenset.access
|
|
39
|
+
*
|
|
40
|
+
* // Fetch user information
|
|
41
|
+
* const userResponse = await fetch('https://api.twitch.tv/helix/users', {
|
|
42
|
+
* headers: {
|
|
43
|
+
* 'Authorization': `Bearer ${accessToken}`,
|
|
44
|
+
* 'Client-ID': process.env.TWITCH_CLIENT_ID
|
|
45
|
+
* }
|
|
46
|
+
* })
|
|
47
|
+
* const { data } = await userResponse.json()
|
|
48
|
+
* const user = data[0]
|
|
49
|
+
*
|
|
50
|
+
* // User info available: id, login, display_name, email, profile_image_url
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* @packageDocumentation
|
|
56
|
+
*/
|
|
57
|
+
/**
|
|
58
|
+
* Creates a Twitch OAuth 2.0 authentication provider.
|
|
59
|
+
* Allows users to authenticate using their Twitch accounts.
|
|
60
|
+
*
|
|
61
|
+
* @param config - Twitch OAuth 2.0 configuration
|
|
62
|
+
* @returns OAuth 2.0 provider configured for Twitch
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* ```ts
|
|
66
|
+
* // Basic Twitch authentication
|
|
67
|
+
* const basicTwitch = TwitchProvider({
|
|
68
|
+
* clientID: process.env.TWITCH_CLIENT_ID,
|
|
69
|
+
* clientSecret: process.env.TWITCH_CLIENT_SECRET
|
|
70
|
+
* })
|
|
71
|
+
*
|
|
72
|
+
* // Twitch with email scope
|
|
73
|
+
* const twitchWithEmail = TwitchProvider({
|
|
74
|
+
* clientID: process.env.TWITCH_CLIENT_ID,
|
|
75
|
+
* clientSecret: process.env.TWITCH_CLIENT_SECRET,
|
|
76
|
+
* scopes: ["user:read:email"]
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* // Using the access token to fetch user data
|
|
80
|
+
* export default issuer({
|
|
81
|
+
* providers: { twitch: twitchWithEmail },
|
|
82
|
+
* success: async (ctx, value) => {
|
|
83
|
+
* if (value.provider === "twitch") {
|
|
84
|
+
* const token = value.tokenset.access
|
|
85
|
+
*
|
|
86
|
+
* const userRes = await fetch('https://api.twitch.tv/helix/users', {
|
|
87
|
+
* headers: {
|
|
88
|
+
* 'Authorization': `Bearer ${token}`,
|
|
89
|
+
* 'Client-ID': process.env.TWITCH_CLIENT_ID
|
|
90
|
+
* }
|
|
91
|
+
* })
|
|
92
|
+
* const { data } = await userRes.json()
|
|
93
|
+
* const user = data[0]
|
|
94
|
+
*
|
|
95
|
+
* return ctx.subject("user", {
|
|
96
|
+
* twitchId: user.id,
|
|
97
|
+
* login: user.login,
|
|
98
|
+
* email: user.email,
|
|
99
|
+
* displayName: user.display_name
|
|
100
|
+
* })
|
|
101
|
+
* }
|
|
102
|
+
* }
|
|
103
|
+
* })
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
const TwitchProvider = (config) => {
|
|
107
|
+
return Oauth2Provider({
|
|
108
|
+
...config,
|
|
109
|
+
type: "twitch",
|
|
110
|
+
endpoint: {
|
|
111
|
+
authorization: "https://id.twitch.tv/oauth2/authorize",
|
|
112
|
+
token: "https://id.twitch.tv/oauth2/token"
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { TwitchProvider };
|
|
@@ -25,8 +25,7 @@ const generateSecureToken = (length = 32) => {
|
|
|
25
25
|
if (length <= 0 || !Number.isInteger(length)) throw new RangeError("Token length must be a positive integer");
|
|
26
26
|
const randomBytes$1 = new Uint8Array(length);
|
|
27
27
|
crypto.getRandomValues(randomBytes$1);
|
|
28
|
-
|
|
29
|
-
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
28
|
+
return btoa(String.fromCharCode.apply(null, Array.from(randomBytes$1))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
30
29
|
};
|
|
31
30
|
/**
|
|
32
31
|
* Generates a cryptographically secure string of random digits without modulo bias.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { StorageAdapter } from "./storage/storage.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/revocation.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Data stored for a revoked token.
|
|
7
|
+
* Tracks when the token was revoked and when it naturally expires.
|
|
8
|
+
*/
|
|
9
|
+
interface RevocationRecord {
|
|
10
|
+
/** Timestamp when the token was revoked (milliseconds) */
|
|
11
|
+
revokedAt: number;
|
|
12
|
+
/** Timestamp when the token naturally expires (milliseconds) */
|
|
13
|
+
expiresAt: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Token revocation manager.
|
|
17
|
+
* Provides methods to revoke tokens and check if a token has been revoked.
|
|
18
|
+
*/
|
|
19
|
+
declare const Revocation: {
|
|
20
|
+
/**
|
|
21
|
+
* Revokes a token, preventing it from being used even if not yet expired.
|
|
22
|
+
*
|
|
23
|
+
* @param storage - Storage adapter to use
|
|
24
|
+
* @param token - The token to revoke (access or refresh token)
|
|
25
|
+
* @param expiresAt - When the token naturally expires (milliseconds since epoch)
|
|
26
|
+
* @returns Promise that resolves when revocation is stored
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* // Revoke a refresh token on logout
|
|
31
|
+
* await Revocation.revoke(storage, refreshToken, expiresAt)
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
readonly revoke: (storage: StorageAdapter, token: string, expiresAt: number) => Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Checks if a token has been revoked.
|
|
37
|
+
* Returns false if token is not in revocation list (never revoked or already expired).
|
|
38
|
+
*
|
|
39
|
+
* @param storage - Storage adapter to use
|
|
40
|
+
* @param token - The token to check
|
|
41
|
+
* @returns Promise resolving to true if token is revoked, false otherwise
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* // Check if token was revoked before using it
|
|
46
|
+
* const isRevoked = await Revocation.isRevoked(storage, accessToken)
|
|
47
|
+
* if (isRevoked) {
|
|
48
|
+
* throw new InvalidAccessTokenError()
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
readonly isRevoked: (storage: StorageAdapter, token: string) => Promise<boolean>;
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { Revocation, RevocationRecord };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Storage } from "./storage/storage.mjs";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
//#region src/revocation.ts
|
|
5
|
+
/**
|
|
6
|
+
* Token revocation management for Draft Auth.
|
|
7
|
+
* Handles blacklisting of revoked tokens to prevent their use.
|
|
8
|
+
*
|
|
9
|
+
* ## Overview
|
|
10
|
+
*
|
|
11
|
+
* Revocation allows users to invalidate specific tokens before their natural expiration.
|
|
12
|
+
* This is essential for logout functionality and security in case of token compromise.
|
|
13
|
+
*
|
|
14
|
+
* ## Storage Structure
|
|
15
|
+
*
|
|
16
|
+
* Revoked tokens are stored with their expiration time to allow automatic cleanup:
|
|
17
|
+
* ```
|
|
18
|
+
* revocation:token:{tokenHash} → { revokedAt: timestamp, expiresAt: timestamp }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* ## Security Considerations
|
|
22
|
+
*
|
|
23
|
+
* - Revoked tokens are checked on every use
|
|
24
|
+
* - Storage automatically cleans up expired revocations
|
|
25
|
+
* - Hash tokens for storage to reduce memory usage
|
|
26
|
+
* - Use constant-time comparison for hash verification
|
|
27
|
+
*
|
|
28
|
+
* @packageDocumentation
|
|
29
|
+
*/
|
|
30
|
+
/**
|
|
31
|
+
* Hashes a token for storage.
|
|
32
|
+
* Uses SHA-256 to reduce storage size and prevent exposure of full token value.
|
|
33
|
+
*
|
|
34
|
+
* @param token - The token to hash
|
|
35
|
+
* @returns SHA-256 hash of the token
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
const hashToken = (token) => {
|
|
40
|
+
return createHash("sha256").update(token).digest("hex");
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Token revocation manager.
|
|
44
|
+
* Provides methods to revoke tokens and check if a token has been revoked.
|
|
45
|
+
*/
|
|
46
|
+
const Revocation = {
|
|
47
|
+
revoke: async (storage, token, expiresAt) => {
|
|
48
|
+
const key = ["revocation:token", hashToken(token)];
|
|
49
|
+
const record = {
|
|
50
|
+
revokedAt: Date.now(),
|
|
51
|
+
expiresAt
|
|
52
|
+
};
|
|
53
|
+
const ttlSeconds = Math.ceil((expiresAt - Date.now()) / 1e3);
|
|
54
|
+
await Storage.set(storage, key, record, Math.max(1, ttlSeconds));
|
|
55
|
+
},
|
|
56
|
+
isRevoked: async (storage, token) => {
|
|
57
|
+
const key = ["revocation:token", hashToken(token)];
|
|
58
|
+
return !!await Storage.get(storage, key);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Revocation };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { joinKey, splitKey } from "./storage.
|
|
1
|
+
import { joinKey, splitKey } from "./storage.mjs";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { writeFile } from "node:fs/promises";
|
|
4
4
|
|
|
@@ -79,8 +79,7 @@ const MemoryStorage = (options) => {
|
|
|
79
79
|
};
|
|
80
80
|
return {
|
|
81
81
|
async get(key) {
|
|
82
|
-
const
|
|
83
|
-
const match = search(searchKey);
|
|
82
|
+
const match = search(joinKey(key));
|
|
84
83
|
if (!match.found) return;
|
|
85
84
|
const storeEntry = store[match.index];
|
|
86
85
|
if (!storeEntry) return;
|
|
@@ -104,8 +103,7 @@ const MemoryStorage = (options) => {
|
|
|
104
103
|
await save();
|
|
105
104
|
},
|
|
106
105
|
async remove(key) {
|
|
107
|
-
const
|
|
108
|
-
const match = search(searchKey);
|
|
106
|
+
const match = search(joinKey(key));
|
|
109
107
|
if (match.found) {
|
|
110
108
|
store.splice(match.index, 1);
|
|
111
109
|
await save();
|
|
@@ -42,50 +42,64 @@ interface StorageAdapter {
|
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
44
|
* Joins an array of key segments into a single string using the separator.
|
|
45
|
+
* Segments are properly escaped to handle any input, including separators and escape characters.
|
|
45
46
|
*
|
|
46
47
|
* @param key - Array of key segments to join
|
|
47
48
|
* @returns Single string representing the full key path
|
|
48
49
|
*
|
|
49
50
|
* @example
|
|
50
51
|
* ```ts
|
|
51
|
-
* joinKey(['user', '
|
|
52
|
-
* // Returns: "user\
|
|
52
|
+
* joinKey(['user', 'data\x1fwith\x1fseparators'])
|
|
53
|
+
* // Returns: "user\x1fdata\\x1fwith\\x1fseparators"
|
|
53
54
|
* ```
|
|
54
55
|
*/
|
|
55
56
|
declare const joinKey: (key: string[]) => string;
|
|
56
57
|
/**
|
|
57
58
|
* Splits a joined key string back into its component segments.
|
|
59
|
+
* Handles escaped characters properly.
|
|
58
60
|
*
|
|
59
61
|
* @param key - Joined key string to split
|
|
60
62
|
* @returns Array of individual key segments
|
|
61
63
|
*
|
|
62
64
|
* @example
|
|
63
65
|
* ```ts
|
|
64
|
-
* splitKey("user\
|
|
65
|
-
* // Returns: ['user', '
|
|
66
|
+
* splitKey("user\x1fdata\\x1fwith\\x1fseparators")
|
|
67
|
+
* // Returns: ['user', 'data\x1fwith\x1fseparators']
|
|
66
68
|
* ```
|
|
67
69
|
*/
|
|
68
70
|
declare const splitKey: (key: string) => string[];
|
|
69
71
|
/**
|
|
70
72
|
* High-level storage operations with key encoding and type safety.
|
|
71
73
|
* Provides a convenient interface over storage adapters with additional features
|
|
72
|
-
* like TTL
|
|
74
|
+
* like TTL validation and secure key encoding to prevent collisions.
|
|
73
75
|
*/
|
|
74
76
|
declare const Storage: {
|
|
75
77
|
/**
|
|
76
|
-
* Encodes key segments by
|
|
77
|
-
* Ensures storage keys don't contain characters that could
|
|
78
|
+
* Encodes key segments by escaping special characters.
|
|
79
|
+
* Ensures storage keys don't contain unescaped separator characters that could cause collisions.
|
|
78
80
|
*
|
|
79
81
|
* @param key - Array of key segments to encode
|
|
80
|
-
* @returns Array of
|
|
82
|
+
* @returns Array of properly escaped key segments
|
|
83
|
+
*
|
|
84
|
+
* @throws {Error} If any segment is empty or whitespace-only
|
|
81
85
|
*
|
|
82
86
|
* @example
|
|
83
87
|
* ```ts
|
|
84
88
|
* Storage.encode(['user', 'data\x1fwith\x1fseparators'])
|
|
85
|
-
* // Returns: ['user', '
|
|
89
|
+
* // Returns: ['user', 'data\\x1fwith\\x1fseparators']
|
|
86
90
|
* ```
|
|
87
91
|
*/
|
|
88
92
|
readonly encode: (key: string[]) => string[];
|
|
93
|
+
/**
|
|
94
|
+
* Decodes key segments by unescaping special characters.
|
|
95
|
+
* Reverse operation of encode().
|
|
96
|
+
*
|
|
97
|
+
* @param key - Array of encoded key segments
|
|
98
|
+
* @returns Array of decoded key segments
|
|
99
|
+
*
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
readonly decode: (key: string[]) => string[];
|
|
89
103
|
/**
|
|
90
104
|
* Retrieves a typed value from storage.
|
|
91
105
|
*
|
|
@@ -110,6 +124,7 @@ declare const Storage: {
|
|
|
110
124
|
readonly get: <T = Record<string, unknown>>(adapter: StorageAdapter, key: string[]) => Promise<T | null>;
|
|
111
125
|
/**
|
|
112
126
|
* Stores a value with optional time-to-live in seconds.
|
|
127
|
+
* Validates that TTL is a positive integer to prevent edge cases like negative or overflow values.
|
|
113
128
|
*
|
|
114
129
|
* @param adapter - Storage adapter to use
|
|
115
130
|
* @param key - Array of key segments identifying where to store
|
|
@@ -117,12 +132,14 @@ declare const Storage: {
|
|
|
117
132
|
* @param ttlSeconds - Optional TTL in seconds for automatic expiration
|
|
118
133
|
* @returns Promise that resolves when storage is complete
|
|
119
134
|
*
|
|
135
|
+
* @throws {RangeError} If TTL is invalid (negative, non-integer, or exceeds maximum)
|
|
136
|
+
*
|
|
120
137
|
* @example
|
|
121
138
|
* ```ts
|
|
122
139
|
* // Store with 1 hour TTL
|
|
123
140
|
* await Storage.set(adapter, ['sessions', sessionId], sessionData, 3600)
|
|
124
141
|
*
|
|
125
|
-
* // Store permanently
|
|
142
|
+
* // Store permanently (no expiration)
|
|
126
143
|
* await Storage.set(adapter, ['users', userId], userData)
|
|
127
144
|
* ```
|
|
128
145
|
*/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
//#region src/storage/storage.ts
|
|
2
|
+
/**
|
|
3
|
+
* ASCII unit separator character used to join key segments.
|
|
4
|
+
* Using a control character ensures it won't conflict with user data.
|
|
5
|
+
*/
|
|
6
|
+
const SEPARATOR = String.fromCharCode(31);
|
|
7
|
+
/**
|
|
8
|
+
* Escape character used to escape SEPARATOR characters in key segments.
|
|
9
|
+
* Uses backslash as the escape character, which is then itself escaped when appearing.
|
|
10
|
+
*/
|
|
11
|
+
const ESCAPE = "\\";
|
|
12
|
+
/**
|
|
13
|
+
* Joins an array of key segments into a single string using the separator.
|
|
14
|
+
* Segments are properly escaped to handle any input, including separators and escape characters.
|
|
15
|
+
*
|
|
16
|
+
* @param key - Array of key segments to join
|
|
17
|
+
* @returns Single string representing the full key path
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* joinKey(['user', 'data\x1fwith\x1fseparators'])
|
|
22
|
+
* // Returns: "user\x1fdata\\x1fwith\\x1fseparators"
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
const joinKey = (key) => {
|
|
26
|
+
return key.join(SEPARATOR);
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Splits a joined key string back into its component segments.
|
|
30
|
+
* Handles escaped characters properly.
|
|
31
|
+
*
|
|
32
|
+
* @param key - Joined key string to split
|
|
33
|
+
* @returns Array of individual key segments
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* splitKey("user\x1fdata\\x1fwith\\x1fseparators")
|
|
38
|
+
* // Returns: ['user', 'data\x1fwith\x1fseparators']
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
const splitKey = (key) => {
|
|
42
|
+
return key.split(SEPARATOR);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Encodes a single key segment by escaping special characters.
|
|
46
|
+
* Prevents collisions by properly escaping separator and escape characters.
|
|
47
|
+
*
|
|
48
|
+
* @param segment - The key segment to encode
|
|
49
|
+
* @returns Encoded segment with special characters escaped
|
|
50
|
+
* @throws {Error} If segment is empty or whitespace-only
|
|
51
|
+
*
|
|
52
|
+
* @internal
|
|
53
|
+
*/
|
|
54
|
+
const encodeSegment = (segment) => {
|
|
55
|
+
if (!segment || !segment.trim()) throw new Error(`Storage key segment cannot be empty or whitespace-only: "${segment}"`);
|
|
56
|
+
return segment.replaceAll(ESCAPE, ESCAPE + ESCAPE).replaceAll(SEPARATOR, ESCAPE + SEPARATOR);
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Decodes a key segment by unescaping special characters.
|
|
60
|
+
* Reverse of encodeSegment operation.
|
|
61
|
+
*
|
|
62
|
+
* @param segment - The encoded segment to decode
|
|
63
|
+
* @returns Decoded segment with special characters restored
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
const decodeSegment = (segment) => {
|
|
68
|
+
return segment.replaceAll(ESCAPE + SEPARATOR, SEPARATOR).replaceAll(ESCAPE + ESCAPE, ESCAPE);
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* High-level storage operations with key encoding and type safety.
|
|
72
|
+
* Provides a convenient interface over storage adapters with additional features
|
|
73
|
+
* like TTL validation and secure key encoding to prevent collisions.
|
|
74
|
+
*/
|
|
75
|
+
const Storage = {
|
|
76
|
+
encode: (key) => {
|
|
77
|
+
return key.map(encodeSegment);
|
|
78
|
+
},
|
|
79
|
+
decode: (key) => {
|
|
80
|
+
return key.map(decodeSegment);
|
|
81
|
+
},
|
|
82
|
+
get: (adapter, key) => {
|
|
83
|
+
return adapter.get(Storage.encode(key));
|
|
84
|
+
},
|
|
85
|
+
set: (adapter, key, value, ttlSeconds) => {
|
|
86
|
+
if (ttlSeconds !== void 0 && ttlSeconds !== null) {
|
|
87
|
+
if (!Number.isInteger(ttlSeconds)) throw new RangeError(`Storage TTL must be an integer in seconds, received ${typeof ttlSeconds}`);
|
|
88
|
+
if (ttlSeconds <= 0) throw new RangeError(`Storage TTL must be positive, received ${ttlSeconds}`);
|
|
89
|
+
const maxTtlSeconds = 3600 * 24 * 365 * 10;
|
|
90
|
+
if (ttlSeconds > maxTtlSeconds) throw new RangeError(`Storage TTL exceeds maximum (${maxTtlSeconds}s = 10 years), received ${ttlSeconds}s`);
|
|
91
|
+
}
|
|
92
|
+
const expiry = ttlSeconds ? new Date(Date.now() + ttlSeconds * 1e3) : void 0;
|
|
93
|
+
return adapter.set(Storage.encode(key), value, expiry);
|
|
94
|
+
},
|
|
95
|
+
remove: (adapter, key) => {
|
|
96
|
+
return adapter.remove(Storage.encode(key));
|
|
97
|
+
},
|
|
98
|
+
scan: (adapter, prefix) => {
|
|
99
|
+
return adapter.scan(Storage.encode(prefix));
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//#endregion
|
|
104
|
+
export { Storage, joinKey, splitKey };
|