@xivdyetools/auth 1.0.2 → 1.1.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/README.md +171 -158
- package/dist/hmac.d.ts +6 -0
- package/dist/hmac.d.ts.map +1 -1
- package/dist/hmac.js +49 -7
- package/dist/hmac.js.map +1 -1
- package/dist/jwt.d.ts.map +1 -1
- package/dist/jwt.js +44 -51
- package/dist/jwt.js.map +1 -1
- package/dist/timing.d.ts.map +1 -1
- package/dist/timing.js +4 -2
- package/dist/timing.js.map +1 -1
- package/package.json +75 -71
- package/src/discord.test.ts +243 -243
- package/src/discord.ts +143 -143
- package/src/hmac.test.ts +339 -325
- package/src/hmac.ts +274 -222
- package/src/index.ts +54 -54
- package/src/jwt.test.ts +337 -337
- package/src/jwt.ts +248 -265
- package/src/timing.test.ts +114 -117
- package/src/timing.ts +86 -84
package/src/discord.ts
CHANGED
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Discord Request Verification
|
|
3
|
-
*
|
|
4
|
-
* Wraps the `discord-interactions` library's Ed25519 signature verification
|
|
5
|
-
* with additional security checks (body size limits, header validation).
|
|
6
|
-
*
|
|
7
|
-
* @see https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorization
|
|
8
|
-
* @module discord
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { verifyKey } from 'discord-interactions';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Result of Discord request verification
|
|
15
|
-
*/
|
|
16
|
-
export interface DiscordVerificationResult {
|
|
17
|
-
/** Whether the signature is valid */
|
|
18
|
-
isValid: boolean;
|
|
19
|
-
/** The raw request body (needed for parsing after verification) */
|
|
20
|
-
body: string;
|
|
21
|
-
/** Error message if verification failed */
|
|
22
|
-
error?: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Options for Discord verification
|
|
27
|
-
*/
|
|
28
|
-
export interface DiscordVerifyOptions {
|
|
29
|
-
/** Maximum request body size in bytes (default: 100KB) */
|
|
30
|
-
maxBodySize?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Default maximum body size (100KB) */
|
|
34
|
-
const DEFAULT_MAX_BODY_SIZE = 100_000;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Verify that a request came from Discord using Ed25519 signature verification.
|
|
38
|
-
*
|
|
39
|
-
* Security features:
|
|
40
|
-
* - Content-Length header check (before reading body)
|
|
41
|
-
* - Actual body size validation (Content-Length can be spoofed)
|
|
42
|
-
* - Required header validation (X-Signature-Ed25519, X-Signature-Timestamp)
|
|
43
|
-
*
|
|
44
|
-
* @param request - The incoming HTTP request
|
|
45
|
-
* @param publicKey - Your Discord application's public key
|
|
46
|
-
* @param options - Verification options
|
|
47
|
-
* @returns Verification result with the request body
|
|
48
|
-
*
|
|
49
|
-
* @example
|
|
50
|
-
* ```typescript
|
|
51
|
-
* const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
|
|
52
|
-
* if (!result.isValid) {
|
|
53
|
-
* return new Response(result.error, { status: 401 });
|
|
54
|
-
* }
|
|
55
|
-
* const interaction = JSON.parse(result.body);
|
|
56
|
-
* ```
|
|
57
|
-
*/
|
|
58
|
-
export async function verifyDiscordRequest(
|
|
59
|
-
request: Request,
|
|
60
|
-
publicKey: string,
|
|
61
|
-
options: DiscordVerifyOptions = {}
|
|
62
|
-
): Promise<DiscordVerificationResult> {
|
|
63
|
-
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
64
|
-
|
|
65
|
-
// Check Content-Length header first (if present) to reject obviously large requests
|
|
66
|
-
const contentLength = request.headers.get('Content-Length');
|
|
67
|
-
if (contentLength && parseInt(contentLength, 10) > maxBodySize) {
|
|
68
|
-
return {
|
|
69
|
-
isValid: false,
|
|
70
|
-
body: '',
|
|
71
|
-
error: 'Request body too large',
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Get required headers
|
|
76
|
-
const signature = request.headers.get('X-Signature-Ed25519');
|
|
77
|
-
const timestamp = request.headers.get('X-Signature-Timestamp');
|
|
78
|
-
|
|
79
|
-
if (!signature || !timestamp) {
|
|
80
|
-
return {
|
|
81
|
-
isValid: false,
|
|
82
|
-
body: '',
|
|
83
|
-
error: 'Missing signature headers',
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Get the raw body
|
|
88
|
-
const body = await request.text();
|
|
89
|
-
|
|
90
|
-
// Verify actual body size (Content-Length can be spoofed)
|
|
91
|
-
if (body.length > maxBodySize) {
|
|
92
|
-
return {
|
|
93
|
-
isValid: false,
|
|
94
|
-
body: '',
|
|
95
|
-
error: 'Request body too large',
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// Verify the signature using discord-interactions library
|
|
100
|
-
try {
|
|
101
|
-
const isValid = await verifyKey(body, signature, timestamp, publicKey);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
isValid,
|
|
105
|
-
body,
|
|
106
|
-
error: isValid ? undefined : 'Invalid signature',
|
|
107
|
-
};
|
|
108
|
-
} catch (error) {
|
|
109
|
-
return {
|
|
110
|
-
isValid: false,
|
|
111
|
-
body,
|
|
112
|
-
error: error instanceof Error ? error.message : 'Verification failed',
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Creates a 401 Unauthorized response for failed verification.
|
|
119
|
-
*
|
|
120
|
-
* @param message - Error message (default: 'Invalid request signature')
|
|
121
|
-
* @returns Response object
|
|
122
|
-
*/
|
|
123
|
-
export function unauthorizedResponse(
|
|
124
|
-
message = 'Invalid request signature'
|
|
125
|
-
): Response {
|
|
126
|
-
return new Response(JSON.stringify({ error: message }), {
|
|
127
|
-
status: 401,
|
|
128
|
-
headers: { 'Content-Type': 'application/json' },
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Creates a 400 Bad Request response.
|
|
134
|
-
*
|
|
135
|
-
* @param message - Error message
|
|
136
|
-
* @returns Response object
|
|
137
|
-
*/
|
|
138
|
-
export function badRequestResponse(message: string): Response {
|
|
139
|
-
return new Response(JSON.stringify({ error: message }), {
|
|
140
|
-
status: 400,
|
|
141
|
-
headers: { 'Content-Type': 'application/json' },
|
|
142
|
-
});
|
|
143
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Discord Request Verification
|
|
3
|
+
*
|
|
4
|
+
* Wraps the `discord-interactions` library's Ed25519 signature verification
|
|
5
|
+
* with additional security checks (body size limits, header validation).
|
|
6
|
+
*
|
|
7
|
+
* @see https://discord.com/developers/docs/interactions/receiving-and-responding#security-and-authorization
|
|
8
|
+
* @module discord
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { verifyKey } from 'discord-interactions';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Result of Discord request verification
|
|
15
|
+
*/
|
|
16
|
+
export interface DiscordVerificationResult {
|
|
17
|
+
/** Whether the signature is valid */
|
|
18
|
+
isValid: boolean;
|
|
19
|
+
/** The raw request body (needed for parsing after verification) */
|
|
20
|
+
body: string;
|
|
21
|
+
/** Error message if verification failed */
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for Discord verification
|
|
27
|
+
*/
|
|
28
|
+
export interface DiscordVerifyOptions {
|
|
29
|
+
/** Maximum request body size in bytes (default: 100KB) */
|
|
30
|
+
maxBodySize?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Default maximum body size (100KB) */
|
|
34
|
+
const DEFAULT_MAX_BODY_SIZE = 100_000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Verify that a request came from Discord using Ed25519 signature verification.
|
|
38
|
+
*
|
|
39
|
+
* Security features:
|
|
40
|
+
* - Content-Length header check (before reading body)
|
|
41
|
+
* - Actual body size validation (Content-Length can be spoofed)
|
|
42
|
+
* - Required header validation (X-Signature-Ed25519, X-Signature-Timestamp)
|
|
43
|
+
*
|
|
44
|
+
* @param request - The incoming HTTP request
|
|
45
|
+
* @param publicKey - Your Discord application's public key
|
|
46
|
+
* @param options - Verification options
|
|
47
|
+
* @returns Verification result with the request body
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```typescript
|
|
51
|
+
* const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
|
|
52
|
+
* if (!result.isValid) {
|
|
53
|
+
* return new Response(result.error, { status: 401 });
|
|
54
|
+
* }
|
|
55
|
+
* const interaction = JSON.parse(result.body);
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
export async function verifyDiscordRequest(
|
|
59
|
+
request: Request,
|
|
60
|
+
publicKey: string,
|
|
61
|
+
options: DiscordVerifyOptions = {}
|
|
62
|
+
): Promise<DiscordVerificationResult> {
|
|
63
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
64
|
+
|
|
65
|
+
// Check Content-Length header first (if present) to reject obviously large requests
|
|
66
|
+
const contentLength = request.headers.get('Content-Length');
|
|
67
|
+
if (contentLength && parseInt(contentLength, 10) > maxBodySize) {
|
|
68
|
+
return {
|
|
69
|
+
isValid: false,
|
|
70
|
+
body: '',
|
|
71
|
+
error: 'Request body too large',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Get required headers
|
|
76
|
+
const signature = request.headers.get('X-Signature-Ed25519');
|
|
77
|
+
const timestamp = request.headers.get('X-Signature-Timestamp');
|
|
78
|
+
|
|
79
|
+
if (!signature || !timestamp) {
|
|
80
|
+
return {
|
|
81
|
+
isValid: false,
|
|
82
|
+
body: '',
|
|
83
|
+
error: 'Missing signature headers',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Get the raw body
|
|
88
|
+
const body = await request.text();
|
|
89
|
+
|
|
90
|
+
// Verify actual body size (Content-Length can be spoofed)
|
|
91
|
+
if (body.length > maxBodySize) {
|
|
92
|
+
return {
|
|
93
|
+
isValid: false,
|
|
94
|
+
body: '',
|
|
95
|
+
error: 'Request body too large',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Verify the signature using discord-interactions library
|
|
100
|
+
try {
|
|
101
|
+
const isValid = await verifyKey(body, signature, timestamp, publicKey);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
isValid,
|
|
105
|
+
body,
|
|
106
|
+
error: isValid ? undefined : 'Invalid signature',
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
isValid: false,
|
|
111
|
+
body,
|
|
112
|
+
error: error instanceof Error ? error.message : 'Verification failed',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates a 401 Unauthorized response for failed verification.
|
|
119
|
+
*
|
|
120
|
+
* @param message - Error message (default: 'Invalid request signature')
|
|
121
|
+
* @returns Response object
|
|
122
|
+
*/
|
|
123
|
+
export function unauthorizedResponse(
|
|
124
|
+
message = 'Invalid request signature'
|
|
125
|
+
): Response {
|
|
126
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
127
|
+
status: 401,
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Creates a 400 Bad Request response.
|
|
134
|
+
*
|
|
135
|
+
* @param message - Error message
|
|
136
|
+
* @returns Response object
|
|
137
|
+
*/
|
|
138
|
+
export function badRequestResponse(message: string): Response {
|
|
139
|
+
return new Response(JSON.stringify({ error: message }), {
|
|
140
|
+
status: 400,
|
|
141
|
+
headers: { 'Content-Type': 'application/json' },
|
|
142
|
+
});
|
|
143
|
+
}
|