@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.
- package/CHANGELOG.md +25 -0
- package/dist/application.d.ts +10 -1
- package/dist/application.js +12 -0
- package/dist/config/config.module.d.ts +8 -1
- package/dist/config/config.module.js +89 -42
- package/dist/config/config.service.d.ts +17 -4
- package/dist/config/config.service.js +18 -20
- package/dist/config/config.store.d.ts +46 -0
- package/dist/config/config.store.js +119 -0
- package/dist/config/config.tokens.d.ts +11 -0
- package/dist/config/config.tokens.js +12 -0
- package/dist/config/config.types.d.ts +37 -1
- package/dist/config/config.types.js +4 -1
- package/dist/config/index.d.ts +5 -2
- package/dist/config/index.js +3 -1
- package/dist/config/register-as.d.ts +52 -0
- package/dist/config/register-as.js +39 -0
- package/dist/container/container.d.ts +8 -1
- package/dist/container/container.js +39 -1
- package/dist/container/index.d.ts +2 -2
- package/dist/container/index.js +1 -1
- package/dist/container/types.d.ts +23 -0
- package/dist/container/types.js +6 -1
- package/dist/{storage → crypto}/signed-url.js +5 -0
- package/dist/factory/bootstrap.js +10 -0
- package/dist/http/decorators.d.ts +18 -9
- package/dist/http/decorators.js +4 -1
- package/dist/http/index.d.ts +4 -0
- package/dist/http/index.js +2 -0
- package/dist/http/route-map.d.ts +30 -0
- package/dist/http/route-map.js +27 -0
- package/dist/http/route.manager.d.ts +24 -0
- package/dist/http/route.manager.js +28 -1
- package/dist/http/types.d.ts +2 -0
- package/dist/http/url/index.d.ts +4 -0
- package/dist/http/url/index.js +3 -0
- package/dist/http/url/signed-url.guard.d.ts +27 -0
- package/dist/http/url/signed-url.guard.js +62 -0
- package/dist/http/url/signing-secret.d.ts +20 -0
- package/dist/http/url/signing-secret.js +24 -0
- package/dist/http/url/url-generator.service.d.ts +52 -0
- package/dist/http/url/url-generator.service.js +104 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +5 -3
- package/dist/openapi/document.js +2 -1
- package/dist/registry/types.d.ts +2 -0
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.js +4 -1
- package/package.json +6 -2
- /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';
|
package/dist/container/index.js
CHANGED
|
@@ -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;
|
package/dist/container/types.js
CHANGED
|
@@ -18,7 +18,12 @@ export class ForwardRef {
|
|
|
18
18
|
export function forwardRef(factory) {
|
|
19
19
|
return new ForwardRef(factory);
|
|
20
20
|
}
|
|
21
|
-
|
|
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
|
-
|
|
35
|
-
export
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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;
|
package/dist/http/decorators.js
CHANGED
|
@@ -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
|
};
|
package/dist/http/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/http/index.js
CHANGED
|
@@ -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
|
}
|
package/dist/http/types.d.ts
CHANGED
|
@@ -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,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
|
+
}
|