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 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": "^0.26.1",
9
- "zod": "^4.0.5",
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.5", "", {}, "sha512-SXcq+sRvVpNxfLxPB1C+8eRatL7ZY4o3EVl/0OdE3MeR9fhPyZt0nmmxLqYmkLvXCN9qp3lXWV/0EUYb3MmMXQ=="],
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.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
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; // Allow for other properties
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", { status: 404 });
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.`, { status: 404 });
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) return result;
147
- if (result === false) return new Response("Forbidden", { status: 403 });
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: { "Content-Type": "application/json" }
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
- { status: 405 }
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
- return await methodHandler(context, db);
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
- return await routeHandler.onError(req, err);
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", { status: 500 });
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
- return routeHandler.onError(error.request, error);
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
- // Default error response
219
- return new Response("Something went wrong!", { status: 500 });
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.5",
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
+ }
@@ -15,7 +15,7 @@ export interface AppConfig {
15
15
  Server: {
16
16
  port: number;
17
17
  origin?: string;
18
- JWT_SECRET: String,
18
+ JWT_Secret: String,
19
19
  }
20
20
  }
21
21