@signe/room 1.4.2 → 2.0.1
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/index.d.ts +365 -22
- package/dist/index.js +1659 -71
- package/dist/index.js.map +1 -1
- package/examples/game/app/client.tsx +2 -2
- package/examples/game/app/components/Admin.tsx +1089 -0
- package/examples/game/app/components/Room.tsx +158 -0
- package/examples/game/party/server.ts +3 -2
- package/examples/game/party/shard.ts +5 -0
- package/examples/game/partykit.json +5 -1
- package/package.json +2 -2
- package/readme.md +234 -2
- package/src/decorators.ts +34 -2
- package/src/index.ts +5 -1
- package/src/interfaces.ts +13 -0
- package/src/jwt.ts +217 -0
- package/src/mock.ts +39 -3
- package/src/request/cors.ts +54 -0
- package/src/request/response.ts +228 -0
- package/src/server.ts +588 -86
- package/src/shard.ts +244 -0
- package/src/testing.ts +47 -6
- package/src/utils.ts +7 -0
- package/src/world.guard.ts +28 -0
- package/src/world.ts +448 -0
- package/examples/game/app/components/Counter.tsx +0 -82
package/src/jwt.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT Authentication Class for Cloudflare Workers
|
|
3
|
+
* Uses Web Crypto API for JWT signing and verification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface JWTOptions {
|
|
7
|
+
expiresIn?: string | number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface JWTHeader {
|
|
11
|
+
alg: string;
|
|
12
|
+
typ: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface JWTPayload {
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
iat?: number;
|
|
18
|
+
exp?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class JWTAuth {
|
|
22
|
+
private secret: string;
|
|
23
|
+
private encoder: TextEncoder;
|
|
24
|
+
private decoder: TextDecoder;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Constructor for the JWTAuth class
|
|
28
|
+
* @param {string} secret - The secret key used for signing and verifying tokens
|
|
29
|
+
*/
|
|
30
|
+
constructor(secret: string) {
|
|
31
|
+
if (!secret || typeof secret !== 'string') {
|
|
32
|
+
throw new Error('Secret is required and must be a string');
|
|
33
|
+
}
|
|
34
|
+
this.secret = secret;
|
|
35
|
+
this.encoder = new TextEncoder();
|
|
36
|
+
this.decoder = new TextDecoder();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Convert the secret to a CryptoKey for HMAC operations
|
|
41
|
+
* @returns {Promise<CryptoKey>} - The CryptoKey for HMAC operations
|
|
42
|
+
*/
|
|
43
|
+
private async getSecretKey(): Promise<CryptoKey> {
|
|
44
|
+
const keyData: Uint8Array = this.encoder.encode(this.secret);
|
|
45
|
+
return await crypto.subtle.importKey(
|
|
46
|
+
'raw', // format
|
|
47
|
+
keyData, // key data
|
|
48
|
+
{
|
|
49
|
+
name: 'HMAC',
|
|
50
|
+
hash: { name: 'SHA-256' },
|
|
51
|
+
},
|
|
52
|
+
false, // extractable
|
|
53
|
+
['sign', 'verify'] // key usages
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Base64Url encode a buffer
|
|
59
|
+
* @param {ArrayBuffer} buffer - The buffer to encode
|
|
60
|
+
* @returns {string} - The base64url encoded string
|
|
61
|
+
*/
|
|
62
|
+
private base64UrlEncode(buffer: ArrayBuffer): string {
|
|
63
|
+
const base64: string = btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
64
|
+
return base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Base64Url decode a string
|
|
69
|
+
* @param {string} base64Url - The base64url encoded string
|
|
70
|
+
* @returns {ArrayBuffer} - The decoded buffer
|
|
71
|
+
*/
|
|
72
|
+
private base64UrlDecode(base64Url: string) {
|
|
73
|
+
const padding: string = '='.repeat((4 - (base64Url.length % 4)) % 4);
|
|
74
|
+
const base64: string = base64Url.replace(/-/g, '+').replace(/_/g, '/') + padding;
|
|
75
|
+
const rawData: string = atob(base64);
|
|
76
|
+
const buffer: Uint8Array = new Uint8Array(rawData.length);
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < rawData.length; i++) {
|
|
79
|
+
buffer[i] = rawData.charCodeAt(i);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return buffer.buffer;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sign a payload and create a JWT token
|
|
87
|
+
* @param {JWTPayload} payload - The payload to include in the token
|
|
88
|
+
* @param {JWTOptions} [options={}] - Options for the token
|
|
89
|
+
* @param {string | number} [options.expiresIn='1h'] - Token expiration time
|
|
90
|
+
* @returns {Promise<string>} - The JWT token
|
|
91
|
+
*/
|
|
92
|
+
public async sign(payload: JWTPayload, options: JWTOptions = {}): Promise<string> {
|
|
93
|
+
if (!payload || typeof payload !== 'object') {
|
|
94
|
+
throw new Error('Payload must be an object');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Set default expiration if not provided
|
|
98
|
+
const expiresIn: string | number = options.expiresIn || '1h';
|
|
99
|
+
|
|
100
|
+
// Calculate expiration time
|
|
101
|
+
let exp: number | undefined;
|
|
102
|
+
if (typeof expiresIn === 'number') {
|
|
103
|
+
exp = Math.floor(Date.now() / 1000) + expiresIn;
|
|
104
|
+
} else if (typeof expiresIn === 'string') {
|
|
105
|
+
const match: RegExpMatchArray | null = expiresIn.match(/^(\d+)([smhd])$/);
|
|
106
|
+
if (match) {
|
|
107
|
+
const value: number = parseInt(match[1]);
|
|
108
|
+
const unit: string = match[2];
|
|
109
|
+
const seconds: number = {
|
|
110
|
+
's': value,
|
|
111
|
+
'm': value * 60,
|
|
112
|
+
'h': value * 60 * 60,
|
|
113
|
+
'd': value * 60 * 60 * 24
|
|
114
|
+
}[unit]!;
|
|
115
|
+
exp = Math.floor(Date.now() / 1000) + seconds;
|
|
116
|
+
} else {
|
|
117
|
+
throw new Error('Invalid expiresIn format. Use a number (seconds) or a string like "1h", "30m", etc.');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create full payload with claims
|
|
122
|
+
const fullPayload: JWTPayload = {
|
|
123
|
+
...payload,
|
|
124
|
+
iat: Math.floor(Date.now() / 1000),
|
|
125
|
+
exp
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Create header
|
|
129
|
+
const header: JWTHeader = {
|
|
130
|
+
alg: 'HS256',
|
|
131
|
+
typ: 'JWT'
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Encode header and payload
|
|
135
|
+
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
136
|
+
const encodedHeader: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(header)));
|
|
137
|
+
// @ts-expect-error - TS doesn't have a built-in TextEncoder
|
|
138
|
+
const encodedPayload: string = this.base64UrlEncode(this.encoder.encode(JSON.stringify(fullPayload)));
|
|
139
|
+
|
|
140
|
+
// Create signature base
|
|
141
|
+
const signatureBase: string = `${encodedHeader}.${encodedPayload}`;
|
|
142
|
+
|
|
143
|
+
// Get key and sign
|
|
144
|
+
const key: CryptoKey = await this.getSecretKey();
|
|
145
|
+
const signature: ArrayBuffer = await crypto.subtle.sign(
|
|
146
|
+
{ name: 'HMAC' },
|
|
147
|
+
key,
|
|
148
|
+
this.encoder.encode(signatureBase)
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// Encode signature and create token
|
|
152
|
+
const encodedSignature: string = this.base64UrlEncode(signature);
|
|
153
|
+
return `${signatureBase}.${encodedSignature}`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Verify a JWT token and return the decoded payload
|
|
158
|
+
* @param {string} token - The JWT token to verify
|
|
159
|
+
* @returns {Promise<JWTPayload>} - The decoded payload if verification succeeds
|
|
160
|
+
* @throws {Error} - If verification fails
|
|
161
|
+
*/
|
|
162
|
+
public async verify(token: string): Promise<JWTPayload> {
|
|
163
|
+
if (!token || typeof token !== 'string') {
|
|
164
|
+
throw new Error('Token is required and must be a string');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Split token into parts
|
|
168
|
+
const parts: string[] = token.split('.');
|
|
169
|
+
if (parts.length !== 3) {
|
|
170
|
+
throw new Error('Invalid token format');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const [encodedHeader, encodedPayload, encodedSignature] = parts;
|
|
174
|
+
|
|
175
|
+
// Decode header and payload
|
|
176
|
+
try {
|
|
177
|
+
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
178
|
+
const header: JWTHeader = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedHeader)));
|
|
179
|
+
// @ts-expect-error - TS doesn't have a built-in TextDecoder
|
|
180
|
+
const payload: JWTPayload = JSON.parse(this.decoder.decode(this.base64UrlDecode(encodedPayload)));
|
|
181
|
+
|
|
182
|
+
// Check algorithm
|
|
183
|
+
if (header.alg !== 'HS256') {
|
|
184
|
+
throw new Error(`Unsupported algorithm: ${header.alg}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Check expiration
|
|
188
|
+
const now: number = Math.floor(Date.now() / 1000);
|
|
189
|
+
if (payload.exp && payload.exp < now) {
|
|
190
|
+
throw new Error('Token has expired');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Verify signature
|
|
194
|
+
const key: CryptoKey = await this.getSecretKey();
|
|
195
|
+
const signatureBase: string = `${encodedHeader}.${encodedPayload}`;
|
|
196
|
+
const signature: ArrayBuffer = this.base64UrlDecode(encodedSignature) as ArrayBuffer;
|
|
197
|
+
|
|
198
|
+
const isValid: boolean = await crypto.subtle.verify(
|
|
199
|
+
{ name: 'HMAC' },
|
|
200
|
+
key,
|
|
201
|
+
signature,
|
|
202
|
+
this.encoder.encode(signatureBase)
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (!isValid) {
|
|
206
|
+
throw new Error('Invalid signature');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return payload;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error instanceof Error) {
|
|
212
|
+
throw new Error(`Token verification failed: ${error.message}`);
|
|
213
|
+
}
|
|
214
|
+
throw new Error('Token verification failed: Unknown error');
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
package/src/mock.ts
CHANGED
|
@@ -28,18 +28,54 @@ export class MockPartyClient {
|
|
|
28
28
|
}
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
class MockLobby {
|
|
32
|
+
constructor(public server: Server) {}
|
|
33
|
+
|
|
34
|
+
socket() {
|
|
35
|
+
return new MockPartyClient(this.server)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class MockContext {
|
|
40
|
+
parties: {
|
|
41
|
+
main: Map<string, any>
|
|
42
|
+
} = {
|
|
43
|
+
main: new Map()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
constructor(public room: MockPartyRoom, options: any = {}) {
|
|
47
|
+
const parties = options.parties || {}
|
|
48
|
+
for (let lobbyId in parties) {
|
|
49
|
+
this.parties.main.set(lobbyId, new MockLobby(parties[lobbyId](room)))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
31
55
|
class MockPartyRoom {
|
|
32
56
|
clients: Map<string, MockPartyClient> = new Map();
|
|
33
57
|
storage = new Storage();
|
|
58
|
+
context: MockContext;
|
|
34
59
|
env = {}
|
|
35
60
|
|
|
36
|
-
constructor(public id?: string) {
|
|
61
|
+
constructor(public id?: string, options: any = {}) {
|
|
37
62
|
this.id = id || generateShortUUID()
|
|
63
|
+
this.context = new MockContext(this, {
|
|
64
|
+
parties: options.parties || {}
|
|
65
|
+
})
|
|
66
|
+
this.env = options.env || {}
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
async connection(server: Server) {
|
|
41
70
|
const socket = new MockPartyClient(server);
|
|
42
|
-
|
|
71
|
+
const url = new URL('http://localhost')
|
|
72
|
+
const request = new Request(url.toString(), {
|
|
73
|
+
method: 'GET',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json'
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
await server.onConnect(socket.conn as any, { request } as any);
|
|
43
79
|
this.clients.set(socket.id, socket);
|
|
44
80
|
return socket
|
|
45
81
|
}
|
|
@@ -55,7 +91,7 @@ class MockPartyRoom {
|
|
|
55
91
|
}
|
|
56
92
|
|
|
57
93
|
getConnections() {
|
|
58
|
-
return this.clients;
|
|
94
|
+
return Array.from(this.clients.values()).map((client) => client.conn);
|
|
59
95
|
}
|
|
60
96
|
|
|
61
97
|
clear() {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function cors(res: Response, options: CorsOptions = {}) {
|
|
2
|
+
const newHeaders = new Headers(res.headers);
|
|
3
|
+
|
|
4
|
+
// Set default CORS headers
|
|
5
|
+
newHeaders.set('Access-Control-Allow-Origin', options.origin || '*');
|
|
6
|
+
|
|
7
|
+
if (options.credentials) {
|
|
8
|
+
newHeaders.set('Access-Control-Allow-Credentials', 'true');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (options.exposedHeaders && options.exposedHeaders.length) {
|
|
12
|
+
newHeaders.set('Access-Control-Expose-Headers', options.exposedHeaders.join(', '));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Handle preflight requests
|
|
16
|
+
if (options.methods && options.methods.length) {
|
|
17
|
+
newHeaders.set('Access-Control-Allow-Methods', options.methods.join(', '));
|
|
18
|
+
} else {
|
|
19
|
+
newHeaders.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (options.allowedHeaders && options.allowedHeaders.length) {
|
|
23
|
+
newHeaders.set('Access-Control-Allow-Headers', options.allowedHeaders.join(', '));
|
|
24
|
+
} else {
|
|
25
|
+
newHeaders.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (options.maxAge) {
|
|
29
|
+
newHeaders.set('Access-Control-Max-Age', options.maxAge.toString());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return new Response(res.body, {
|
|
33
|
+
status: res.status,
|
|
34
|
+
headers: newHeaders
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CorsOptions {
|
|
39
|
+
origin?: string;
|
|
40
|
+
methods?: string[];
|
|
41
|
+
allowedHeaders?: string[];
|
|
42
|
+
exposedHeaders?: string[];
|
|
43
|
+
credentials?: boolean;
|
|
44
|
+
maxAge?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a CORS interceptor with the specified options
|
|
49
|
+
* @param options CORS configuration options
|
|
50
|
+
* @returns An interceptor function that can be used with ServerResponse
|
|
51
|
+
*/
|
|
52
|
+
export function createCorsInterceptor(options: CorsOptions = {}): (res: Response) => Response {
|
|
53
|
+
return (res: Response) => cors(res, options);
|
|
54
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
export class ServerResponse {
|
|
2
|
+
private interceptors: ((res: Response) => Promise<Response> | Response)[];
|
|
3
|
+
private statusCode: number = 200;
|
|
4
|
+
private responseBody: any = {};
|
|
5
|
+
private responseHeaders: Record<string, string> = {
|
|
6
|
+
'Content-Type': 'application/json'
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new ServerResponse instance
|
|
11
|
+
* @param interceptors Array of interceptor functions that can modify the response
|
|
12
|
+
*/
|
|
13
|
+
constructor(interceptors: ((res: Response) => Promise<Response> | Response)[] = []) {
|
|
14
|
+
this.interceptors = interceptors;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Sets the status code for the response
|
|
19
|
+
* @param code HTTP status code
|
|
20
|
+
* @returns this instance for chaining
|
|
21
|
+
*/
|
|
22
|
+
status(code: number): ServerResponse {
|
|
23
|
+
this.statusCode = code;
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Sets the response body and returns this instance (chainable method)
|
|
29
|
+
*
|
|
30
|
+
* @param body Response body
|
|
31
|
+
* @returns this instance for chaining
|
|
32
|
+
*/
|
|
33
|
+
body(body: any): ServerResponse {
|
|
34
|
+
this.responseBody = body;
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Adds a header to the response
|
|
40
|
+
* @param name Header name
|
|
41
|
+
* @param value Header value
|
|
42
|
+
* @returns this instance for chaining
|
|
43
|
+
*/
|
|
44
|
+
header(name: string, value: string): ServerResponse {
|
|
45
|
+
this.responseHeaders[name] = value;
|
|
46
|
+
return this;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Adds multiple headers to the response
|
|
51
|
+
* @param headers Object containing headers
|
|
52
|
+
* @returns this instance for chaining
|
|
53
|
+
*/
|
|
54
|
+
setHeaders(headers: Record<string, string>): ServerResponse {
|
|
55
|
+
this.responseHeaders = { ...this.responseHeaders, ...headers };
|
|
56
|
+
return this;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Add an interceptor to the chain
|
|
61
|
+
* @param interceptor Function that takes a Response and returns a modified Response
|
|
62
|
+
* @returns this instance for chaining
|
|
63
|
+
*/
|
|
64
|
+
use(interceptor: (res: Response) => Promise<Response> | Response): ServerResponse {
|
|
65
|
+
this.interceptors.push(interceptor);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builds and returns the Response object after applying all interceptors
|
|
71
|
+
* @returns Promise<Response> The final Response object
|
|
72
|
+
* @private Internal method used by terminal methods
|
|
73
|
+
*/
|
|
74
|
+
private async buildResponse(): Promise<Response> {
|
|
75
|
+
// Create initial response
|
|
76
|
+
let response = new Response(JSON.stringify(this.responseBody), {
|
|
77
|
+
status: this.statusCode,
|
|
78
|
+
headers: this.responseHeaders
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Apply all interceptors sequentially
|
|
82
|
+
for (const interceptor of this.interceptors) {
|
|
83
|
+
try {
|
|
84
|
+
const interceptedResponse = interceptor(response);
|
|
85
|
+
if (interceptedResponse instanceof Promise) {
|
|
86
|
+
response = await interceptedResponse;
|
|
87
|
+
} else {
|
|
88
|
+
response = interceptedResponse;
|
|
89
|
+
}
|
|
90
|
+
} catch (error) {
|
|
91
|
+
console.error('Error in interceptor:', error);
|
|
92
|
+
// Continue with the current response if an interceptor fails
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return response;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Sets the response body to the JSON-stringified version of the provided value
|
|
101
|
+
* and sends the response (terminal method)
|
|
102
|
+
*
|
|
103
|
+
* @param body Response body to be JSON stringified
|
|
104
|
+
* @returns Promise<Response> The final Response object
|
|
105
|
+
*/
|
|
106
|
+
async json(body: any): Promise<Response> {
|
|
107
|
+
this.responseBody = body;
|
|
108
|
+
this.responseHeaders['Content-Type'] = 'application/json';
|
|
109
|
+
return this.buildResponse();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sends the response with the current configuration (terminal method)
|
|
114
|
+
*
|
|
115
|
+
* @param body Optional body to set before sending
|
|
116
|
+
* @returns Promise<Response> The final Response object
|
|
117
|
+
*/
|
|
118
|
+
async send(body?: any): Promise<Response> {
|
|
119
|
+
if (body !== undefined) {
|
|
120
|
+
this.responseBody = body;
|
|
121
|
+
}
|
|
122
|
+
return this.buildResponse();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Sends a plain text response (terminal method)
|
|
127
|
+
*
|
|
128
|
+
* @param text Text to send
|
|
129
|
+
* @returns Promise<Response> The final Response object
|
|
130
|
+
*/
|
|
131
|
+
async text(text: string): Promise<Response> {
|
|
132
|
+
this.responseBody = text;
|
|
133
|
+
this.responseHeaders['Content-Type'] = 'text/plain';
|
|
134
|
+
|
|
135
|
+
// Create a text response without JSON stringifying the body
|
|
136
|
+
let response = new Response(text, {
|
|
137
|
+
status: this.statusCode,
|
|
138
|
+
headers: this.responseHeaders
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Apply interceptors
|
|
142
|
+
for (const interceptor of this.interceptors) {
|
|
143
|
+
try {
|
|
144
|
+
const interceptedResponse = interceptor(response);
|
|
145
|
+
if (interceptedResponse instanceof Promise) {
|
|
146
|
+
response = await interceptedResponse;
|
|
147
|
+
} else {
|
|
148
|
+
response = interceptedResponse;
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
console.error('Error in interceptor:', error);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Redirects to the specified URL (terminal method)
|
|
160
|
+
*
|
|
161
|
+
* @param url URL to redirect to
|
|
162
|
+
* @param statusCode HTTP status code (default: 302)
|
|
163
|
+
* @returns Promise<Response> The final Response object
|
|
164
|
+
*/
|
|
165
|
+
async redirect(url: string, statusCode: number = 302): Promise<Response> {
|
|
166
|
+
this.statusCode = statusCode;
|
|
167
|
+
this.responseHeaders['Location'] = url;
|
|
168
|
+
return this.buildResponse();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Creates a success response with status 200
|
|
173
|
+
* @param body Response body
|
|
174
|
+
* @returns Promise<Response> The final Response object
|
|
175
|
+
*/
|
|
176
|
+
async success(body: any = {}): Promise<Response> {
|
|
177
|
+
return this.status(200).json(body);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Creates an error response with status 400
|
|
182
|
+
* @param message Error message
|
|
183
|
+
* @param details Additional error details
|
|
184
|
+
* @returns Promise<Response> The final Response object
|
|
185
|
+
*/
|
|
186
|
+
async badRequest(message: string, details: any = {}): Promise<Response> {
|
|
187
|
+
return this.status(400).json({
|
|
188
|
+
error: message,
|
|
189
|
+
...details
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Creates an error response with status 403
|
|
195
|
+
* @param message Error message
|
|
196
|
+
* @returns Promise<Response> The final Response object
|
|
197
|
+
*/
|
|
198
|
+
async notPermitted(message: string = "Not permitted"): Promise<Response> {
|
|
199
|
+
return this.status(403).json({ error: message });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Creates an error response with status 401
|
|
204
|
+
* @param message Error message
|
|
205
|
+
* @returns Promise<Response> The final Response object
|
|
206
|
+
*/
|
|
207
|
+
async unauthorized(message: string = "Unauthorized"): Promise<Response> {
|
|
208
|
+
return this.status(401).json({ error: message });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Creates an error response with status 404
|
|
213
|
+
* @param message Error message
|
|
214
|
+
* @returns Promise<Response> The final Response object
|
|
215
|
+
*/
|
|
216
|
+
async notFound(message: string = "Not found"): Promise<Response> {
|
|
217
|
+
return this.status(404).json({ error: message });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Creates an error response with status 500
|
|
222
|
+
* @param message Error message
|
|
223
|
+
* @returns Promise<Response> The final Response object
|
|
224
|
+
*/
|
|
225
|
+
async serverError(message: string = "Internal Server Error"): Promise<Response> {
|
|
226
|
+
return this.status(500).json({ error: message });
|
|
227
|
+
}
|
|
228
|
+
}
|