@triophore/falconjs 1.0.0 → 1.0.4

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.
@@ -63,7 +63,9 @@ class FalconAuth {
63
63
  socketio: Joi.object({
64
64
  enabled: Joi.boolean().default(true),
65
65
  timeout: Joi.number().default(5000)
66
- }).default({ enabled: true, timeout: 5000 })
66
+ }).default({ enabled: true, timeout: 5000 }),
67
+
68
+ default: Joi.string().optional()
67
69
  });
68
70
 
69
71
  const { error, value } = schema.validate(options);
@@ -95,10 +97,15 @@ class FalconAuth {
95
97
  await this.registerJWKS(server);
96
98
  }
97
99
 
98
- // Set default strategy if only one is configured
99
- const strategies = Object.keys(this.options).filter(k => k !== 'socketio');
100
- if (strategies.length === 1) {
101
- server.auth.default(strategies[0]);
100
+ // Set default strategy
101
+ if (this.options.default) {
102
+ server.auth.default(this.options.default);
103
+ } else {
104
+ // Auto-set default strategy if only one is configured (and no default specified)
105
+ const strategies = Object.keys(this.options).filter(k => k !== 'socketio' && k !== 'default');
106
+ if (strategies.length === 1) {
107
+ server.auth.default(strategies[0]);
108
+ }
102
109
  }
103
110
  }
104
111
 
