hapta 1.0.5 → 1.0.7
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/bun.lock +7 -4
- package/index.ts +178 -30
- package/package.json +4 -3
- package/src/core/config.ts +1 -1
package/bun.lock
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
"workspaces": {
|
|
5
5
|
"": {
|
|
6
6
|
"dependencies": {
|
|
7
|
+
"hapta": "^1.0.5",
|
|
7
8
|
"jsonwebtoken": "^9.0.2",
|
|
8
|
-
"pocketbase": "
|
|
9
|
-
"zod": "
|
|
9
|
+
"pocketbase": "latest",
|
|
10
|
+
"zod": "latest",
|
|
10
11
|
},
|
|
11
12
|
},
|
|
12
13
|
},
|
|
@@ -15,6 +16,8 @@
|
|
|
15
16
|
|
|
16
17
|
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
|
|
17
18
|
|
|
19
|
+
"hapta": ["hapta@1.0.5", "", { "dependencies": { "jsonwebtoken": "^9.0.2", "pocketbase": "latest", "zod": "latest" }, "bin": { "hapta": "index.ts" } }, "sha512-yvow9I/Vbnusn24clSeMUtIo036ImE/i3p5a6XsWDJgUDbl3xQkxY5+/VrkyQC7zwgQiUK5XluZPn2HkMFk9gg=="],
|
|
20
|
+
|
|
18
21
|
"jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="],
|
|
19
22
|
|
|
20
23
|
"jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="],
|
|
@@ -37,12 +40,12 @@
|
|
|
37
40
|
|
|
38
41
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
39
42
|
|
|
40
|
-
"pocketbase": ["pocketbase@0.26.
|
|
43
|
+
"pocketbase": ["pocketbase@0.26.8", "", {}, "sha512-aQ/ewvS7ncvAE8wxoW10iAZu6ElgbeFpBhKPnCfvRovNzm2gW8u/sQNPGN6vNgVEagz44kK//C61oKjfa+7Low=="],
|
|
41
44
|
|
|
42
45
|
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
|
43
46
|
|
|
44
47
|
"semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
|
|
45
48
|
|
|
46
|
-
"zod": ["zod@4.3.
|
|
49
|
+
"zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="],
|
|
47
50
|
}
|
|
48
51
|
}
|
package/index.ts
CHANGED
|
@@ -52,7 +52,65 @@ interface RouteHandler {
|
|
|
52
52
|
PATCH?: (ctx: Context, db: DatabaseService) => Promise<Response>;
|
|
53
53
|
DELETE?: (ctx: Context, db: DatabaseService) => Promise<Response>;
|
|
54
54
|
onError?: (request: Request, error: any) => Promise<Response> | Response;
|
|
55
|
-
[key: string]: any;
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- CORS Configuration ---
|
|
59
|
+
interface CorsConfig {
|
|
60
|
+
origins: string[] | "*";
|
|
61
|
+
methods: string[];
|
|
62
|
+
allowedHeaders: string[];
|
|
63
|
+
exposedHeaders: string[];
|
|
64
|
+
credentials: boolean;
|
|
65
|
+
maxAge: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const defaultCorsConfig: CorsConfig = {
|
|
69
|
+
origins: config.Server.origin === "*" ? "*" : [config.Server.origin],
|
|
70
|
+
methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
|
|
71
|
+
allowedHeaders: ["Content-Type", "Authorization", "X-Requested-With", "Accept", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers"],
|
|
72
|
+
exposedHeaders: ["Content-Range", "X-Content-Range"],
|
|
73
|
+
credentials: true,
|
|
74
|
+
maxAge: 86400,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Helper to check if origin is allowed
|
|
78
|
+
function isOriginAllowed(origin: string | null, corsConfig: CorsConfig): boolean {
|
|
79
|
+
if (!origin) return false;
|
|
80
|
+
if (corsConfig.origins === "*") return true;
|
|
81
|
+
if (Array.isArray(corsConfig.origins)) {
|
|
82
|
+
return corsConfig.origins.includes(origin);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Build CORS headers
|
|
88
|
+
function buildCorsHeaders(origin: string | null, corsConfig: CorsConfig): Record<string, string> {
|
|
89
|
+
const headers: Record<string, string> = {};
|
|
90
|
+
|
|
91
|
+
if (origin && isOriginAllowed(origin, corsConfig)) {
|
|
92
|
+
headers["Access-Control-Allow-Origin"] = origin;
|
|
93
|
+
} else if (corsConfig.origins === "*") {
|
|
94
|
+
headers["Access-Control-Allow-Origin"] = "*";
|
|
95
|
+
} else if (Array.isArray(corsConfig.origins) && corsConfig.origins.length > 0) {
|
|
96
|
+
// Use first origin as fallback
|
|
97
|
+
headers["Access-Control-Allow-Origin"] = corsConfig.origins[0];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (corsConfig.credentials) {
|
|
101
|
+
headers["Access-Control-Allow-Credentials"] = "true";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
headers["Access-Control-Allow-Methods"] = corsConfig.methods.join(", ");
|
|
105
|
+
headers["Access-Control-Allow-Headers"] = corsConfig.allowedHeaders.join(", ");
|
|
106
|
+
|
|
107
|
+
if (corsConfig.exposedHeaders.length > 0) {
|
|
108
|
+
headers["Access-Control-Expose-Headers"] = corsConfig.exposedHeaders.join(", ");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
headers["Access-Control-Max-Age"] = corsConfig.maxAge.toString();
|
|
112
|
+
|
|
113
|
+
return headers;
|
|
56
114
|
}
|
|
57
115
|
|
|
58
116
|
// --- Server Configuration Builder ---
|
|
@@ -106,24 +164,25 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
106
164
|
return {
|
|
107
165
|
port: config.port,
|
|
108
166
|
async fetch(req: Request) {
|
|
167
|
+
const url = new URL(req.url);
|
|
168
|
+
const origin = req.headers.get("origin");
|
|
169
|
+
const corsHeaders = buildCorsHeaders(origin, defaultCorsConfig);
|
|
170
|
+
|
|
109
171
|
// Handle CORS preflight
|
|
110
172
|
if (req.method === "OPTIONS") {
|
|
111
173
|
return new Response(null, {
|
|
112
174
|
status: 204,
|
|
113
|
-
headers:
|
|
114
|
-
"Access-Control-Allow-Origin": config.origin,
|
|
115
|
-
"Access-Control-Allow-Methods": "OPTIONS, GET, POST, PUT, PATCH, DELETE",
|
|
116
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
|
117
|
-
"Access-Control-Max-Age": "86400",
|
|
118
|
-
},
|
|
175
|
+
headers: corsHeaders,
|
|
119
176
|
});
|
|
120
177
|
}
|
|
121
178
|
|
|
122
|
-
const url = new URL(req.url);
|
|
123
179
|
const routeMatch = router.match(url.href);
|
|
124
180
|
|
|
125
181
|
if (!routeMatch) {
|
|
126
|
-
return new Response("404 Not Found", {
|
|
182
|
+
return new Response("404 Not Found", {
|
|
183
|
+
status: 404,
|
|
184
|
+
headers: corsHeaders
|
|
185
|
+
});
|
|
127
186
|
}
|
|
128
187
|
|
|
129
188
|
const routeHandler = routeHandlers.get(routeMatch.name);
|
|
@@ -131,7 +190,10 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
131
190
|
const schema = schemas.get(routeMatch.name);
|
|
132
191
|
|
|
133
192
|
if (!routeHandler) {
|
|
134
|
-
return new Response(`Route handler for ${routeMatch.name} not found.`, {
|
|
193
|
+
return new Response(`Route handler for ${routeMatch.name} not found.`, {
|
|
194
|
+
status: 404,
|
|
195
|
+
headers: corsHeaders
|
|
196
|
+
});
|
|
135
197
|
}
|
|
136
198
|
|
|
137
199
|
try {
|
|
@@ -143,8 +205,22 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
143
205
|
// Run middleware if exists
|
|
144
206
|
if (middleware) {
|
|
145
207
|
const result = await middleware(context);
|
|
146
|
-
if (result instanceof Response)
|
|
147
|
-
|
|
208
|
+
if (result instanceof Response) {
|
|
209
|
+
// Add CORS headers to middleware response
|
|
210
|
+
const responseHeaders = new Headers(result.headers);
|
|
211
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
212
|
+
responseHeaders.set(key, value);
|
|
213
|
+
}
|
|
214
|
+
return new Response(result.body, {
|
|
215
|
+
status: result.status,
|
|
216
|
+
statusText: result.statusText,
|
|
217
|
+
headers: responseHeaders,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (result === false) return new Response("Forbidden", {
|
|
221
|
+
status: 403,
|
|
222
|
+
headers: corsHeaders
|
|
223
|
+
});
|
|
148
224
|
}
|
|
149
225
|
|
|
150
226
|
// Schema validation
|
|
@@ -157,6 +233,11 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
157
233
|
|
|
158
234
|
for (const key of ["body", "query", "headers"] as const) {
|
|
159
235
|
if (validation[key] && !validation[key]?.success) {
|
|
236
|
+
routeHandler.onError && routeHandler.onError(req, {
|
|
237
|
+
name: "ValidationError",
|
|
238
|
+
message: `Invalid ${key} format`,
|
|
239
|
+
details: validation[key]?.error.flatten(),
|
|
240
|
+
});
|
|
160
241
|
return new Response(
|
|
161
242
|
JSON.stringify({
|
|
162
243
|
success: false,
|
|
@@ -164,7 +245,10 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
164
245
|
}),
|
|
165
246
|
{
|
|
166
247
|
status: 400,
|
|
167
|
-
headers: {
|
|
248
|
+
headers: {
|
|
249
|
+
"Content-Type": "application/json",
|
|
250
|
+
...corsHeaders
|
|
251
|
+
}
|
|
168
252
|
}
|
|
169
253
|
);
|
|
170
254
|
}
|
|
@@ -174,49 +258,103 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
174
258
|
// Get method-specific handler
|
|
175
259
|
const method = req.method.toUpperCase();
|
|
176
260
|
const methodHandler = routeHandler[method] || routeHandler.default;
|
|
261
|
+
console.log(`➡️ ${method} ${url.pathname} -> ${routeMatch.name}`);
|
|
177
262
|
|
|
178
263
|
if (!methodHandler) {
|
|
179
264
|
return new Response(
|
|
180
265
|
`Method ${method} not allowed for ${routeMatch.name}`,
|
|
181
|
-
{
|
|
266
|
+
{
|
|
267
|
+
status: 405,
|
|
268
|
+
headers: {
|
|
269
|
+
"Allow": Object.keys(routeHandler)
|
|
270
|
+
.filter(key => typeof routeHandler[key] === 'function')
|
|
271
|
+
.join(", "),
|
|
272
|
+
...corsHeaders
|
|
273
|
+
}
|
|
274
|
+
}
|
|
182
275
|
);
|
|
183
276
|
}
|
|
184
277
|
|
|
185
278
|
// Execute the route handler
|
|
186
|
-
|
|
279
|
+
const response = await methodHandler(context, db);
|
|
280
|
+
|
|
281
|
+
// Add CORS headers to the response
|
|
282
|
+
if (response instanceof Response) {
|
|
283
|
+
const responseHeaders = new Headers(response.headers);
|
|
284
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
285
|
+
responseHeaders.set(key, value);
|
|
286
|
+
}
|
|
287
|
+
return new Response(response.body, {
|
|
288
|
+
status: response.status,
|
|
289
|
+
statusText: response.statusText,
|
|
290
|
+
headers: responseHeaders,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return response;
|
|
187
295
|
} catch (err) {
|
|
188
296
|
console.error(`❌ Error handling ${url.pathname}:`, err);
|
|
189
297
|
|
|
190
298
|
// Check if route handler has custom error handler
|
|
191
299
|
if (routeHandler.onError) {
|
|
192
300
|
try {
|
|
193
|
-
|
|
301
|
+
const errorResponse = await routeHandler.onError(req, err);
|
|
302
|
+
// Add CORS headers to error response
|
|
303
|
+
if (errorResponse instanceof Response) {
|
|
304
|
+
const responseHeaders = new Headers(errorResponse.headers);
|
|
305
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
306
|
+
responseHeaders.set(key, value);
|
|
307
|
+
}
|
|
308
|
+
return new Response(errorResponse.body, {
|
|
309
|
+
status: errorResponse.status,
|
|
310
|
+
statusText: errorResponse.statusText,
|
|
311
|
+
headers: responseHeaders,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
194
314
|
} catch (errorHandlerErr) {
|
|
195
315
|
console.error(`❌ Error handler also failed:`, errorHandlerErr);
|
|
196
316
|
}
|
|
197
317
|
}
|
|
198
318
|
|
|
199
|
-
return new Response("Internal Server Error", {
|
|
319
|
+
return new Response("Internal Server Error", {
|
|
320
|
+
status: 500,
|
|
321
|
+
headers: corsHeaders
|
|
322
|
+
});
|
|
200
323
|
}
|
|
201
324
|
},
|
|
202
325
|
error(error) {
|
|
203
326
|
const url = new URL(error.request.url);
|
|
327
|
+
const origin = error.request.headers.get("origin");
|
|
328
|
+
const corsHeaders = buildCorsHeaders(origin, defaultCorsConfig);
|
|
329
|
+
|
|
204
330
|
const routeMatch = router.match(url.href);
|
|
205
331
|
|
|
206
332
|
if (routeMatch) {
|
|
207
333
|
const routeHandler = routeHandlers.get(routeMatch.name);
|
|
208
|
-
// Use the route's onError if available
|
|
209
334
|
if (routeHandler && typeof routeHandler.onError === "function") {
|
|
210
335
|
try {
|
|
211
|
-
|
|
336
|
+
const errorResponse = routeHandler.onError(error.request, error);
|
|
337
|
+
if (errorResponse instanceof Response) {
|
|
338
|
+
const responseHeaders = new Headers(errorResponse.headers);
|
|
339
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
340
|
+
responseHeaders.set(key, value);
|
|
341
|
+
}
|
|
342
|
+
return new Response(errorResponse.body, {
|
|
343
|
+
status: errorResponse.status,
|
|
344
|
+
statusText: errorResponse.statusText,
|
|
345
|
+
headers: responseHeaders,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
212
348
|
} catch (e) {
|
|
213
349
|
console.error("❌ Route error handler failed:", e);
|
|
214
350
|
}
|
|
215
351
|
}
|
|
216
352
|
}
|
|
217
353
|
|
|
218
|
-
|
|
219
|
-
|
|
354
|
+
return new Response("Something went wrong!", {
|
|
355
|
+
status: 500,
|
|
356
|
+
headers: corsHeaders
|
|
357
|
+
});
|
|
220
358
|
},
|
|
221
359
|
};
|
|
222
360
|
}
|
|
@@ -283,7 +421,23 @@ async function buildRequestContext(req: Request, headers: Headers): Promise<Cont
|
|
|
283
421
|
|
|
284
422
|
return {
|
|
285
423
|
principal: { ...principal, isAuthenticated },
|
|
286
|
-
services: {
|
|
424
|
+
services: {
|
|
425
|
+
async Authenticate(options) {
|
|
426
|
+
const { type } = options;
|
|
427
|
+
if (type === "passwordAuth") {
|
|
428
|
+
const { emailOrUsername, password } = options
|
|
429
|
+
const login = await pb.collection("users").authWithPassword(emailOrUsername, password) as unknown as Promise<{ record: any }>;
|
|
430
|
+
// make a token
|
|
431
|
+
const token = jwt.sign({ id: (await login).record.id, username: (await login).record.username } , config.Server.JWT_Secret, { expiresIn: '1h' });
|
|
432
|
+
|
|
433
|
+
if (login) {
|
|
434
|
+
return { isAuthenticated: true, token, ...(await login).record };
|
|
435
|
+
} else {
|
|
436
|
+
return { isAuthenticated: false };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
},
|
|
287
441
|
metadata: {
|
|
288
442
|
requestID: crypto.randomUUID(),
|
|
289
443
|
timestamp: new Date(),
|
|
@@ -296,11 +450,6 @@ async function buildRequestContext(req: Request, headers: Headers): Promise<Cont
|
|
|
296
450
|
return Response.json(value, {
|
|
297
451
|
...(status && { status }),
|
|
298
452
|
...(statusText && { statusText }),
|
|
299
|
-
headers: {
|
|
300
|
-
"Access-Control-Allow-Origin": config.origin,
|
|
301
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
302
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
303
|
-
}
|
|
304
453
|
});
|
|
305
454
|
},
|
|
306
455
|
html: (value: string, status?: number, statusText?: string) => {
|
|
@@ -309,9 +458,6 @@ async function buildRequestContext(req: Request, headers: Headers): Promise<Cont
|
|
|
309
458
|
...(statusText && { statusText }),
|
|
310
459
|
headers: {
|
|
311
460
|
"Content-Type": "text/html",
|
|
312
|
-
"Access-Control-Allow-Origin": config.origin,
|
|
313
|
-
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
314
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization"
|
|
315
461
|
}
|
|
316
462
|
});
|
|
317
463
|
}
|
|
@@ -321,6 +467,8 @@ async function buildRequestContext(req: Request, headers: Headers): Promise<Cont
|
|
|
321
467
|
// --- Initial Server Start ---
|
|
322
468
|
server = Bun.serve(await createServeConfig());
|
|
323
469
|
console.log(`🚀 Hapta listening at http://localhost:${server.port}`);
|
|
470
|
+
console.log(`🌐 CORS configured for origins: ${defaultCorsConfig.origins === "*" ? "* (all)" : defaultCorsConfig.origins.join(", ")}`);
|
|
471
|
+
console.log(`🔧 Allowed methods: ${defaultCorsConfig.methods.join(", ")}`);
|
|
324
472
|
watchFiles();
|
|
325
473
|
|
|
326
474
|
const Hapta = {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hapta",
|
|
3
3
|
"bin": {
|
|
4
|
-
"hapta":"./index.ts"
|
|
4
|
+
"hapta": "./index.ts"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.7",
|
|
7
7
|
"description": "modular, scalable, and feature-rich backend framework designed to extend Pocketbase with authentication, schema validation, caching, and tenant-based service orchestration.",
|
|
8
8
|
"dependencies": {
|
|
9
|
+
"hapta": "^1.0.5",
|
|
9
10
|
"jsonwebtoken": "^9.0.2",
|
|
10
11
|
"pocketbase": "latest",
|
|
11
12
|
"zod": "latest"
|
|
12
13
|
}
|
|
13
|
-
}
|
|
14
|
+
}
|