@velajs/vela 1.14.0 → 1.16.0

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/application.d.ts +10 -1
  3. package/dist/application.js +12 -0
  4. package/dist/config/config.module.d.ts +8 -1
  5. package/dist/config/config.module.js +89 -42
  6. package/dist/config/config.service.d.ts +17 -4
  7. package/dist/config/config.service.js +18 -20
  8. package/dist/config/config.store.d.ts +46 -0
  9. package/dist/config/config.store.js +119 -0
  10. package/dist/config/config.tokens.d.ts +11 -0
  11. package/dist/config/config.tokens.js +12 -0
  12. package/dist/config/config.types.d.ts +37 -1
  13. package/dist/config/config.types.js +4 -1
  14. package/dist/config/index.d.ts +5 -2
  15. package/dist/config/index.js +3 -1
  16. package/dist/config/register-as.d.ts +52 -0
  17. package/dist/config/register-as.js +39 -0
  18. package/dist/container/container.d.ts +8 -1
  19. package/dist/container/container.js +39 -1
  20. package/dist/container/index.d.ts +2 -2
  21. package/dist/container/index.js +1 -1
  22. package/dist/container/types.d.ts +23 -0
  23. package/dist/container/types.js +6 -1
  24. package/dist/{storage → crypto}/signed-url.js +5 -0
  25. package/dist/factory/bootstrap.js +10 -0
  26. package/dist/http/decorators.d.ts +18 -9
  27. package/dist/http/decorators.js +4 -1
  28. package/dist/http/index.d.ts +4 -0
  29. package/dist/http/index.js +2 -0
  30. package/dist/http/route-map.d.ts +30 -0
  31. package/dist/http/route-map.js +27 -0
  32. package/dist/http/route.manager.d.ts +24 -0
  33. package/dist/http/route.manager.js +28 -1
  34. package/dist/http/types.d.ts +2 -0
  35. package/dist/http/url/index.d.ts +4 -0
  36. package/dist/http/url/index.js +3 -0
  37. package/dist/http/url/signed-url.guard.d.ts +27 -0
  38. package/dist/http/url/signed-url.guard.js +62 -0
  39. package/dist/http/url/signing-secret.d.ts +20 -0
  40. package/dist/http/url/signing-secret.js +24 -0
  41. package/dist/http/url/url-generator.service.d.ts +52 -0
  42. package/dist/http/url/url-generator.service.js +104 -0
  43. package/dist/index.d.ts +9 -5
  44. package/dist/index.js +5 -3
  45. package/dist/openapi/document.js +2 -1
  46. package/dist/registry/types.d.ts +2 -0
  47. package/dist/storage/index.d.ts +2 -2
  48. package/dist/storage/index.js +4 -1
  49. package/package.json +6 -2
  50. /package/dist/{storage → crypto}/signed-url.d.ts +0 -0
@@ -0,0 +1,39 @@
1
+ // Config-namespace registration. Design ported from stratal's `registerAs`
2
+ // (MIT, © Temitayo Fadojutimi) and reworked for vela's multi-runtime DI: env
3
+ // enters through the `CONFIG_ENV` token instead of a Cloudflare-specific one.
4
+ import { CONFIG_ENV } from "./config.tokens.js";
5
+ /**
6
+ * Create a namespaced configuration factory (NestJS `registerAs` parity).
7
+ *
8
+ * The `KEY` is minted with `Symbol.for` so it keeps a stable identity across
9
+ * module re-evaluation (HMR / repeated imports) — a fresh `InjectionToken`
10
+ * would mint a new identity each eval and break dedup. It is typed as
11
+ * `InjectionToken<TConfig>` purely for injection-site DX.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * export const dbConfig = registerAs('database', (env: Env) => ({
16
+ * url: env.DATABASE_URL,
17
+ * pool: 10,
18
+ * }));
19
+ * // ConfigModule.forRoot({ load: [dbConfig] });
20
+ * // cfg.get('database.url'); // via ConfigService
21
+ * // constructor(@Inject(dbConfig.KEY) db: InferConfigType<typeof dbConfig>) {}
22
+ * ```
23
+ */ export function registerAs(namespace, factory) {
24
+ const KEY = Symbol.for(`vela:config:${namespace}`);
25
+ return {
26
+ KEY,
27
+ namespace,
28
+ factory,
29
+ asProvider () {
30
+ return {
31
+ provide: KEY,
32
+ useFactory: factory,
33
+ inject: [
34
+ CONFIG_ENV
35
+ ]
36
+ };
37
+ }
38
+ };
39
+ }
@@ -1,5 +1,5 @@
1
1
  import { Scope } from '../constants';
