hapta 1.0.11 → 1.0.13
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/index.ts +103 -83
- package/package.json +1 -1
package/index.ts
CHANGED
|
@@ -10,7 +10,7 @@ import jwt from "jsonwebtoken";
|
|
|
10
10
|
import crypto from "crypto";
|
|
11
11
|
import * as z from "zod";
|
|
12
12
|
import { watch } from "fs";
|
|
13
|
-
import fs from "fs"
|
|
13
|
+
import fs from "fs"
|
|
14
14
|
import { pathToFileURL } from "url";
|
|
15
15
|
import { MemoryState } from "./src/core/MemoryState";
|
|
16
16
|
|
|
@@ -37,9 +37,9 @@ if (!config.Database?.url || !config.Database?.Admin_Password || !config.Databas
|
|
|
37
37
|
|
|
38
38
|
// --- Pocketbase Connection ---
|
|
39
39
|
export const pb = new Pocketbase(config.Database?.url || "");
|
|
40
|
+
pb.autoCancellation(false)
|
|
40
41
|
try {
|
|
41
|
-
await pb.collection("_superusers").authWithPassword(config.Database?.Admin_Email || "", config.Database?.Admin_Password || "");
|
|
42
|
-
pb.autoCancellation(false)
|
|
42
|
+
await pb.collection("_superusers").authWithPassword(config.Database?.Admin_Email || "", config.Database?.Admin_Password || "");
|
|
43
43
|
} catch (_) { }
|
|
44
44
|
|
|
45
45
|
const cache = new Cache();
|
|
@@ -88,7 +88,7 @@ function isOriginAllowed(origin: string | null, corsConfig: CorsConfig): boolean
|
|
|
88
88
|
// Build CORS headers
|
|
89
89
|
function buildCorsHeaders(origin: string | null, corsConfig: CorsConfig): Record<string, string> {
|
|
90
90
|
const headers: Record<string, string> = {};
|
|
91
|
-
|
|
91
|
+
|
|
92
92
|
if (origin && isOriginAllowed(origin, corsConfig)) {
|
|
93
93
|
headers["Access-Control-Allow-Origin"] = origin;
|
|
94
94
|
} else if (corsConfig.origins === "*") {
|
|
@@ -97,20 +97,20 @@ function buildCorsHeaders(origin: string | null, corsConfig: CorsConfig): Record
|
|
|
97
97
|
// Use first origin as fallback
|
|
98
98
|
headers["Access-Control-Allow-Origin"] = corsConfig.origins[0];
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
|
|
101
101
|
if (corsConfig.credentials) {
|
|
102
102
|
headers["Access-Control-Allow-Credentials"] = "true";
|
|
103
103
|
}
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
headers["Access-Control-Allow-Methods"] = corsConfig.methods.join(", ");
|
|
106
106
|
headers["Access-Control-Allow-Headers"] = corsConfig.allowedHeaders.join(", ");
|
|
107
|
-
|
|
107
|
+
|
|
108
108
|
if (corsConfig.exposedHeaders.length > 0) {
|
|
109
109
|
headers["Access-Control-Expose-Headers"] = corsConfig.exposedHeaders.join(", ");
|
|
110
110
|
}
|
|
111
|
-
|
|
111
|
+
|
|
112
112
|
headers["Access-Control-Max-Age"] = corsConfig.maxAge.toString();
|
|
113
|
-
|
|
113
|
+
|
|
114
114
|
return headers;
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -125,7 +125,7 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
125
125
|
const schemas = new Map();
|
|
126
126
|
const middlewares = new Map<string, (ctx: Context) => Promise<Response | boolean>>();
|
|
127
127
|
|
|
128
|
-
const reimportModule = async (modulePath: string) => {
|
|
128
|
+
const reimportModule = async (modulePath: string) => {
|
|
129
129
|
const resolvedPath = path.resolve(modulePath);
|
|
130
130
|
if (require.cache[resolvedPath]) {
|
|
131
131
|
delete require.cache[resolvedPath];
|
|
@@ -138,7 +138,7 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
138
138
|
for (const [pathname, routePath] of Object.entries(router.routes)) {
|
|
139
139
|
try {
|
|
140
140
|
const routeModule = await reimportModule(routePath as string);
|
|
141
|
-
|
|
141
|
+
|
|
142
142
|
// Store the entire module object for method-based routing
|
|
143
143
|
routeHandlers.set(pathname, routeModule.default || routeModule);
|
|
144
144
|
|
|
@@ -155,7 +155,7 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
155
155
|
const middleware = await reimportModule(middlewarePath);
|
|
156
156
|
if (middleware.default) middlewares.set(pathname, middleware.default);
|
|
157
157
|
}
|
|
158
|
-
} catch (e) {
|
|
158
|
+
} catch (e) {
|
|
159
159
|
console.error(`❌ Error loading module for route ${pathname}:`, e);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
@@ -178,13 +178,13 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
|
|
182
182
|
const routeMatch = router.match(url.pathname)
|
|
183
183
|
|
|
184
184
|
if (!routeMatch) {
|
|
185
|
-
return new Response("404 Not Found", {
|
|
185
|
+
return new Response("404 Not Found", {
|
|
186
186
|
status: 404,
|
|
187
|
-
headers: corsHeaders
|
|
187
|
+
headers: corsHeaders
|
|
188
188
|
});
|
|
189
189
|
}
|
|
190
190
|
|
|
@@ -193,15 +193,15 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
193
193
|
const schema = schemas.get(routeMatch.name);
|
|
194
194
|
|
|
195
195
|
if (!routeHandler) {
|
|
196
|
-
return new Response(`Route handler for ${routeMatch.name} not found.`, {
|
|
196
|
+
return new Response(`Route handler for ${routeMatch.name} not found.`, {
|
|
197
197
|
status: 404,
|
|
198
|
-
headers: corsHeaders
|
|
198
|
+
headers: corsHeaders
|
|
199
199
|
});
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
try {
|
|
203
203
|
const context = await buildRequestContext(req, req.headers);
|
|
204
|
-
context.metadata.params = routeMatch.params;
|
|
204
|
+
context.metadata.params = routeMatch.params;
|
|
205
205
|
context.metadata.query = Object.fromEntries(url.searchParams.entries());
|
|
206
206
|
context.metadata.headers = Object.fromEntries(req.headers.entries());
|
|
207
207
|
|
|
@@ -220,9 +220,9 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
220
220
|
headers: responseHeaders,
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
-
if (result === false) return new Response("Forbidden", {
|
|
223
|
+
if (result === false) return new Response("Forbidden", {
|
|
224
224
|
status: 403,
|
|
225
|
-
headers: corsHeaders
|
|
225
|
+
headers: corsHeaders
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
|
|
@@ -233,7 +233,7 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
233
233
|
query: schema.query?.safeParse(context.metadata.query),
|
|
234
234
|
headers: schema.headers?.safeParse(context.metadata.headers),
|
|
235
235
|
};
|
|
236
|
-
|
|
236
|
+
|
|
237
237
|
for (const key of ["body", "query", "headers"] as const) {
|
|
238
238
|
if (validation[key] && !validation[key]?.success) {
|
|
239
239
|
routeHandler.onError && routeHandler.onError(req, {
|
|
@@ -242,16 +242,16 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
242
242
|
details: validation[key]?.error.flatten(),
|
|
243
243
|
});
|
|
244
244
|
return new Response(
|
|
245
|
-
JSON.stringify({
|
|
246
|
-
success: false,
|
|
247
|
-
error: validation[key]?.error.flatten()
|
|
248
|
-
}),
|
|
249
|
-
{
|
|
250
|
-
status: 400,
|
|
245
|
+
JSON.stringify({
|
|
246
|
+
success: false,
|
|
247
|
+
data: {error: validation[key]?.error.flatten()}
|
|
248
|
+
}),
|
|
249
|
+
{
|
|
250
|
+
status: 400,
|
|
251
251
|
headers: {
|
|
252
252
|
"Content-Type": "application/json",
|
|
253
253
|
...corsHeaders
|
|
254
|
-
}
|
|
254
|
+
}
|
|
255
255
|
}
|
|
256
256
|
);
|
|
257
257
|
}
|
|
@@ -261,11 +261,11 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
261
261
|
// Get method-specific handler
|
|
262
262
|
const methodHandler = routeHandler[method] || routeHandler.default;
|
|
263
263
|
console.log(`➡️ ${method} ${url.pathname} -> ${routeMatch.name}`);
|
|
264
|
-
|
|
264
|
+
|
|
265
265
|
if (!methodHandler) {
|
|
266
266
|
return new Response(
|
|
267
|
-
`Method ${method} not allowed for ${routeMatch.name}`,
|
|
268
|
-
{
|
|
267
|
+
`Method ${method} not allowed for ${routeMatch.name}`,
|
|
268
|
+
{
|
|
269
269
|
status: 405,
|
|
270
270
|
headers: {
|
|
271
271
|
"Allow": Object.keys(routeHandler)
|
|
@@ -279,7 +279,7 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
279
279
|
|
|
280
280
|
// Execute the route handler
|
|
281
281
|
const response = await methodHandler(context, db);
|
|
282
|
-
|
|
282
|
+
|
|
283
283
|
// Add CORS headers to the response
|
|
284
284
|
if (response instanceof Response) {
|
|
285
285
|
const responseHeaders = new Headers(response.headers);
|
|
@@ -292,11 +292,11 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
292
292
|
headers: responseHeaders,
|
|
293
293
|
});
|
|
294
294
|
}
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
return response;
|
|
297
297
|
} catch (err) {
|
|
298
298
|
console.error(`❌ Error handling ${url.pathname}:`, err);
|
|
299
|
-
|
|
299
|
+
|
|
300
300
|
// Check if route handler has custom error handler
|
|
301
301
|
if (routeHandler.onError) {
|
|
302
302
|
try {
|
|
@@ -317,10 +317,10 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
317
317
|
console.error(`❌ Error handler also failed:`, errorHandlerErr);
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
|
-
|
|
321
|
-
return new Response("Internal Server Error", {
|
|
320
|
+
|
|
321
|
+
return new Response("Internal Server Error", {
|
|
322
322
|
status: 500,
|
|
323
|
-
headers: corsHeaders
|
|
323
|
+
headers: corsHeaders
|
|
324
324
|
});
|
|
325
325
|
}
|
|
326
326
|
},
|
|
@@ -328,9 +328,9 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
328
328
|
const url = new URL(error.request.url);
|
|
329
329
|
const origin = error.request.headers.get("origin");
|
|
330
330
|
const corsHeaders = buildCorsHeaders(origin, defaultCorsConfig);
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
const routeMatch = router.match(url.href);
|
|
333
|
-
|
|
333
|
+
|
|
334
334
|
if (routeMatch) {
|
|
335
335
|
const routeHandler = routeHandlers.get(routeMatch.name);
|
|
336
336
|
if (routeHandler && typeof routeHandler.onError === "function") {
|
|
@@ -352,10 +352,10 @@ async function createServeConfig(): Promise<Serve> {
|
|
|
352
352
|
}
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
-
|
|
356
|
-
return new Response("Something went wrong!", {
|
|
355
|
+
|
|
356
|
+
return new Response("Something went wrong!", {
|
|
357
357
|
status: 500,
|
|
358
|
-
headers: corsHeaders
|
|
358
|
+
headers: corsHeaders
|
|
359
359
|
});
|
|
360
360
|
},
|
|
361
361
|
};
|
|
@@ -402,49 +402,69 @@ function validateToken(token: string) {
|
|
|
402
402
|
}
|
|
403
403
|
}
|
|
404
404
|
async function Register(data: { username: string; email: string; password: string }) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
405
|
+
try {
|
|
406
|
+
data.passwordConfirm = data.password
|
|
407
|
+
const response = await pb.collection("users").create(data);
|
|
408
|
+
return { success: true, data: response, status: 200 };
|
|
409
|
+
} catch (err: any) {
|
|
410
|
+
const fieldErrors: Record<string, { code: string; message: string }> = {};
|
|
411
|
+
|
|
412
|
+
if (err?.data) {
|
|
413
|
+
for (const key in err.data) {
|
|
414
|
+
let value = err.data[key];
|
|
415
|
+
|
|
416
|
+
// If value is a stringified JSON, parse it
|
|
417
|
+
if (typeof value === "string" && value.startsWith("{")) {
|
|
418
|
+
try {
|
|
419
|
+
value = JSON.parse(value);
|
|
420
|
+
} catch (_) {
|
|
421
|
+
// fallback: leave as string
|
|
422
|
+
}
|
|
423
|
+
}
|
|
409
424
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
425
|
+
// If value is now an object of field errors
|
|
426
|
+
if (typeof value === "object" && !Array.isArray(value)) {
|
|
427
|
+
for (const field in value) {
|
|
428
|
+
const msg = value[field];
|
|
429
|
+
fieldErrors[field] = {
|
|
430
|
+
code: msg.code || "validation_error",
|
|
431
|
+
message: msg.message || String(msg),
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
} else if (Array.isArray(value)) {
|
|
435
|
+
fieldErrors[key] = {
|
|
436
|
+
code: "validation_error",
|
|
437
|
+
message: value.join(" "),
|
|
438
|
+
};
|
|
439
|
+
} else {
|
|
440
|
+
fieldErrors[key] = {
|
|
441
|
+
code: "validation_error",
|
|
442
|
+
message: String(value),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
success: false,
|
|
450
|
+
data: {
|
|
451
|
+
error: {
|
|
452
|
+
fieldErrors,
|
|
453
|
+
formErrors: [err?.message || "Failed to create record."],
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
status: err?.status || 400,
|
|
427
457
|
};
|
|
428
|
-
}
|
|
429
458
|
}
|
|
430
|
-
|
|
431
|
-
return {
|
|
432
|
-
success: false,
|
|
433
|
-
error: {
|
|
434
|
-
fieldErrors,
|
|
435
|
-
formErrors: [error?.message || "Failed to create record."],
|
|
436
|
-
},
|
|
437
|
-
status: error?.status || 400,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
459
|
}
|
|
441
460
|
|
|
461
|
+
|
|
442
462
|
async function buildRequestContext(req: Request, headers: Headers): Promise<Context> {
|
|
443
463
|
const reqJSON = await req.json().catch(() => ({}));
|
|
444
464
|
|
|
445
465
|
const authHeader = headers.get("Authorization");
|
|
446
466
|
const token = authHeader?.startsWith("Bearer ") ? authHeader.split(" ")[1] : null;
|
|
447
|
-
|
|
467
|
+
|
|
448
468
|
let isAuthenticated = false;
|
|
449
469
|
let principal: any = { isAuthenticated };
|
|
450
470
|
|
|
@@ -462,22 +482,22 @@ async function buildRequestContext(req: Request, headers: Headers): Promise<Cont
|
|
|
462
482
|
principal: { ...principal, isAuthenticated },
|
|
463
483
|
services: {
|
|
464
484
|
async Authenticate(options) {
|
|
465
|
-
const
|
|
485
|
+
const { type } = options;
|
|
466
486
|
if (type === "passwordAuth") {
|
|
467
|
-
const { emailOrUsername, password } = options
|
|
468
|
-
const login
|
|
487
|
+
const { emailOrUsername, password } = options
|
|
488
|
+
const login = await pb.collection("users").authWithPassword(emailOrUsername, password) as unknown as Promise<{ record: any }>;
|
|
469
489
|
// make a token
|
|
470
|
-
const token = jwt.sign({ id: (await login).record.id, username: (await login).record.username }
|
|
471
|
-
|
|
490
|
+
const token = jwt.sign({ id: (await login).record.id, username: (await login).record.username }, config.Server.JWT_Secret, { expiresIn: '1h' });
|
|
491
|
+
|
|
472
492
|
if (login) {
|
|
473
493
|
return { isAuthenticated: true, token, ...(await login).record };
|
|
474
494
|
} else {
|
|
475
495
|
return { isAuthenticated: false };
|
|
476
496
|
}
|
|
477
497
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
498
|
+
},
|
|
499
|
+
Register,
|
|
500
|
+
|
|
481
501
|
},
|
|
482
502
|
metadata: {
|
|
483
503
|
requestID: crypto.randomUUID(),
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"bin": {
|
|
4
4
|
"hapta": "./index.ts"
|
|
5
5
|
},
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.13",
|
|
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
9
|
"jsonwebtoken": "^9.0.3",
|