@lesto/router 0.1.1 → 0.1.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lesto/router",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "license": "MIT",
5
5
  "description": "Lesto's RESTful router — declare routes and named paths, resolve method+path to a controller#action.",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "format:check": "oxfmt --check src test"
20
20
  },
21
21
  "dependencies": {
22
- "@lesto/errors": "0.1.1"
22
+ "@lesto/errors": "0.1.2"
23
23
  },
24
24
  "publishConfig": {
25
25
  "access": "public"
package/src/errors.ts CHANGED
@@ -19,7 +19,8 @@ export type RouterErrorCode =
19
19
  | "ROUTER_FILE_BAD_SEGMENT"
20
20
  | "ROUTER_FILE_CATCHALL_POSITION"
21
21
  | "ROUTER_FILE_DUPLICATE_ROUTE"
22
- | "ROUTER_FILE_DUPLICATE_PARAM";
22
+ | "ROUTER_FILE_DUPLICATE_PARAM"
23
+ | "ROUTER_FILE_ORPHAN_MIDDLEWARE";
23
24
 
24
25
  /** Anything the router can refuse to do. */
25
26
  export class RouterError extends LestoError<RouterErrorCode> {
@@ -422,6 +422,61 @@ function assertCatchAllTerminal(urlSegments: ReadonlyArray<string>, pattern: str
422
422
  }
423
423
  }
424
424
 
425
+ /**
426
+ * Refuse a `middleware.ts` that has NO page at or below its directory — an orphan
427
+ * guard that silently never runs.
428
+ *
429
+ * A `middleware` registers no route of its own: a page picks it up only when the
430
+ * page's path passes THROUGH the middleware's directory (the page's segments start
431
+ * with the middleware's — the same prefix rule {@link layoutDepthsFor} composes the
432
+ * chain by). A `middleware` whose directory prefixes no page is therefore referenced
433
+ * by no page's `middlewareDepth`, so the applier never runs it. For a layout or a
434
+ * boundary that is merely dead weight; for a `middleware` — typically an AUTH guard —
435
+ * it is a fail-OPEN hole: a `middleware.ts` placed where no page sits below it — a
436
+ * wrong/typo'd directory, or a file one level too deep — ships a guard that quietly
437
+ * protects nothing. We surface it as a coded `ROUTER_FILE_ORPHAN_MIDDLEWARE` at compile
438
+ * time (the same loud-build-time refusal every other file-route mistake gets), naming
439
+ * the directory to fix.
440
+ *
441
+ * "At or below" is the prefix test: a page wraps in a middleware iff the page's raw
442
+ * segments start with the middleware's raw segments — directory keys compared by
443
+ * {@link dirKey} so a `[id]` segment matches itself, not a sibling. A middleware at a
444
+ * directory that is itself a page's directory counts (the page is "below" at depth 0).
445
+ */
446
+ function assertNoOrphanMiddleware(
447
+ files: ReadonlyArray<DiscoveredFile>,
448
+ pages: ReadonlyArray<FileRoute>,
449
+ ): void {
450
+ // The directory key of every page — `middleware` guards a page iff the page's
451
+ // segments START WITH the middleware's, i.e. some page key has the middleware key
452
+ // as a path prefix (`""` = root prefixes all; `"admin"` prefixes `admin`/`admin/x`).
453
+ const pageKeys = pages.map((page) => dirKey(page.segments));
454
+
455
+ for (const file of files) {
456
+ if (file.kind !== "middleware") continue;
457
+
458
+ const middlewareKey = dirKey(file.segments);
459
+
460
+ // The ROOT middleware (`""`) is a prefix of every page, so any page at all means
461
+ // it guards something; a deeper middleware (`"admin"`) guards a page iff that
462
+ // page's key is the directory itself or sits below it (`"admin"` / `"admin/x"`).
463
+ const guardsAPage =
464
+ middlewareKey === ""
465
+ ? pageKeys.length > 0
466
+ : pageKeys.some(
467
+ (pageKey) => pageKey === middlewareKey || pageKey.startsWith(`${middlewareKey}/`),
468
+ );
469
+
470
+ if (guardsAPage) continue;
471
+
472
+ throw new RouterError(
473
+ "ROUTER_FILE_ORPHAN_MIDDLEWARE",
474
+ `File-route middleware at "${middlewareKey || "<root>"}" has no page at or below its directory — it guards nothing and would silently never run (a fail-open auth hole if it is a guard); move it to a directory with a page, or delete it.`,
475
+ { dir: middlewareKey },
476
+ );
477
+ }
478
+ }
479
+
425
480
  /**
426
481
  * Compile a flat list of {@link DiscoveredFile}s into ordered {@link FileRoute}s
427
482
  * ready for the applier to register, oldest-convention rules enforced here once.
@@ -545,6 +600,13 @@ export function compileFileRoutes(files: ReadonlyArray<DiscoveredFile>): Readonl
545
600
  });
546
601
  }
547
602
 
603
+ // A `middleware.ts` with NO page at or below its directory guards nothing — no
604
+ // page's `middlewareDepth` ever references it, so the applier never runs it. That
605
+ // is a silent FAIL-OPEN for an auth guard (a typo'd dir, or one placed a level too
606
+ // deep), so refuse it by code here rather than ship a guard that quietly never
607
+ // runs. Run after the pages are collected so the whole page set is known.
608
+ assertNoOrphanMiddleware(files, pages);
609
+
548
610
  // Most-specific first: a deeper path before a shallower one, and at equal depth
549
611
  // the path whose first differing segment is static (not `:param`) before the
550
612
  // dynamic one. The first-match-wins table then resolves a literal route ahead of