@lesto/router 0.1.0 → 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 +2 -2
- package/src/errors.ts +2 -1
- package/src/file-routes.ts +62 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lesto/router",
|
|
3
|
-
"version": "0.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": "
|
|
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> {
|
package/src/file-routes.ts
CHANGED
|
@@ -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
|