@xivdyetools/auth 1.0.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.
@@ -0,0 +1,65 @@
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
+ * Result of Discord request verification
12
+ */
13
+ export interface DiscordVerificationResult {
14
+ /** Whether the signature is valid */
15
+ isValid: boolean;
16
+ /** The raw request body (needed for parsing after verification) */
17
+ body: string;
18
+ /** Error message if verification failed */
19
+ error?: string;
20
+ }
21
+ /**
22
+ * Options for Discord verification
23
+ */
24
+ export interface DiscordVerifyOptions {
25
+ /** Maximum request body size in bytes (default: 100KB) */
26
+ maxBodySize?: number;
27
+ }
28
+ /**
29
+ * Verify that a request came from Discord using Ed25519 signature verification.
30
+ *
31
+ * Security features:
32
+ * - Content-Length header check (before reading body)
33
+ * - Actual body size validation (Content-Length can be spoofed)
34
+ * - Required header validation (X-Signature-Ed25519, X-Signature-Timestamp)
35
+ *
36
+ * @param request - The incoming HTTP request
37
+ * @param publicKey - Your Discord application's public key
38
+ * @param options - Verification options
39
+ * @returns Verification result with the request body
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
44
+ * if (!result.isValid) {
45
+ * return new Response(result.error, { status: 401 });
46
+ * }
47
+ * const interaction = JSON.parse(result.body);
48
+ * ```
49
+ */
50
+ export declare function verifyDiscordRequest(request: Request, publicKey: string, options?: DiscordVerifyOptions): Promise<DiscordVerificationResult>;
51
+ /**
52
+ * Creates a 401 Unauthorized response for failed verification.
53
+ *
54
+ * @param message - Error message (default: 'Invalid request signature')
55
+ * @returns Response object
56
+ */
57
+ export declare function unauthorizedResponse(message?: string): Response;
58
+ /**
59
+ * Creates a 400 Bad Request response.
60
+ *
61
+ * @param message - Error message
62
+ * @returns Response object
63
+ */
64
+ export declare function badRequestResponse(message: string): Response;
65
+ //# sourceMappingURL=discord.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord.d.ts","sourceRoot":"","sources":["../src/discord.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,qCAAqC;IACrC,OAAO,EAAE,OAAO,CAAC;IACjB,mEAAmE;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,2CAA2C;IAC3C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAKD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,yBAAyB,CAAC,CAqDpC;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,SAA8B,GACpC,QAAQ,CAKV;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,QAAQ,CAK5D"}
@@ -0,0 +1,107 @@
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
+ import { verifyKey } from 'discord-interactions';
11
+ /** Default maximum body size (100KB) */
12
+ const DEFAULT_MAX_BODY_SIZE = 100_000;
13
+ /**
14
+ * Verify that a request came from Discord using Ed25519 signature verification.
15
+ *
16
+ * Security features:
17
+ * - Content-Length header check (before reading body)
18
+ * - Actual body size validation (Content-Length can be spoofed)
19
+ * - Required header validation (X-Signature-Ed25519, X-Signature-Timestamp)
20
+ *
21
+ * @param request - The incoming HTTP request
22
+ * @param publicKey - Your Discord application's public key
23
+ * @param options - Verification options
24
+ * @returns Verification result with the request body
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
29
+ * if (!result.isValid) {
30
+ * return new Response(result.error, { status: 401 });
31
+ * }
32
+ * const interaction = JSON.parse(result.body);
33
+ * ```
34
+ */
35
+ export async function verifyDiscordRequest(request, publicKey, options = {}) {
36
+ const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
37
+ // Check Content-Length header first (if present) to reject obviously large requests
38
+ const contentLength = request.headers.get('Content-Length');
39
+ if (contentLength && parseInt(contentLength, 10) > maxBodySize) {
40
+ return {
41
+ isValid: false,
42
+ body: '',
43
+ error: 'Request body too large',
44
+ };
45
+ }
46
+ // Get required headers
47
+ const signature = request.headers.get('X-Signature-Ed25519');
48
+ const timestamp = request.headers.get('X-Signature-Timestamp');
49
+ if (!signature || !timestamp) {
50
+ return {
51
+ isValid: false,
52
+ body: '',
53
+ error: 'Missing signature headers',
54
+ };
55
+ }
56
+ // Get the raw body
57
+ const body = await request.text();
58
+ // Verify actual body size (Content-Length can be spoofed)
59
+ if (body.length > maxBodySize) {
60
+ return {
61
+ isValid: false,
62
+ body: '',
63
+ error: 'Request body too large',
64
+ };
65
+ }
66
+ // Verify the signature using discord-interactions library
67
+ try {
68
+ const isValid = await verifyKey(body, signature, timestamp, publicKey);
69
+ return {
70
+ isValid,
71
+ body,
72
+ error: isValid ? undefined : 'Invalid signature',
73
+ };
74
+ }
75
+ catch (error) {
76
+ return {
77
+ isValid: false,
78
+ body,
79
+ error: error instanceof Error ? error.message : 'Verification failed',
80
+ };
81
+ }
82
+ }
83
+ /**
84
+ * Creates a 401 Unauthorized response for failed verification.
85
+ *
86
+ * @param message - Error message (default: 'Invalid request signature')
87
+ * @returns Response object
88
+ */
89
+ export function unauthorizedResponse(message = 'Invalid request signature') {
90
+ return new Response(JSON.stringify({ error: message }), {
91
+ status: 401,
92
+ headers: { 'Content-Type': 'application/json' },
93
+ });
94
+ }
95
+ /**
96
+ * Creates a 400 Bad Request response.
97
+ *
98
+ * @param message - Error message
99
+ * @returns Response object
100
+ */
101
+ export function badRequestResponse(message) {
102
+ return new Response(JSON.stringify({ error: message }), {
103
+ status: 400,
104
+ headers: { 'Content-Type': 'application/json' },
105
+ });
106
+ }
107
+ //# sourceMappingURL=discord.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discord.js","sourceRoot":"","sources":["../src/discord.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAsBjD,wCAAwC;AACxC,MAAM,qBAAqB,GAAG,OAAO,CAAC;AAEtC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,OAAgB,EAChB,SAAiB,EACjB,UAAgC,EAAE;IAElC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAC;IAEjE,oFAAoF;IACpF,MAAM,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IAC5D,IAAI,aAAa,IAAI,QAAQ,CAAC,aAAa,EAAE,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC;QAC/D,OAAO;YACL,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,EAAE;YACR,KAAK,EAAE,wBAAwB;SAChC,CAAC;IACJ,CAAC;IAED,uBAAuB;IACvB,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAC;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,uBAAuB,CAAC,CAAC;IAE/D,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS,EAAE,CAAC;QAC7B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,EAAE;YACR,KAAK,EAAE,2BAA2B;SACnC,CAAC;IACJ,CAAC;IAED,mBAAmB;IACnB,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,IAAI,EAAE,CAAC;IAElC,0DAA0D;IAC1D,IAAI,IAAI,CAAC,MAAM,GAAG,WAAW,EAAE,CAAC;QAC9B,OAAO;YACL,OAAO,EAAE,KAAK;YACd,IAAI,EAAE,EAAE;YACR,KAAK,EAAE,wBAAwB;SAChC,CAAC;IACJ,CAAC;IAED,0DAA0D;IAC1D,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,SAAS,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;QAEvE,OAAO;YACL,OAAO;YACP,IAAI;YACJ,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,mBAAmB;SACjD,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO;YACL,OAAO,EAAE,KAAK;YACd,IAAI;YACJ,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,qBAAqB;SACtE,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAO,GAAG,2BAA2B;IAErC,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;QACtD,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE;QACtD,MAAM,EAAE,GAAG;QACX,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;KAChD,CAAC,CAAC;AACL,CAAC"}
package/dist/hmac.d.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * HMAC Signing Utilities
3
+ *
4
+ * Provides HMAC-SHA256 signing and verification using the Web Crypto API.
5
+ * Used for JWT signing and bot request authentication.
6
+ *
7
+ * @module hmac
8
+ */
9
+ /**
10
+ * Options for bot signature verification
11
+ */
12
+ export interface BotSignatureOptions {
13
+ /** Maximum age of signature in milliseconds (default: 5 minutes) */
14
+ maxAgeMs?: number;
15
+ /** Allowed clock skew in milliseconds (default: 1 minute) */
16
+ clockSkewMs?: number;
17
+ }
18
+ /**
19
+ * Create an HMAC-SHA256 CryptoKey from a secret string.
20
+ *
21
+ * @param secret - The secret string to use as key material
22
+ * @param usage - Key usage: 'sign', 'verify', or 'both'
23
+ * @returns CryptoKey for HMAC operations
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * const key = await createHmacKey(process.env.JWT_SECRET, 'verify');
28
+ * ```
29
+ */
30
+ export declare function createHmacKey(secret: string, usage?: 'sign' | 'verify' | 'both'): Promise<CryptoKey>;
31
+ /**
32
+ * Sign data with HMAC-SHA256 and return base64url-encoded signature.
33
+ *
34
+ * @param data - The data to sign
35
+ * @param secret - The secret key
36
+ * @returns Base64URL-encoded signature
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * const signature = await hmacSign('header.payload', jwtSecret);
41
+ * ```
42
+ */
43
+ export declare function hmacSign(data: string, secret: string): Promise<string>;
44
+ /**
45
+ * Sign data with HMAC-SHA256 and return hex-encoded signature.
46
+ *
47
+ * @param data - The data to sign
48
+ * @param secret - The secret key
49
+ * @returns Hex-encoded signature
50
+ *
51
+ * @example
52
+ * ```typescript
53
+ * const signature = await hmacSignHex('timestamp:userId:userName', secret);
54
+ * ```
55
+ */
56
+ export declare function hmacSignHex(data: string, secret: string): Promise<string>;
57
+ /**
58
+ * Verify HMAC-SHA256 signature (base64url-encoded).
59
+ *
60
+ * @param data - The original data that was signed
61
+ * @param signature - Base64URL-encoded signature to verify
62
+ * @param secret - The secret key
63
+ * @returns true if signature is valid
64
+ */
65
+ export declare function hmacVerify(data: string, signature: string, secret: string): Promise<boolean>;
66
+ /**
67
+ * Verify HMAC-SHA256 signature (hex-encoded).
68
+ *
69
+ * @param data - The original data that was signed
70
+ * @param signature - Hex-encoded signature to verify
71
+ * @param secret - The secret key
72
+ * @returns true if signature is valid
73
+ */
74
+ export declare function hmacVerifyHex(data: string, signature: string, secret: string): Promise<boolean>;
75
+ /**
76
+ * Verify a bot request signature.
77
+ *
78
+ * Bot signatures use the format: `${timestamp}:${userDiscordId}:${userName}`
79
+ * Signatures are hex-encoded HMAC-SHA256.
80
+ *
81
+ * @param signature - Hex-encoded signature
82
+ * @param timestamp - Unix timestamp string (seconds)
83
+ * @param userDiscordId - Discord user ID
84
+ * @param userName - Discord username
85
+ * @param secret - The signing secret
86
+ * @param options - Verification options
87
+ * @returns true if signature is valid and not expired
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const isValid = await verifyBotSignature(
92
+ * request.headers.get('X-Signature'),
93
+ * request.headers.get('X-Timestamp'),
94
+ * request.headers.get('X-User-Id'),
95
+ * request.headers.get('X-User-Name'),
96
+ * env.BOT_SIGNING_SECRET
97
+ * );
98
+ * ```
99
+ */
100
+ export declare function verifyBotSignature(signature: string | undefined, timestamp: string | undefined, userDiscordId: string | undefined, userName: string | undefined, secret: string, options?: BotSignatureOptions): Promise<boolean>;
101
+ //# sourceMappingURL=hmac.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmac.d.ts","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,6DAA6D;IAC7D,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,EACd,KAAK,GAAE,MAAM,GAAG,QAAQ,GAAG,MAAe,GACzC,OAAO,CAAC,SAAS,CAAC,CAcpB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAK5E;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAKjB;AAED;;;;;;;GAOG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAQlB;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,OAAO,CAAC,CAelB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,aAAa,EAAE,MAAM,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,OAAO,CAAC,CAgClB"}
package/dist/hmac.js ADDED
@@ -0,0 +1,153 @@
1
+ /**
2
+ * HMAC Signing Utilities
3
+ *
4
+ * Provides HMAC-SHA256 signing and verification using the Web Crypto API.
5
+ * Used for JWT signing and bot request authentication.
6
+ *
7
+ * @module hmac
8
+ */
9
+ import { base64UrlEncodeBytes, bytesToHex, hexToBytes, } from '@xivdyetools/crypto';
10
+ /**
11
+ * Create an HMAC-SHA256 CryptoKey from a secret string.
12
+ *
13
+ * @param secret - The secret string to use as key material
14
+ * @param usage - Key usage: 'sign', 'verify', or 'both'
15
+ * @returns CryptoKey for HMAC operations
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const key = await createHmacKey(process.env.JWT_SECRET, 'verify');
20
+ * ```
21
+ */
22
+ export async function createHmacKey(secret, usage = 'both') {
23
+ const encoder = new TextEncoder();
24
+ const keyData = encoder.encode(secret);
25
+ const keyUsages = usage === 'both' ? ['sign', 'verify'] : [usage];
26
+ return crypto.subtle.importKey('raw', keyData, { name: 'HMAC', hash: 'SHA-256' }, false, keyUsages);
27
+ }
28
+ /**
29
+ * Sign data with HMAC-SHA256 and return base64url-encoded signature.
30
+ *
31
+ * @param data - The data to sign
32
+ * @param secret - The secret key
33
+ * @returns Base64URL-encoded signature
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * const signature = await hmacSign('header.payload', jwtSecret);
38
+ * ```
39
+ */
40
+ export async function hmacSign(data, secret) {
41
+ const key = await createHmacKey(secret, 'sign');
42
+ const encoder = new TextEncoder();
43
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
44
+ return base64UrlEncodeBytes(new Uint8Array(signature));
45
+ }
46
+ /**
47
+ * Sign data with HMAC-SHA256 and return hex-encoded signature.
48
+ *
49
+ * @param data - The data to sign
50
+ * @param secret - The secret key
51
+ * @returns Hex-encoded signature
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * const signature = await hmacSignHex('timestamp:userId:userName', secret);
56
+ * ```
57
+ */
58
+ export async function hmacSignHex(data, secret) {
59
+ const key = await createHmacKey(secret, 'sign');
60
+ const encoder = new TextEncoder();
61
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
62
+ return bytesToHex(new Uint8Array(signature));
63
+ }
64
+ /**
65
+ * Verify HMAC-SHA256 signature (base64url-encoded).
66
+ *
67
+ * @param data - The original data that was signed
68
+ * @param signature - Base64URL-encoded signature to verify
69
+ * @param secret - The secret key
70
+ * @returns true if signature is valid
71
+ */
72
+ export async function hmacVerify(data, signature, secret) {
73
+ try {
74
+ const expectedSignature = await hmacSign(data, secret);
75
+ // Use timing-safe comparison
76
+ return expectedSignature === signature;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
82
+ /**
83
+ * Verify HMAC-SHA256 signature (hex-encoded).
84
+ *
85
+ * @param data - The original data that was signed
86
+ * @param signature - Hex-encoded signature to verify
87
+ * @param secret - The secret key
88
+ * @returns true if signature is valid
89
+ */
90
+ export async function hmacVerifyHex(data, signature, secret) {
91
+ try {
92
+ const key = await createHmacKey(secret, 'verify');
93
+ const encoder = new TextEncoder();
94
+ const signatureBytes = hexToBytes(signature);
95
+ return crypto.subtle.verify('HMAC', key, signatureBytes, encoder.encode(data));
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ /**
102
+ * Verify a bot request signature.
103
+ *
104
+ * Bot signatures use the format: `${timestamp}:${userDiscordId}:${userName}`
105
+ * Signatures are hex-encoded HMAC-SHA256.
106
+ *
107
+ * @param signature - Hex-encoded signature
108
+ * @param timestamp - Unix timestamp string (seconds)
109
+ * @param userDiscordId - Discord user ID
110
+ * @param userName - Discord username
111
+ * @param secret - The signing secret
112
+ * @param options - Verification options
113
+ * @returns true if signature is valid and not expired
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const isValid = await verifyBotSignature(
118
+ * request.headers.get('X-Signature'),
119
+ * request.headers.get('X-Timestamp'),
120
+ * request.headers.get('X-User-Id'),
121
+ * request.headers.get('X-User-Name'),
122
+ * env.BOT_SIGNING_SECRET
123
+ * );
124
+ * ```
125
+ */
126
+ export async function verifyBotSignature(signature, timestamp, userDiscordId, userName, secret, options = {}) {
127
+ const { maxAgeMs = 5 * 60 * 1000, clockSkewMs = 60 * 1000 } = options;
128
+ // Validate required fields
129
+ if (!signature || !timestamp || !userDiscordId || !userName) {
130
+ return false;
131
+ }
132
+ // Validate timestamp format
133
+ const timestampNum = parseInt(timestamp, 10);
134
+ if (isNaN(timestampNum)) {
135
+ return false;
136
+ }
137
+ // Check timestamp age (with clock skew tolerance)
138
+ const now = Date.now();
139
+ const signatureTime = timestampNum * 1000; // Convert to milliseconds
140
+ const age = now - signatureTime;
141
+ // Reject if too old
142
+ if (age > maxAgeMs) {
143
+ return false;
144
+ }
145
+ // Reject if too far in the future (clock skew protection)
146
+ if (signatureTime > now + clockSkewMs) {
147
+ return false;
148
+ }
149
+ // Verify the signature
150
+ const message = `${timestamp}:${userDiscordId}:${userName}`;
151
+ return hmacVerifyHex(message, signature, secret);
152
+ }
153
+ //# sourceMappingURL=hmac.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hmac.js","sourceRoot":"","sources":["../src/hmac.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @xivdyetools/auth
3
+ *
4
+ * Shared authentication utilities for the xivdyetools ecosystem.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { verifyJWT, verifyDiscordRequest, timingSafeEqual } from '@xivdyetools/auth';
9
+ *
10
+ * // Verify JWT
11
+ * const payload = await verifyJWT(token, env.JWT_SECRET);
12
+ *
13
+ * // Verify Discord request
14
+ * const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
15
+ *
16
+ * // Timing-safe comparison
17
+ * const isValid = await timingSafeEqual(provided, expected);
18
+ * ```
19
+ *
20
+ * @module @xivdyetools/auth
21
+ */
22
+ export { verifyJWT, verifyJWTSignatureOnly, decodeJWT, isJWTExpired, getJWTTimeToExpiry, type JWTPayload, } from './jwt.js';
23
+ export { createHmacKey, hmacSign, hmacSignHex, hmacVerify, hmacVerifyHex, verifyBotSignature, type BotSignatureOptions, } from './hmac.js';
24
+ export { timingSafeEqual, timingSafeEqualBytes } from './timing.js';
25
+ export { verifyDiscordRequest, unauthorizedResponse, badRequestResponse, type DiscordVerificationResult, type DiscordVerifyOptions, } from './discord.js';
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,SAAS,EACT,YAAY,EACZ,kBAAkB,EAClB,KAAK,UAAU,GAChB,MAAM,UAAU,CAAC;AAGlB,OAAO,EACL,aAAa,EACb,QAAQ,EACR,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,EAClB,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAGpE,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,yBAAyB,EAC9B,KAAK,oBAAoB,GAC1B,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @xivdyetools/auth
3
+ *
4
+ * Shared authentication utilities for the xivdyetools ecosystem.
5
+ *
6
+ * @example
7
+ * ```typescript
8
+ * import { verifyJWT, verifyDiscordRequest, timingSafeEqual } from '@xivdyetools/auth';
9
+ *
10
+ * // Verify JWT
11
+ * const payload = await verifyJWT(token, env.JWT_SECRET);
12
+ *
13
+ * // Verify Discord request
14
+ * const result = await verifyDiscordRequest(request, env.DISCORD_PUBLIC_KEY);
15
+ *
16
+ * // Timing-safe comparison
17
+ * const isValid = await timingSafeEqual(provided, expected);
18
+ * ```
19
+ *
20
+ * @module @xivdyetools/auth
21
+ */
22
+ // JWT utilities
23
+ export { verifyJWT, verifyJWTSignatureOnly, decodeJWT, isJWTExpired, getJWTTimeToExpiry, } from './jwt.js';
24
+ // HMAC utilities
25
+ export { createHmacKey, hmacSign, hmacSignHex, hmacVerify, hmacVerifyHex, verifyBotSignature, } from './hmac.js';
26
+ // Timing-safe utilities
27
+ export { timingSafeEqual, timingSafeEqualBytes } from './timing.js';
28
+ // Discord verification
29
+ export { verifyDiscordRequest, unauthorizedResponse, badRequestResponse, } from './discord.js';
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,gBAAgB;AAChB,OAAO,EACL,SAAS,EACT,sBAAsB,EACtB,SAAS,EACT,YAAY,EACZ,kBAAkB,GAEnB,MAAM,UAAU,CAAC;AAElB,iBAAiB;AACjB,OAAO,EACL,aAAa,EACb,QAAQ,EACR,WAAW,EACX,UAAU,EACV,aAAa,EACb,kBAAkB,GAEnB,MAAM,WAAW,CAAC;AAEnB,wBAAwB;AACxB,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAEpE,uBAAuB;AACvB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,kBAAkB,GAGnB,MAAM,cAAc,CAAC"}
package/dist/jwt.d.ts ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * JWT Verification Utilities
3
+ *
4
+ * Provides JWT verification using HMAC-SHA256 (HS256) with the Web Crypto API.
5
+ * Intentionally does NOT include JWT creation - that stays in the oauth service.
6
+ *
7
+ * Security features:
8
+ * - Algorithm validation (rejects non-HS256 tokens)
9
+ * - Expiration checking
10
+ * - Timing-safe signature comparison
11
+ *
12
+ * @module jwt
13
+ */
14
+ /**
15
+ * JWT payload structure
16
+ *
17
+ * Re-exported from @xivdyetools/types for convenience.
18
+ * Consumers should import from here rather than directly from types.
19
+ */
20
+ export interface JWTPayload {
21
+ /** Subject - Discord user ID */
22
+ sub: string;
23
+ /** Issued at timestamp (seconds) */
24
+ iat: number;
25
+ /** Expiration timestamp (seconds) */
26
+ exp: number;
27
+ /** Token type: 'access' or 'refresh' */
28
+ type: 'access' | 'refresh';
29
+ /** Discord username */
30
+ username?: string;
31
+ /** Discord avatar hash */
32
+ avatar?: string | null;
33
+ }
34
+ /**
35
+ * Decode a JWT without verifying the signature.
36
+ *
37
+ * WARNING: Only use this for debugging or when you'll verify separately.
38
+ * For production use, always use `verifyJWT()`.
39
+ *
40
+ * @param token - The JWT string
41
+ * @returns Decoded payload or null if malformed
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const payload = decodeJWT(token);
46
+ * console.log('Token expires:', new Date(payload.exp * 1000));
47
+ * ```
48
+ */
49
+ export declare function decodeJWT(token: string): JWTPayload | null;
50
+ /**
51
+ * Verify a JWT and return the payload if valid.
52
+ *
53
+ * Performs full verification:
54
+ * 1. Validates token structure (3 parts)
55
+ * 2. Validates algorithm is HS256 (prevents confusion attacks)
56
+ * 3. Verifies HMAC-SHA256 signature
57
+ * 4. Checks expiration time
58
+ *
59
+ * @param token - The JWT string
60
+ * @param secret - The HMAC secret used to sign the token
61
+ * @returns Verified payload or null if invalid/expired
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * const payload = await verifyJWT(token, env.JWT_SECRET);
66
+ * if (!payload) {
67
+ * return new Response('Unauthorized', { status: 401 });
68
+ * }
69
+ * ```
70
+ */
71
+ export declare function verifyJWT(token: string, secret: string): Promise<JWTPayload | null>;
72
+ /**
73
+ * Verify JWT signature only, ignoring expiration.
74
+ *
75
+ * Used for refresh tokens where we want to verify authenticity
76
+ * but allow some grace period past expiration.
77
+ *
78
+ * @param token - The JWT string
79
+ * @param secret - The HMAC secret
80
+ * @param maxAgeMs - Optional maximum age in milliseconds (from iat)
81
+ * @returns Payload if signature valid, null otherwise
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Allow refresh tokens up to 7 days old
86
+ * const payload = await verifyJWTSignatureOnly(
87
+ * refreshToken,
88
+ * env.JWT_SECRET,
89
+ * 7 * 24 * 60 * 60 * 1000
90
+ * );
91
+ * ```
92
+ */
93
+ export declare function verifyJWTSignatureOnly(token: string, secret: string, maxAgeMs?: number): Promise<JWTPayload | null>;
94
+ /**
95
+ * Check if a JWT is expired without full verification.
96
+ *
97
+ * Useful for quick checks before making API calls.
98
+ *
99
+ * @param token - The JWT string
100
+ * @returns true if token is expired or malformed
101
+ */
102
+ export declare function isJWTExpired(token: string): boolean;
103
+ /**
104
+ * Get time until JWT expiration.
105
+ *
106
+ * @param token - The JWT string
107
+ * @returns Seconds until expiration, or 0 if expired/invalid
108
+ */
109
+ export declare function getJWTTimeToExpiry(token: string): number;
110
+ //# sourceMappingURL=jwt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../src/jwt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,gCAAgC;IAChC,GAAG,EAAE,MAAM,CAAC;IACZ,oCAAoC;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,wCAAwC;IACxC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAC3B,uBAAuB;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAY1D;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,SAAS,CAC7B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAmD5B;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAqD5B;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAOnD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOxD"}