2
- import type { ContainerOptions, Diagnostics, LazyResolutionHook, ModuleScope, ProviderOptions, Token, Type } from './types';
2
+ import type { ContainerOptions, Diagnostics, LazyResolutionHook, ModuleDescription, ModuleScope, ProviderOptions, Token, Type } from './types';
3
3
  /**
4
4
  * Per-module provider buckets. Each module instance owns its providers under
5
5
  * its `moduleId`; the same logical token can have distinct registrations in
@@ -95,6 +95,13 @@ export declare class Container {
95
95
  * binding module — so framework adapters can initialize all of them.
96
96
  */
97
97
  getUseValues(): unknown[];
98
+ /**
99
+ * Serializable description of every loaded module instance (load order),
100
+ * plus the `__root__` bucket (bootstrap primitives) when non-empty —
101
+ * the `vela module graph` seam. Reads registration state only: safe pre-
102
+ * and post-bootstrap, never constructs, never claims lazy modules.
103
+ */
104
+ getModuleDescriptions(): ModuleDescription[];
98
105
  /**
99
106
  * Compute request-scope bubbling for every registration (call once at
100
107
  * bootstrap, after all providers are registered). A provider whose declared
@@ -1,7 +1,7 @@
1
1
  import { Scope } from "../constants.js";
2
2
  import { disposeInstance, isDisposable } from "./disposable.js";
3
3
  import { getConstructorDependencies, getInjectMetadata, getScope, isInjectable } from "./decorators.js";
4
- import { ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
4
+ import { describeToken, ForwardRef, InjectionToken, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
5
5
  const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips type-only imports at ' + 'runtime and `design:paramtypes` emits `Object`/`undefined` for their ' + 'positions. Use a runtime `import { X }` for DI tokens.';
6
6
  /**
7
7
  * Sentinel comparing a token to the bare `Object` class — `design:paramtypes`
@@ -415,6 +415,44 @@ const IMPORT_TYPE_HINT = 'Did you use `import type { X }`? TypeScript strips typ
415
415
  return out;
416
416
  }
417
417
  /**
418
+ * Serializable description of every loaded module instance (load order),
419
+ * plus the `__root__` bucket (bootstrap primitives) when non-empty —
420
+ * the `vela module graph` seam. Reads registration state only: safe pre-
421
+ * and post-bootstrap, never constructs, never claims lazy modules.
422
+ */ getModuleDescriptions() {
423
+ const out = [];
424
+ for (const scope of this.scopes.values()){
425
+ out.push({
426
+ moduleId: scope.moduleId,
427
+ imports: [
428
+ ...scope.importedModules
429
+ ],
430
+ isGlobal: scope.isGlobal,
431
+ lazy: scope.lazy === true,
432
+ providers: [
433
+ ...this.providers.get(scope.moduleId)?.keys() ?? []
434
+ ].map(describeToken),
435
+ exports: [
436
+ ...scope.exportedTokens
437
+ ].map(describeToken)
438
+ });
439
+ }
440
+ const rootBucket = this.providers.get(ROOT_MODULE_ID);
441
+ if (rootBucket && rootBucket.size > 0) {
442
+ out.push({
443
+ moduleId: ROOT_MODULE_ID,
444
+ imports: [],
445
+ isGlobal: false,
446
+ lazy: false,
447
+ providers: [
448
+ ...rootBucket.keys()
449
+ ].map(describeToken),
450
+ exports: []
451
+ });
452
+ }
453
+ return out;
454
+ }
455
+ /**
418
456
  * Compute request-scope bubbling for every registration (call once at
419
457
  * bootstrap, after all providers are registered). A provider whose declared
420
458
  * scope is not REQUEST but which (transitively) depends on a request-scoped
@@ -1,6 +1,6 @@
1
1
  export { Container } from './container';
2
2
  export { Injectable, Inject, Optional, isInjectable, getScope } from './decorators';
3
- export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, } from './types';
3
+ export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, describeToken, } from './types';
4
4
  export { ModuleRef } from './module-ref';
5
5
  export { mixin } from './mixin';
6
- export type { Type, Token, InferToken, InferTokens, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, ModuleScope, ContainerOptions, Diagnostics, } from './types';
6
+ export type { Type, Token, InferToken, InferTokens, InjectableOptions, InjectMetadata, ProviderOptions, ProviderRegistration, InjectionTokenOptions, ModuleScope, ModuleDescription, ContainerOptions, Diagnostics, } from './types';
@@ -1,5 +1,5 @@
1
1
  export { Container } from "./container.js";
2
2
  export { Injectable, Inject, Optional, isInjectable, getScope } from "./decorators.js";
3
- export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID } from "./types.js";
3
+ export { InjectionToken, ForwardRef, forwardRef, ModuleVisibilityError, MultipleProvidersFoundError, ROOT_MODULE_ID, describeToken } from "./types.js";
4
4
  export { ModuleRef } from "./module-ref.js";
5
5
  export { mixin } from "./mixin.js";
@@ -117,6 +117,29 @@ export type Diagnostics = 'silent' | 'log' | 'throw';
117
117
  export interface ContainerOptions {
118
118
  diagnostics?: Diagnostics;
119
119
  }
120
+ /**
121
+ * Human-readable token label — class name, `InjectionToken(desc)`, symbol
122
+ * string, or String() fallback. Public so introspection tooling (module
123
+ * graphs, entrypoint listings) renders tokens the same way vela's own errors
124
+ * do.
125
+ */
126
+ export declare function describeToken(token: Token): string;
127
+ /**
128
+ * One module instance in the loaded graph, serializable (strings only) —
129
+ * what `Container.getModuleDescriptions()` returns for introspection tooling
130
+ * (`vela module graph`). Reads registration state only; never constructs.
131
+ */
132
+ export interface ModuleDescription {
133
+ moduleId: string;
134
+ /** moduleIds this instance imports. */
135
+ imports: string[];
136
+ isGlobal: boolean;
137
+ lazy: boolean;
138
+ /** Token labels registered in this instance's bucket (registration order). */
139
+ providers: string[];
140
+ /** Token labels this instance exports. */
141
+ exports: string[];
142
+ }
120
143
  export declare class ModuleVisibilityError extends Error {
121
144
  readonly moduleId: string;
122
145
  readonly token: Token;
@@ -18,7 +18,12 @@ export class ForwardRef {
18
18
  export function forwardRef(factory) {
19
19
  return new ForwardRef(factory);
20
20
  }
21
- function describeToken(token) {
21
+ /**
22
+ * Human-readable token label — class name, `InjectionToken(desc)`, symbol
23
+ * string, or String() fallback. Public so introspection tooling (module
24
+ * graphs, entrypoint listings) renders tokens the same way vela's own errors
25
+ * do.
26
+ */ export function describeToken(token) {
22
27
  if (token instanceof InjectionToken) return token.toString();
23
28
  if (typeof token === 'function') return token.name;
24
29
  if (typeof token === 'symbol') return token.toString();
@@ -1,6 +1,11 @@
1
1
  // Signed-URL utilities using HMAC-SHA256 via the Web Crypto API (edge-safe;
2
2
  // no node:crypto). Verification uses crypto.subtle.verify for timing-safety.
3
3
  // Pattern: https://developers.cloudflare.com/workers/examples/signing-requests/
4
+ //
5
+ // This is the canonical home for the util. `@velajs/vela/storage` re-exports it
6
+ // (see src/storage/index.ts) so the storage subpath API is unchanged, while
7
+ // core (the URL generator + signed-URL guard) imports it here directly — core
8
+ // never reaches into a published subpath.
4
9
  async function importKey(secret) {
5
10
  return crypto.subtle.importKey('raw', new TextEncoder().encode(secret), {
6
11
  name: 'HMAC',
@@ -4,6 +4,8 @@ import { ModuleRef } from "../container/module-ref.js";
4
4
  import { DiscoveryService } from "../discovery/discovery.service.js";
5
5
  import { REQUEST_CONTEXT } from "../http/request-context.js";
6
6
  import { RouteManager } from "../http/route.manager.js";
7
+ import { UrlGeneratorService } from "../http/url/url-generator.service.js";
8
+ import { SignedUrlGuard } from "../http/url/signed-url.guard.js";
7
9
  import { ModuleLoader } from "../module/module-loader.js";
8
10
  import { bindAppProviders } from "../pipeline/app-providers.js";
9
11
  import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from "../pipeline/tokens.js";
@@ -70,6 +72,14 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from
70
72
  useValue: routeManager
71
73
  });
72
74
  container.markGlobalToken(RouteManager);
75
+ // Named-route URL generation + signed-URL verification are app-level
76
+ // singletons: injectable from any module, and (for the guard) instantiable by
77
+ // the pipeline when a route opts in via `@SignedUrl()`. Both read their secret
78
+ // lazily and never force controllers/lazy modules to materialize.
79
+ container.register(UrlGeneratorService);
80
+ container.markGlobalToken(UrlGeneratorService);
81
+ container.register(SignedUrlGuard);
82
+ container.markGlobalToken(SignedUrlGuard);
73
83
  const loader = new ModuleLoader(container, routeManager);
74
84
  // loader.load() also arms the deferred-init seam (LazyModuleManager) — kept
75
85
  // inside the loader so hand-rolled bootstrap paths that never call this
@@ -31,15 +31,24 @@ export declare function Controller(pathOrOptions?: string | ControllerOptions):
31
31
  */
32
32
  export declare function Version(version: number | number[]): MethodDecorator;
33
33
  export declare function getRouteVersion(target: Constructor, propertyKey: string | symbol): number | number[] | undefined;
34
- export declare const Get: (path?: string) => MethodDecorator;
35
- export declare const Post: (path?: string) => MethodDecorator;
36
- export declare const Put: (path?: string) => MethodDecorator;
37
- export declare const Patch: (path?: string) => MethodDecorator;
38
- export declare const Delete: (path?: string) => MethodDecorator;
39
- export declare const Options: (path?: string) => MethodDecorator;
40
- export declare const Head: (path?: string) => MethodDecorator;
41
- export declare const All: (path?: string) => MethodDecorator;
42
- export declare const Sse: (path?: string) => MethodDecorator;
34
+ /** Per-route options for the HTTP method decorators (`@Get`, `@Post`, …). */
35
+ export interface RouteOptions {
36
+ /**
37
+ * A stable, human-readable name for this route. Enables URL generation
38
+ * (`UrlGeneratorService.urlFor(name, …)`), surfaces on `app.describeRoutes()`,
39
+ * and when set becomes the OpenAPI `operationId`.
40
+ */
41
+ name?: string;
42
+ }
43
+ export declare const Get: (path?: string, options?: RouteOptions) => MethodDecorator;
44
+ export declare const Post: (path?: string, options?: RouteOptions) => MethodDecorator;
45
+ export declare const Put: (path?: string, options?: RouteOptions) => MethodDecorator;
46
+ export declare const Patch: (path?: string, options?: RouteOptions) => MethodDecorator;
47
+ export declare const Delete: (path?: string, options?: RouteOptions) => MethodDecorator;
48
+ export declare const Options: (path?: string, options?: RouteOptions) => MethodDecorator;
49
+ export declare const Head: (path?: string, options?: RouteOptions) => MethodDecorator;
50
+ export declare const All: (path?: string, options?: RouteOptions) => MethodDecorator;
51
+ export declare const Sse: (path?: string, options?: RouteOptions) => MethodDecorator;
43
52
  export declare const Param: (nameOrPipe?: string | PipeType, ...pipes: PipeType[]) => ParameterDecorator;
44
53
  export declare const Query: (nameOrPipe?: string | PipeType, ...pipes: PipeType[]) => ParameterDecorator;
45
54
  export declare const Body: (nameOrPipe?: string | PipeType, ...pipes: PipeType[]) => ParameterDecorator;
@@ -57,7 +57,7 @@ export function getRouteVersion(target, propertyKey) {
57
57
  return MetadataRegistry.getRouteVersion(target, propertyKey);
58
58
  }
59
59
  function createMethodDecorator(method) {
60
- return (path = '')=>{
60
+ return (path = '', options)=>{
61
61
  return (target, propertyKey, _descriptor)=>{
62
62
  const ctor = target.constructor;
63
63
  const normalizedPath = normalizePath(path);
@@ -68,6 +68,9 @@ function createMethodDecorator(method) {
68
68
  handlerName: propertyKey,
69
69
  ...version !== undefined ? {
70
70
  version
71
+ } : {},
72
+ ...options?.name !== undefined ? {
73
+ name: options.name
71
74
  } : {}
72
75
  });
73
76
  };
@@ -3,6 +3,10 @@ export type { RouteManagerOptions } from './route.manager';
3
3
  export { MiddlewareBuilder } from '../module/middleware';
4
4
  export type { MiddlewareConsumer, MiddlewareConfigProxy, RouteInfo, NestModule, MiddlewareRouteDefinition } from '../module/middleware';
5
5
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController, } from './decorators';
6
+ export type { RouteOptions } from './decorators';
6
7
  export { createLazyParamDecorator } from './lazy-param.decorator';
8
+ export { UrlGeneratorService, SignedUrlGuard, SignedUrl, URL_SIGNING_SECRET, } from './url/index';
9
+ export type { UrlForOptions, SignedUrlGenerateOptions } from './url/index';
10
+ export type { VelaRouteMap, RouteName, RouteParams } from './route-map';
7
11
  export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext, } from './ambient';
8
12
  export type { RouteMetadata, ControllerMetadata, ControllerOptions, ParamMetadata, ControllerRegistration, } from './types';
@@ -2,4 +2,6 @@ export { RouteManager } from "./route.manager.js";
2
2
  export { MiddlewareBuilder } from "../module/middleware.js";
3
3
  export { Controller, Version, Get, Post, Put, Patch, Delete, Options, Head, All, Sse, Param, Query, Body, Headers, Req, Res, Ip, Cookie, Cookies, RawBody, HttpCode, Header, Redirect, createParamDecorator, applyDecorators, isController } from "./decorators.js";
4
4
  export { createLazyParamDecorator } from "./lazy-param.decorator.js";
5
+ // Named-route URL generation + signed URLs
6
+ export { UrlGeneratorService, SignedUrlGuard, SignedUrl, URL_SIGNING_SECRET } from "./url/index.js";
5
7
  export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext } from "./ambient.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Augmentable map of route name → its path params. Users opt in to type-safe
3
+ * URL generation by augmenting this interface — no build step, no codegen:
4
+ *
5
+ * ```ts
6
+ * declare module '@velajs/vela' {
7
+ * interface VelaRouteMap {
8
+ * 'users.index': Record<string, never>;
9
+ * 'users.show': { id: string };
10
+ * }
11
+ * }
12
+ * ```
13
+ *
14
+ * Until augmented it stays empty, so `RouteName` falls back to `string` and
15
+ * `RouteParams<N>` to a loose `Record<string, string> | undefined` — untyped
16
+ * callers keep working unchanged.
17
+ */
18
+ export interface VelaRouteMap {
19
+ }
20
+ /**
21
+ * Every valid route name. Falls back to `string` when `VelaRouteMap` has not
22
+ * been augmented, so `urlFor`/`signedUrl` accept any name in an untyped app.
23
+ */
24
+ export type RouteName = keyof VelaRouteMap extends never ? string : Extract<keyof VelaRouteMap, string>;
25
+ /**
26
+ * The params object required to build a named route's URL. When `VelaRouteMap`
27
+ * is augmented, resolves to the declared params shape for `N`; otherwise a
28
+ * loose `Record<string, string> | undefined`.
29
+ */
30
+ export type RouteParams<N extends RouteName> = N extends keyof VelaRouteMap ? VelaRouteMap[N] : Record<string, string> | undefined;
@@ -0,0 +1,27 @@
1
+ // Zero-codegen, type-safe route names — mirrors the augmentation pattern from
2
+ // stratal `packages/core/src/router/route-map.ts` (MIT © Temitayo Fadojutimi).
3
+ // Adapted for vela: `VelaRouteMap` maps a route name directly to its params
4
+ // object (stratal wraps params in `{ params: … }`; vela keeps it flat to match
5
+ // the `urlFor(name, params)` call shape).
6
+ /**
7
+ * Augmentable map of route name → its path params. Users opt in to type-safe
8
+ * URL generation by augmenting this interface — no build step, no codegen:
9
+ *
10
+ * ```ts
11
+ * declare module '@velajs/vela' {
12
+ * interface VelaRouteMap {
13
+ * 'users.index': Record<string, never>;
14
+ * 'users.show': { id: string };
15
+ * }
16
+ * }
17
+ * ```
18
+ *
19
+ * Until augmented it stays empty, so `RouteName` falls back to `string` and
20
+ * `RouteParams<N>` to a loose `Record<string, string> | undefined` — untyped
21
+ * callers keep working unchanged.
22
+ */ // eslint-disable-next-line @typescript-eslint/no-empty-object-type -- augmentable interface, intentionally empty until a consumer declares routes
23
+ /**
24
+ * The params object required to build a named route's URL. When `VelaRouteMap`
25
+ * is augmented, resolves to the declared params shape for `N`; otherwise a
26
+ * loose `Record<string, string> | undefined`.
27
+ */ export { };
@@ -5,6 +5,25 @@ import type { MiddlewareRouteDefinition } from '../module/middleware';
5
5
  import type { CanActivate, ExceptionFilter, NestInterceptor, NestMiddleware, PipeTransform } from '../pipeline/types';
6
6
  import type { FilterType, GuardType, InterceptorType, MiddlewareType, PipeType } from '../registry/types';
7
7
  import type { ControllerRegistration } from './types';
8
+ /**
9
+ * One explicit controller route as the framework registered it — recorded by
10
+ * `build()` with the SAME composed path handed to Hono (global prefix +
11
+ * version segment + controller prefix + route path), so introspection tooling
12
+ * (`vela route list`) never re-derives composition. Contributed routes
13
+ * (`RouteContributor`, e.g. `@velajs/crud`) mount directly on the Hono app
14
+ * and are not described here — diff `app.getHonoApp().routes` for those.
15
+ */
16
+ export interface RouteDescription {
17
+ /** As declared on the handler — `@Head()` reports HEAD (Hono serves it under GET). */
18
+ method: string;
19
+ /** Fully composed path exactly as registered. */
20
+ path: string;
21
+ controller: string;
22
+ handler: string;
23
+ version?: number;
24
+ /** Route name (`@Get(path, { name })`) — the URL-generation / `operationId` key. */
25
+ name?: string;
26
+ }
8
27
  export interface RouteManagerOptions {
9
28
  getClientIp?: (c: Context) => string | null;
10
29
  middleware?: MiddlewareHandler[];
@@ -27,6 +46,7 @@ export declare class RouteManager {
27
46
  private globalFilters;
28
47
  private globalPrefix;
29
48
  private consumerMiddlewareDefinitions;
49
+ private routeDescriptions;
30
50
  private readonly handlerExecutor;
31
51
  private readonly ambientContainer;
32
52
  constructor(container: Container, options?: RouteManagerOptions);
@@ -59,6 +79,10 @@ export declare class RouteManager {
59
79
  private wrapMiddlewareWithFilters;
60
80
  private mapMiddlewareError;
61
81
  registerController(controller: Type): this;
82
+ /** The explicit controller routes recorded by the last `build()`. */
83
+ getRouteDescriptions(): RouteDescription[];
84
+ /** The global prefix in effect (set at bootstrap; '' when none). */
85
+ getGlobalPrefix(): string;
62
86
  build(): Promise<Hono>;
63
87
  private registerRoute;
64
88
  private buildVersionedPaths;
@@ -92,6 +92,7 @@ export class RouteManager {
92
92
  globalFilters = [];
93
93
  globalPrefix = '';
94
94
  consumerMiddlewareDefinitions = [];
95
+ routeDescriptions = [];
95
96
  handlerExecutor;
96
97
  ambientContainer;
97
98
  constructor(container, options = {}){
@@ -273,8 +274,17 @@ export class RouteManager {
273
274
  });
274
275
  return this;
275
276
  }
277
+ /** The explicit controller routes recorded by the last `build()`. */ getRouteDescriptions() {
278
+ return [
279
+ ...this.routeDescriptions
280
+ ];
281
+ }
282
+ /** The global prefix in effect (set at bootstrap; '' when none). */ getGlobalPrefix() {
283
+ return this.globalPrefix;
284
+ }
276
285
  async build() {
277
286
  const app = new Hono();
287
+ this.routeDescriptions = [];
278
288
  // Outermost: dispose the per-request child container once the request is
279
289
  // fully done. Fast-paths out when the child has no request-scoped
280
290
  // disposables (the common case → zero overhead / no behavior change).
@@ -353,11 +363,18 @@ export class RouteManager {
353
363
  for (const route of routes){
354
364
  const effectiveVersion = route.version ?? metadata.version;
355
365
  const versionedPaths = this.buildVersionedPaths(this.globalPrefix, metadata.prefix, route.path, effectiveVersion);
366
+ // Parallel to versionedPaths: buildVersionedPaths maps versions to
367
+ // paths 1:1 in order, so index i of both arrays belongs together.
368
+ const versionsForPaths = effectiveVersion === undefined ? [
369
+ undefined
370
+ ] : Array.isArray(effectiveVersion) ? effectiveVersion : [
371
+ effectiveVersion
372
+ ];
356
373
  // Scoped (controller/handler) middleware only — global middleware is
357
374
  // applied once by this manager's own global pass, never re-read here.
358
375
  const middlewareItems = ComponentManager.getScopedComponents('middleware', controller, route.handlerName);
359
376
  const handler = this.handlerExecutor.create(route, controller, allParamMetadata);
360
- for (const fullPath of versionedPaths){
377
+ for (const [pathIndex, fullPath] of versionedPaths.entries()){
361
378
  for (const middlewareItem of middlewareItems){
362
379
  app.use(fullPath, this.wrapMiddlewareWithFilters((c, next)=>{
363
380
  const requestContainer = this.getRequestContainer(c);
@@ -366,6 +383,16 @@ export class RouteManager {
366
383
  }));
367
384
  }
368
385
  this.registerRoute(app, route.method, fullPath, handler);
386
+ this.routeDescriptions.push({
387
+ method: String(route.method),
388
+ path: fullPath || '/',
389
+ controller: controller.name,
390
+ handler: String(route.handlerName),
391
+ version: versionsForPaths[pathIndex],
392
+ ...route.name !== undefined ? {
393
+ name: route.name
394
+ } : {}
395
+ });
369
396
  }
370
397
  }
371
398
  }
@@ -6,6 +6,8 @@ export interface RouteMetadata {
6
6
  path: string;
7
7
  handlerName: string | symbol;
8
8
  version?: number | number[];
9
+ /** Route name for URL generation / OpenAPI operationId (`@Get(path, { name })`). */
10
+ name?: string;
9
11
  }
10
12
  export interface ControllerOptions {
11
13
  path?: string;
@@ -0,0 +1,4 @@
1
+ export { UrlGeneratorService } from './url-generator.service';
2
+ export type { UrlForOptions, SignedUrlGenerateOptions } from './url-generator.service';
3
+ export { SignedUrlGuard, SignedUrl } from './signed-url.guard';
4
+ export { URL_SIGNING_SECRET } from './signing-secret';
@@ -0,0 +1,3 @@
1
+ export { UrlGeneratorService } from "./url-generator.service.js";
2
+ export { SignedUrlGuard, SignedUrl } from "./signed-url.guard.js";
3
+ export { URL_SIGNING_SECRET } from "./signing-secret.js";
@@ -0,0 +1,27 @@
1
+ import { applyDecorators } from '../decorators';
2
+ import type { CanActivate, ExecutionContext } from '../../pipeline/types';
3
+ /**
4
+ * Verifies the HMAC signature (and `expires`) of the incoming request URL,
5
+ * using the same secret source as {@link UrlGeneratorService.signedUrl}: the
6
+ * {@link URL_SIGNING_SECRET} token, else `CONFIG_ENV`. Throws
7
+ * `ForbiddenException` (403) when the signature is missing, tampered, or
8
+ * expired. Registered app-wide by `bootstrap`; apply it per-route with
9
+ * {@link SignedUrl}.
10
+ */
11
+ export declare class SignedUrlGuard implements CanActivate {
12
+ private readonly secretToken?;
13
+ private readonly env;
14
+ constructor(secretToken?: string | undefined, env?: Record<string, unknown>);
15
+ canActivate(context: ExecutionContext): Promise<boolean>;
16
+ }
17
+ /**
18
+ * Guards a route with {@link SignedUrlGuard} — the request must carry a valid,
19
+ * unexpired HMAC signature (produced by `UrlGeneratorService.signedUrl`).
20
+ *
21
+ * ```ts
22
+ * @Get('download', { name: 'file.download' })
23
+ * @SignedUrl()
24
+ * download() { ... }
25
+ * ```
26
+ */
27
+ export declare function SignedUrl(): ReturnType<typeof applyDecorators>;
@@ -0,0 +1,62 @@
1
+ function _ts_decorate(decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ }
7
+ function _ts_metadata(k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ }
10
+ function _ts_param(paramIndex, decorator) {
11
+ return function(target, key) {
12
+ decorator(target, key, paramIndex);
13
+ };
14
+ }
15
+ import { Injectable, Inject, Optional } from "../../container/decorators.js";
16
+ import { CONFIG_ENV } from "../../config/config.tokens.js";
17
+ import { verifySignedUrl } from "../../crypto/signed-url.js";
18
+ import { ForbiddenException } from "../../errors/http-exception.js";
19
+ import { applyDecorators } from "../decorators.js";
20
+ import { UseGuards } from "../../pipeline/decorators.js";
21
+ import { URL_SIGNING_SECRET, resolveSigningSecret } from "./signing-secret.js";
22
+ export class SignedUrlGuard {
23
+ secretToken;
24
+ env;
25
+ constructor(secretToken, env = {}){
26
+ this.secretToken = secretToken;
27
+ this.env = env;
28
+ }
29
+ async canActivate(context) {
30
+ const request = context.switchToHttp().getRequest();
31
+ const secret = resolveSigningSecret(undefined, this.secretToken, this.env);
32
+ const valid = await verifySignedUrl(request.url, secret);
33
+ if (!valid) {
34
+ throw new ForbiddenException('Invalid or expired signed URL');
35
+ }
36
+ return true;
37
+ }
38
+ }
39
+ SignedUrlGuard = _ts_decorate([
40
+ Injectable(),
41
+ _ts_param(0, Optional()),
42
+ _ts_param(0, Inject(URL_SIGNING_SECRET)),
43
+ _ts_param(1, Optional()),
44
+ _ts_param(1, Inject(CONFIG_ENV)),
45
+ _ts_metadata("design:type", Function),
46
+ _ts_metadata("design:paramtypes", [
47
+ String,
48
+ typeof Record === "undefined" ? Object : Record
49
+ ])
50
+ ], SignedUrlGuard);
51
+ /**
52
+ * Guards a route with {@link SignedUrlGuard} — the request must carry a valid,
53
+ * unexpired HMAC signature (produced by `UrlGeneratorService.signedUrl`).
54
+ *
55
+ * ```ts
56
+ * @Get('download', { name: 'file.download' })
57
+ * @SignedUrl()
58
+ * download() { ... }
59
+ * ```
60
+ */ export function SignedUrl() {
61
+ return applyDecorators(UseGuards(SignedUrlGuard));
62
+ }
@@ -0,0 +1,20 @@
1
+ import { InjectionToken } from '../../container/types';
2
+ /**
3
+ * Optional DI token holding the HMAC secret used to sign / verify URLs. Provide
4
+ * it globally (e.g. from a platform adapter or a `@Global` module) so both
5
+ * {@link UrlGeneratorService} and the signed-URL guard can read it:
6
+ *
7
+ * ```ts
8
+ * { provide: URL_SIGNING_SECRET, useValue: env.URL_SIGNING_SECRET }
9
+ * ```
10
+ */
11
+ export declare const URL_SIGNING_SECRET: InjectionToken<string>;
12
+ /** Key read from `CONFIG_ENV` when no explicit secret / token is available. */
13
+ export declare const URL_SIGNING_SECRET_ENV_KEY = "URL_SIGNING_SECRET";
14
+ /**
15
+ * Resolve the signing secret from, in order: an explicit argument, the
16
+ * {@link URL_SIGNING_SECRET} token, then `CONFIG_ENV[URL_SIGNING_SECRET]`.
17
+ * Throws a descriptive error when none is available — signing must never fall
18
+ * back to an empty/implicit key.
19
+ */
20
+ export declare function resolveSigningSecret(explicit: string | undefined, token: string | undefined, env: Record<string, unknown> | undefined): string;
@@ -0,0 +1,24 @@
1
+ import { InjectionToken } from "../../container/types.js";
2
+ /**
3
+ * Optional DI token holding the HMAC secret used to sign / verify URLs. Provide
4
+ * it globally (e.g. from a platform adapter or a `@Global` module) so both
5
+ * {@link UrlGeneratorService} and the signed-URL guard can read it:
6
+ *
7
+ * ```ts
8
+ * { provide: URL_SIGNING_SECRET, useValue: env.URL_SIGNING_SECRET }
9
+ * ```
10
+ */ export const URL_SIGNING_SECRET = new InjectionToken('URL_SIGNING_SECRET');
11
+ /** Key read from `CONFIG_ENV` when no explicit secret / token is available. */ export const URL_SIGNING_SECRET_ENV_KEY = 'URL_SIGNING_SECRET';
12
+ /**
13
+ * Resolve the signing secret from, in order: an explicit argument, the
14
+ * {@link URL_SIGNING_SECRET} token, then `CONFIG_ENV[URL_SIGNING_SECRET]`.
15
+ * Throws a descriptive error when none is available — signing must never fall
16
+ * back to an empty/implicit key.
17
+ */ export function resolveSigningSecret(explicit, token, env) {
18
+ const fromEnv = env?.[URL_SIGNING_SECRET_ENV_KEY];
19
+ const secret = explicit ?? token ?? (typeof fromEnv === 'string' ? fromEnv : undefined);
20
+ if (!secret) {
21
+ throw new Error('No URL signing secret is available. Pass one explicitly, register a ' + '`URL_SIGNING_SECRET` provider, or set `URL_SIGNING_SECRET` in `CONFIG_ENV`.');
22
+ }
23
+ return secret;
24
+ }