@@ -279,15 +286,15 @@ class FalconAuth {
279
286
 
280
287
  extractSocketToken(socket) {
281
288
  const { token, auth, authorization } = socket.handshake.auth || {};
282
-
289
+
283
290
  if (token) return token;
284
291
  if (auth) return auth;
285
-
292
+
286
293
  const authHeader = socket.handshake.headers.authorization;
287
294
  if (authHeader?.startsWith('Bearer ')) {
288
295
  return authHeader.substring(7);
289
296
  }
290
-
297
+
291
298
  return socket.handshake.query.token;
292
299
  }
293
300
 
@@ -297,14 +304,14 @@ class FalconAuth {
297
304
  try {
298
305
  const cacheKey = `auth:custom:${this.hashToken(token)}`;
299
306
  let user = await this.getFromCache(cacheKey);
300
-
307
+
301
308
  if (!user) {
302
309
  user = await this.options.custom.validate(token, null, this.context);
303
310
  if (user) {
304
311
  await this.setCache(cacheKey, user, 300);
305
312
  }
306
313
  }
307
-
314
+
308
315
  if (user) return user;
309
316
  } catch (error) {
310
317
  this.logger.debug('Custom socket auth failed:', error.message);
@@ -318,21 +325,21 @@ class FalconAuth {
318
325
  const decoded = jwt.verify(token, this.options.jwt.secret, {
319
326
  algorithms: this.options.jwt.algorithms
320
327
  });
321
-
328
+
322
329
  const cacheKey = `auth:jwt:${decoded.sub || decoded.id}`;
323
330
  let user = null;
324
-
331
+
325
332
  if (this.options.jwt.cache.enabled) {
326
333
  user = await this.getFromCache(cacheKey);
327
334
  }
328
-
335
+
329
336
  if (!user) {
330
337
  user = await this.options.jwt.validate(decoded, null, this.context);
331
338
  if (user && this.options.jwt.cache.enabled) {
332
339
  await this.setCache(cacheKey, user, this.options.jwt.cache.ttl);
333
340
  }
334
341
  }
335
-
342
+
336
343
  if (user) return user;
337
344
  } catch (error) {
338
345
  this.logger.debug('JWT socket auth failed:', error.message);
@@ -348,21 +355,21 @@ class FalconAuth {
348
355
  issuer: this.options.jwks.issuer,
349
356
  audience: this.options.jwks.audience
350
357
  });
351
-
358
+
352
359
  const cacheKey = `auth:jwks:${payload.sub}`;
353
360
  let user = null;
354
-
361
+
355
362
  if (this.options.jwks.cache.enabled) {
356
363
  user = await this.getFromCache(cacheKey);
357
364
  }
358
-
365
+
359
366
  if (!user) {
360
367
  user = await this.options.jwks.validate(payload, null, this.context);
361
368
  if (user && this.options.jwks.cache.enabled) {
362
369
  await this.setCache(cacheKey, user, this.options.jwks.cache.ttl);
363
370
  }
364
371
  }
365
-
372
+
366
373
  if (user) return user;
367
374
  }
368
375
  } catch (error) {
@@ -378,12 +385,12 @@ class FalconAuth {
378
385
  if (customExtractor) {
379
386
  return customExtractor(request);
380
387
  }
381
-
388
+
382
389
  const authHeader = request.headers.authorization;
383
390
  if (authHeader?.startsWith('Bearer ')) {
384
391
  return authHeader.substring(7);
385
392
  }
386
-
393
+
387
394
  return request.query.token || request.payload?.token;
388
395
  }
389
396
 
@@ -394,7 +401,7 @@ class FalconAuth {
394
401
 
395
402
  async getFromCache(key) {
396
403
  if (!this.redis) return null;
397
-
404
+
398
405
  try {
399
406
  const cached = await this.redis.get(key);
400
407
  return cached ? JSON.parse(cached) : null;
@@ -406,7 +413,7 @@ class FalconAuth {
406
413
 
407
414
  async setCache(key, value, ttl) {
408
415
  if (!this.redis) return;
409
-
416
+
410
417
  try {
411
418
  await this.redis.setEx(key, ttl, JSON.stringify(value));
412
419
  } catch (error) {
@@ -461,10 +468,10 @@ const plugin = {
461
468
  register: async function (server, options) {
462
469
  // Get context from server app (passed from Falcon.js)
463
470
  const context = server.app.falconContext || {};
464
-
471
+
465
472
  const falconAuth = new FalconAuth(options, context);
466
473
  await falconAuth.registerHapi(server);
467
-
474
+
468
475
  // Expose auth instance for Socket.IO use
469
476
  server.app.falconAuth = falconAuth;
470
477
  }
package/falcon.js CHANGED
@@ -469,49 +469,74 @@ class Falcon {
469
469
  * @param {string[]} files - Array to collect file paths (used for recursion)
470
470
  * @returns {Promise<string[]>} Array of JavaScript file paths
471
471
  */
472
- async #findJsFilesRecursively(dir, files = []) {
473
- const items = await fs.promises.readdir(dir, { withFileTypes: true });
472
+ async #findJsFilesRecursively(dir) {
473
+ let files = [];
474
+ try {
475
+ const items = await fs.promises.readdir(dir, { withFileTypes: true });
474
476
 
475
- for (const item of items) {
476
- const fullPath = path.join(dir, item.name);
477
+ for (const item of items) {
478
+ const fullPath = path.join(dir, item.name);
477
479
 
478
- if (item.isDirectory() && !item.name.startsWith('.')) {
479
- await this.#findJsFilesRecursively(fullPath, files);
480
- }
481
- else if (item.isFile() && item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
482
- files.push(fullPath);
480
+ if (item.isDirectory() && !item.name.startsWith('.')) {
481
+ // Recursively find files and concat
482
+ const nestedFiles = await this.#findJsFilesRecursively(fullPath);
483
+ files = files.concat(nestedFiles);
484
+ }
485
+ else if (item.isFile() && item.name.endsWith('.js') && !item.name.endsWith('.test.js')) {
486
+ files.push(fullPath);
487
+ }
483
488
  }
489
+ } catch (err) {
490
+ this.CONTEXT["logger"].error(`Error scanning directory ${dir}:`, err.message);
484
491
  }
485
492
 
486
493
  return files;
487
494
  }
488
495
 
489
496
  /**
490
- * Builds a clean route path from file system path.
491
- * Converts folder structure to REST-style paths with parameter support.
497
+ * Parses route file path to extract method and route path.
498
+ * Validates against [METHOD]#filename.js pattern.
492
499
  *
493
500
  * @private
494
501
  * @param {string} relativePath - Relative path from routes directory
495
- * @returns {string} Clean route path
502
+ * @returns {Object|null} Object { method, path } or null if invalid
496
503
  */
497
- #buildRoutePath(relativePath) {
498
- let routePath = '/' + path.dirname(relativePath)
499
- .replace(/\\/g, '/')
500
- .replace(/\/index$/gi, '')
501
- .replace(/\/_/g, '/:')
502
- .replace(/_/g, '-')
503
- .replace(/(^|\/)\[([^\]]+)\]/g, '$1:$2');
504
-
505
- const baseName = path.basename(relativePath, '.js');
506
- if (baseName !== 'index') {
507
- let segment = baseName
508
- .replace(/^_/, ':')
509
- .replace(/_/g, '-')
510
- .replace(/^\[([^\]]+)\]$/, ':$1');
511
- routePath = path.posix.join(routePath, segment);
504
+ #parseRouteFile(relativePath) {
505
+ const filename = path.basename(relativePath);
506
+
507
+ // Check for [METHOD]# pattern
508
+ // Allows any methods characters, will be validated by Hapi or logic if needed, but user said ANY is valid
509
+ const match = filename.match(/^\[([A-Z]+)\]#(.*)\.js$/i);
510
+ if (!match) {
511
+ return null;
512
512
  }
513
513
 
514
- return routePath.replace(/\/+$/, '') || '/';
514
+ const method = match[1].toUpperCase();
515
+ const rawName = match[2];
516
+
517
+ let dirname = path.dirname(relativePath);
518
+ if (dirname === '.') dirname = '';
519
+
520
+ // Convert dirname params [param] -> {param}
521
+ let routePath = '/' + dirname
522
+ .split('/')
523
+ .map(segment => segment.replace(/^\[(.+)\]$/, '{$1}'))
524
+ .join('/');
525
+
526
+ // Clean up path
527
+ routePath = routePath.replace(/\/+/g, '/');
528
+
529
+ // Append filename (unless index)
530
+ if (rawName !== 'index') {
531
+ routePath = path.posix.join(routePath, rawName);
532
+ }
533
+
534
+ // Ensure no trailing slash (unless root)
535
+ if (routePath !== '/') {
536
+ routePath = routePath.replace(/\/$/, '');
537
+ }
538
+
539
+ return { method, path: routePath };
515
540
  }
516
541
 
517
542
  /**
@@ -526,12 +551,15 @@ class Falcon {
526
551
  #findBestValidatorName(baseName, relativePath, validators) {
527
552
  const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
528
553
 
554
+ // Adjust baseName for validator search since we stripped [METHOD]#
555
+ const realBaseName = baseName.replace(/^\[[A-Z]+\]#/, '');
556
+
529
557
  const candidates = [
530
- baseName,
531
- baseName + 'Payload',
532
- path.dirname(relativePath).split(path.sep).pop() + baseName,
533
- capitalize(baseName),
534
- baseName.replace(/Handler$/i, ''),
558
+ realBaseName,
559
+ realBaseName + 'Payload',
560
+ path.dirname(relativePath).split(path.sep).pop() + realBaseName,
561
+ capitalize(realBaseName),
562
+ realBaseName.replace(/Handler$/i, ''),
535
563
  ];
536
564
  for (const c of candidates) {
537
565
  if (validators[c]) return c;
@@ -585,8 +613,8 @@ class Falcon {
585
613
  }
586
614
 
587
615
  /**
588
- * Loads routes recursively from the routes directory with automatic validation and Swagger documentation.
589
- * Supports nested folder structures and automatic validator matching.
616
+ * Loads routes recursively from the routes directory using dynamic file naming conventions.
617
+ * Pattern: [METHOD]#filename.js
590
618
  *
591
619
  * @private
592
620
  * @async
@@ -619,45 +647,73 @@ class Falcon {
619
647
  // Register Swagger first (needs to be before routes)
620
648
  await this.#registerSwagger();
621
649
 
622
- this.CONTEXT["logger"].info("Loading routes with validation + Swagger docs...");
650
+ this.CONTEXT["logger"].info("Loading routes with dynamic naming convention...");
623
651
 
624
652
  const routeFiles = await this.#findJsFilesRecursively(routesDir);
625
653
 
626
654
  for (const filePath of routeFiles) {
627
655
  const relativePath = path.relative(routesDir, filePath);
628
- const baseName = path.basename(filePath, '.js');
629
- let routePath = this.#buildRoutePath(relativePath);
630
- this.CONTEXT["logger"].info("Route Name : " + routePath);
656
+
657
+ const parsed = this.#parseRouteFile(relativePath);
658
+ if (!parsed) {
659
+ // Discard if not matching pattern
660
+ continue;
661
+ }
662
+
663
+ const { method, path: routePath } = parsed;
664
+ this.CONTEXT["logger"].info(`Route found: [${method}] ${routePath} (${relativePath})`);
631
665
 
632
666
  try {
633
667
  const routeModule = require(filePath);
634
- const register = typeof routeModule === 'function' ? routeModule : routeModule.route?.bind(routeModule);
635
668
 
636
- if (typeof register !== 'function') continue;
669
+ // Flexible export: can be a handler function, or an object with handler/options
670
+ // OR legacy support: function that takes context (but we need to manually register)
637
671
 
638
- // Find validator + extract schema
639
- const validatorName = this.#findBestValidatorName(baseName, relativePath, validators);
640
- const validator = validatorName ? validators[validatorName] : null;
672
+ let handler, options = {};
673
+
674
+ if (typeof routeModule === 'function') {
675
+ // Assume it's a handler(request, h)
676
+ handler = routeModule;
677
+ } else if (typeof routeModule === 'object') {
678
+ if (routeModule.handler) handler = routeModule.handler;
679
+ if (routeModule.options) options = routeModule.options;
680
+
681
+ // If user exported .route function (legacy), we can't really use it easily with new params
682
+ // unless we assume they migrated. We'll stick to handler/options pattern for new files.
683
+ }
684
+
685
+ if (!handler) {
686
+ this.CONTEXT["logger"].warn(`Skipping ${relativePath}: No handler exported`);
687
+ continue;
688
+ }
641
689
 
642
690
  // Auto-generate tag from folder
643
691
  const tag = path.dirname(relativePath).split(path.sep)[0] || 'api';
644
692
 
645
- const wrappedRoute = async (server, ctx) => {
646
- // Add auto-generated route info to context
647
- ctx.route = {
648
- path: routePath,
649
- tag: tag,
650
- validator: validator,
651
- validatorName: validatorName
652
- };
653
-
654
- await register(ctx); // Only pass context, server is already in ctx.server
655
- };
693
+ // Find validator attempt
694
+ const baseName = path.basename(filePath, '.js');
695
+ const validatorName = this.#findBestValidatorName(baseName, relativePath, validators);
696
+ if (validatorName && validators[validatorName]) {
697
+ // Merge into options.validate if not present
698
+ options.validate = options.validate || {};
699
+ if (!options.validate.payload && !options.validate.query && !options.validate.params) {
700
+ // Heuristic: default to payload for POST/PUT/PATCH, query for others?
701
+ // Or just user provided. For now let's leave flexible or auto-attach if user wants.
702
+ // The old logic attached it to ctx.route.validator.
703
+ }
704
+ }
656
705
 
657
- await wrappedRoute(this.httpServer, this.CONTEXT);
706
+ // Register the route
707
+ this.httpServer.route({
708
+ method: method,
709
+ path: routePath,
710
+ handler: handler,
711
+ options: {
712
+ tags: [tag],
713
+ ...options
714
+ }
715
+ });
658
716
 
659
- const valMark = validator ? 'Validated + Documented' : 'Documented';
660
- this.CONTEXT["logger"].info(`${valMark} ${routePath} ← ${relativePath}`);
661
717
  } catch (err) {
662
718
  this.CONTEXT["logger"].error(`Route failed: ${filePath}`, err.message);
663
719
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@triophore/falconjs",
3
- "version": "1.0.0",
3
+ "version": "1.0.4",
4
4
  "description": "simple server framework for nodejs",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,63 @@
1
+ const path = require('path');
2
+
3
+ function parseRoute(relativePath) {
4
+ const filename = path.basename(relativePath);
5
+
6
+ // 1. Check for [METHOD]# pattern
7
+ const match = filename.match(/^\[([A-Z]+)\]#(.*)\.js$/i);
8
+ if (!match) {
9
+ return null; // Ignore file
10
+ }
11
+
12
+ const method = match[1].toUpperCase();
13
+ const rawName = match[2]; // filename without [METHOD]# and .js
14
+
15
+ // 2. Build Path
16
+ let dirname = path.dirname(relativePath);
17
+ if (dirname === '.') dirname = '';
18
+
19
+ // Convert dirname params [param] -> {param}
20
+ let routePath = '/' + dirname
21
+ .split('/')
22
+ .map(segment => segment.replace(/^\[(.+)\]$/, '{$1}')) // [id] -> {id}
23
+ .join('/');
24
+
25
+ // Clean up path
26
+ routePath = routePath.replace(/\/+/g, '/');
27
+
28
+ // Append filename (unless index)
29
+ if (rawName !== 'index') {
30
+ routePath = path.posix.join(routePath, rawName);
31
+ }
32
+
33
+ // Ensure no trailing slash (unless root)
34
+ if (routePath !== '/') {
35
+ routePath = routePath.replace(/\/$/, '');
36
+ }
37
+
38
+ return {
39
+ method,
40
+ path: routePath
41
+ };
42
+ }
43
+
44
+ const tests = [
45
+ 'files.js', // Should be ignored
46
+ '[GET]#file.js', // -> /file, GET
47
+ '[POST]#index.js', // -> /, POST
48
+ 'folder_name/[GET]#file.js', // -> /folder_name/file, GET
49
+ 'folder_name/[param]/[PUT]#file.js', // -> /folder_name/{param}/file, PUT
50
+ 'users/[id]/[GET]#details.js', // -> /users/{id}/details, GET
51
+ 'users/[id]/[GET]#index.js', // -> /users/{id}, GET
52
+ 'api/v1/[DELETE]#user.js' // -> /api/v1/user, DELETE
53
+ ];
54
+
55
+ console.log('--- Testing Route Parsing ---');
56
+ tests.forEach(t => {
57
+ const result = parseRoute(t);
58
+ if (result) {
59
+ console.log(`${t} -> ${result.method} ${result.path}`);
60
+ } else {
61
+ console.log(`${t} -> IGNORED`);
62
+ }
63
+ });
@@ -0,0 +1 @@
1
+ module.exports = {}
@@ -0,0 +1 @@
1
+ module.exports = {}