@velajs/vela 1.15.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 (41) hide show
  1. package/dist/config/config.module.d.ts +8 -1
  2. package/dist/config/config.module.js +89 -42
  3. package/dist/config/config.service.d.ts +17 -4
  4. package/dist/config/config.service.js +18 -20
  5. package/dist/config/config.store.d.ts +46 -0
  6. package/dist/config/config.store.js +119 -0
  7. package/dist/config/config.tokens.d.ts +11 -0
  8. package/dist/config/config.tokens.js +12 -0
  9. package/dist/config/config.types.d.ts +37 -1
  10. package/dist/config/config.types.js +4 -1
  11. package/dist/config/index.d.ts +5 -2
  12. package/dist/config/index.js +3 -1
  13. package/dist/config/register-as.d.ts +52 -0
  14. package/dist/config/register-as.js +39 -0
  15. package/dist/{storage → crypto}/signed-url.js +5 -0
  16. package/dist/factory/bootstrap.js +10 -0
  17. package/dist/http/decorators.d.ts +18 -9
  18. package/dist/http/decorators.js +4 -1
  19. package/dist/http/index.d.ts +4 -0
  20. package/dist/http/index.js +2 -0
  21. package/dist/http/route-map.d.ts +30 -0
  22. package/dist/http/route-map.js +27 -0
  23. package/dist/http/route.manager.d.ts +2 -0
  24. package/dist/http/route.manager.js +4 -1
  25. package/dist/http/types.d.ts +2 -0
  26. package/dist/http/url/index.d.ts +4 -0
  27. package/dist/http/url/index.js +3 -0
  28. package/dist/http/url/signed-url.guard.d.ts +27 -0
  29. package/dist/http/url/signed-url.guard.js +62 -0
  30. package/dist/http/url/signing-secret.d.ts +20 -0
  31. package/dist/http/url/signing-secret.js +24 -0
  32. package/dist/http/url/url-generator.service.d.ts +52 -0
  33. package/dist/http/url/url-generator.service.js +104 -0
  34. package/dist/index.d.ts +6 -3
  35. package/dist/index.js +4 -2
  36. package/dist/openapi/document.js +2 -1
  37. package/dist/registry/types.d.ts +2 -0
  38. package/dist/storage/index.d.ts +2 -2
  39. package/dist/storage/index.js +4 -1
  40. package/package.json +6 -2
  41. /package/dist/{storage → crypto}/signed-url.d.ts +0 -0
@@ -4,7 +4,14 @@ declare const ConfigurableModuleClass: import("..").ConfigurableModuleClassType<
4
4
  isGlobal?: boolean;
5
5
  }>;
6
6
  export declare class ConfigModule extends ConfigurableModuleClass {
7
- static forRoot<T extends Record<string, unknown>>(options: ConfigModuleOptions<T> & {
7
+ /**
8
+ * Preserve (a) per-call generic inference over the flat config record and
9
+ * (b) the eager-validation contract: `validate` runs at call time so bad
10
+ * config fails fast, then is stripped so the derived `CONFIG_OPTIONS`
11
+ * provider is a passthrough (no double validation). `validateSchema` (the
12
+ * merged-config schema) is deferred to first read — its input needs env.
13
+ */
14
+ static forRoot<T extends Record<string, unknown>>(options?: ConfigModuleOptions<T> & {
8
15
  isGlobal?: boolean;
9
16
  key?: string;
10
17
  }): DynamicModule;
@@ -1,51 +1,98 @@
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
- import { Module } from "../module/decorators.js";
8
- import { ConfigurableModuleBuilder } from "../module/configurable-module.builder.js";
1
+ import { Container } from "../container/container.js";
2
+ import { defineModule } from "../module/define-module.js";
9
3
  import { ConfigService } from "./config.service.js";
4
+ import { ConfigStore } from "./config.store.js";
10
5
  import { CONFIG_OPTIONS } from "./config.tokens.js";
11
- // MODULE_OPTIONS_TOKEN carries the RAW options; CONFIG_OPTIONS stays the
12
- // distinct validated-record token, derived in @Module. The derived provider
13
- // validates the forRootAsync path lazily (its factory is deferred); forRoot
14
- // validates eagerly at call time (see the override below), matching the
15
- // long-standing contract, and passes an already-validated config through.
16
- const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN } = new ConfigurableModuleBuilder({
17
- moduleName: 'Config'
18
- }).build();
6
+ /**
7
+ * Carrier for the namespace `asProvider()` KEY providers ONLY. Split out of
8
+ * ConfigModule and marked `lazy: true` so those factories — which inject
9
+ * `CONFIG_ENV`, live per-request on edge runtimes never run at bootstrap.
10
+ * ConfigModule itself stays EAGER (see below).
11
+ */ let ConfigNamespacesModule = class ConfigNamespacesModule {
12
+ };
13
+ /**
14
+ * Build the lazy sub-module holding one KEY provider per namespace. Its `key`
15
+ * is derived from the owning ConfigModule instance key so multiple ConfigModule
16
+ * instances get distinct sub-module instances (no import-dedup collision).
17
+ */ function namespacesSubModule(load, key) {
18
+ return {
19
+ module: ConfigNamespacesModule,
20
+ key: `config-ns:${key}`,
21
+ lazy: true,
22
+ providers: load.map((namespace)=>namespace.asProvider()),
23
+ exports: load.map((namespace)=>namespace.KEY)
24
+ };
25
+ }
26
+ // Rebuilt on `defineModule` (the blessed engine — CLAUDE.md: changed modules
27
+ // use it). ConfigModule is EAGER: `ConfigService`, `ConfigStore`, and
28
+ // `CONFIG_OPTIONS` construct at bootstrap, so a `forRootAsync` async
29
+ // `useFactory` resolves in the awaited bootstrap pass and a *synchronous*
30
+ // `app.get(ConfigService)` afterwards returns the cached singleton (no drain).
31
+ //
32
+ // ONLY the namespace `asProvider()` KEY providers are deferred — they move into
33
+ // a LAZY sub-module (`ConfigNamespacesModule`) that ConfigModule imports and
34
+ // re-exports. Their factories inject `CONFIG_ENV` (live per-request on edge
35
+ // runtimes), so construction must not happen at bootstrap. `ConfigStore`
36
+ // resolves each KEY through the container on first `get()`; that first
37
+ // resolution claims + sync-drains the sub-module (namespace factories are
38
+ // synchronous, so the sync seam is safe). `resolveAllInstances` treats the KEY
39
+ // tokens as lazy-only (they belong solely to the lazy sub-module) and skips
40
+ // them at bootstrap.
41
+ const { ConfigurableModuleClass } = defineModule({
42
+ name: 'Config',
43
+ setup: ({ OPTIONS, options, key })=>{
44
+ const load = options.load ?? [];
45
+ const { validateSchema } = options;
46
+ return {
47
+ // Lazy sub-module carries the KEY providers; re-exported below so direct
48
+ // `@Inject(ns.KEY)` and `ConfigStore`'s container lookup both reach them.
49
+ imports: load.length > 0 ? [
50
+ namespacesSubModule(load, key)
51
+ ] : [],
52
+ providers: [
53
+ {
54
+ // Flat config record. `validate` is applied here for the async path;
55
+ // `forRoot` pre-validates eagerly and strips it (see the override).
56
+ provide: CONFIG_OPTIONS,
57
+ useFactory: (opts)=>opts.validate ? opts.validate(opts.config ?? {}) : opts.config ?? {},
58
+ inject: [
59
+ OPTIONS
60
+ ]
61
+ },
62
+ {
63
+ // Factory-provided so the store closes over the namespace list +
64
+ // schema; it resolves each namespace's KEY lazily via the container.
65
+ provide: ConfigStore,
66
+ useFactory: (container, config)=>new ConfigStore(container, config, load, validateSchema),
67
+ inject: [
68
+ Container,
69
+ CONFIG_OPTIONS
70
+ ]
71
+ },
72
+ ConfigService
73
+ ],
74
+ exports: [
75
+ ConfigService,
76
+ ConfigStore,
77
+ CONFIG_OPTIONS,
78
+ ...load.map((n)=>n.KEY)
79
+ ]
80
+ };
81
+ }
82
+ });
19
83
  export class ConfigModule extends ConfigurableModuleClass {
20
- // Preserve (a) per-call generic inference over the config record and (b) the
21
- // eager-validation contract: forRoot validates at call time so bad config
22
- // fails fast. `super.forRoot` runs the generated static with `this === ConfigModule`.
23
- static forRoot(options) {
24
- const validated = options.validate ? options.validate(options.config) : options.config;
25
- // Strip `validate` so the derived CONFIG_OPTIONS provider is a passthrough
26
- // (no double validation) for the already-validated config.
84
+ /**
85
+ * Preserve (a) per-call generic inference over the flat config record and
86
+ * (b) the eager-validation contract: `validate` runs at call time so bad
87
+ * config fails fast, then is stripped so the derived `CONFIG_OPTIONS`
88
+ * provider is a passthrough (no double validation). `validateSchema` (the
89
+ * merged-config schema) is deferred to first read its input needs env.
90
+ */ static forRoot(options = {}) {
91
+ const validatedConfig = options.validate ? options.validate(options.config ?? {}) : options.config;
27
92
  const { validate: _validate, ...rest } = options;
28
93
  return super.forRoot({
29
94
  ...rest,
30
- config: validated
95
+ config: validatedConfig
31
96
  });
32
97
  }
33
98
  }
34
- ConfigModule = _ts_decorate([
35
- Module({
36
- providers: [
37
- ConfigService,
38
- {
39
- provide: CONFIG_OPTIONS,
40
- useFactory: (options)=>options.validate ? options.validate(options.config) : options.config,
41
- inject: [
42
- MODULE_OPTIONS_TOKEN
43
- ]
44
- }
45
- ],
46
- exports: [
47
- ConfigService,
48
- CONFIG_OPTIONS
49
- ]
50
- })
51
- ], ConfigModule);
@@ -1,7 +1,20 @@
1
+ import { ConfigStore } from './config.store';
2
+ import type { ConfigPath, ConfigPathValue } from './config.types';
3
+ /**
4
+ * Typed, dot-notation reads over the merged config. Stays a singleton (NestJS
5
+ * parity); delegates to the {@link ConfigStore}. Pass `ConfigType<[...]>` as the
6
+ * generic to type namespaced paths (`cfg.get('database.url')` → `string`).
7
+ */
1
8
  export declare class ConfigService<T extends Record<string, unknown> = Record<string, unknown>> {
2
- private readonly config;
3
- constructor(config: T);
4
- get<V>(key: string): V | undefined;
5
- get<V>(key: string, defaultValue: V): V;
9
+ private readonly store;
10
+ constructor(store: ConfigStore);
11
+ /** Read a config value; `undefined` when absent, or the supplied default. */
12
+ get<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P> | undefined;
13
+ get<P extends ConfigPath<T>>(path: P, defaultValue: ConfigPathValue<T, P>): ConfigPathValue<T, P>;
14
+ /** Read a config value; throws when the path is absent. */
15
+ getOrThrow<P extends ConfigPath<T>>(path: P): ConfigPathValue<T, P>;
16
+ /** Whether a config path resolves to a defined value. */
17
+ has<P extends ConfigPath<T>>(path: P): boolean;
18
+ /** The full merged config object (flat record + resolved namespaces). */
6
19
  getAll(): T;
7
20
  }
@@ -12,34 +12,32 @@ function _ts_param(paramIndex, decorator) {
12
12
  decorator(target, key, paramIndex);
13
13
  };
14
14
  }
15
- import { Injectable } from "../container/decorators.js";
16
- import { Inject } from "../container/decorators.js";
17
- import { CONFIG_OPTIONS } from "./config.tokens.js";
15
+ import { Injectable, Inject } from "../container/decorators.js";
16
+ import { ConfigStore } from "./config.store.js";
18
17
  export class ConfigService {
19
- config;
20
- constructor(config){
21
- this.config = config;
18
+ store;
19
+ constructor(store){
20
+ this.store = store;
22
21
  }
23
- get(key, defaultValue) {
24
- const parts = key.split('.');
25
- let current = this.config;
26
- for (const part of parts){
27
- if (current === null || current === undefined || typeof current !== 'object') {
28
- return defaultValue;
29
- }
30
- current = current[part];
31
- }
32
- return current ?? defaultValue;
22
+ get(path, defaultValue) {
23
+ const value = this.store.get(path);
24
+ return value === undefined ? defaultValue : value;
33
25
  }
34
- getAll() {
35
- return this.config;
26
+ /** Read a config value; throws when the path is absent. */ getOrThrow(path) {
27
+ return this.store.getOrThrow(path);
28
+ }
29
+ /** Whether a config path resolves to a defined value. */ has(path) {
30
+ return this.store.has(path);
31
+ }
32
+ /** The full merged config object (flat record + resolved namespaces). */ getAll() {
33
+ return this.store.all();
36
34
  }
37
35
  }
38
36
  ConfigService = _ts_decorate([
39
37
  Injectable(),
40
- _ts_param(0, Inject(CONFIG_OPTIONS)),
38
+ _ts_param(0, Inject(ConfigStore)),
41
39
  _ts_metadata("design:type", Function),
42
40
  _ts_metadata("design:paramtypes", [
43
- typeof T === "undefined" ? Object : T
41
+ typeof ConfigStore === "undefined" ? Object : ConfigStore
44
42
  ])
45
43
  ], ConfigService);
@@ -0,0 +1,46 @@
1
+ import type { Container } from '../container/container';
2
+ import type { ConfigSchema } from './config.types';
3
+ import type { AnyConfigNamespace } from './register-as';
4
+ /**
5
+ * Singleton source of truth for merged configuration.
6
+ *
7
+ * Holds the flat `config` record plus a `namespace → KEY` map. Namespaces are
8
+ * resolved LAZILY — the KEY is pulled from the container on the first read of
9
+ * that namespace and cached — so the store never forces namespace factories to
10
+ * materialize at construction. Resolution uses the synchronous container seam
11
+ * (`resolve`), which stays compatible with lazy modules.
12
+ *
13
+ * The merged shape is `{ ...config, [ns]: value }` — a namespace shadows a flat
14
+ * key of the same name.
15
+ *
16
+ * Constructed via a factory provider in {@link ConfigModule} so it can close
17
+ * over the `load` list and `validateSchema` without extra tokens.
18
+ */
19
+ export declare class ConfigStore {
20
+ private readonly container;
21
+ private readonly config;
22
+ private readonly validateSchema?;
23
+ private readonly namespaceKeys;
24
+ private readonly cache;
25
+ private validated;
26
+ private validationError;
27
+ constructor(container: Pick<Container, 'resolve'>, config?: Record<string, unknown>, namespaces?: ReadonlyArray<AnyConfigNamespace>, validateSchema?: ConfigSchema | undefined);
28
+ /** Read a value by dot-notation path; `undefined` if absent. */
29
+ get(path: string): unknown;
30
+ /** Read a value by dot-notation path; throws if absent. */
31
+ getOrThrow(path: string): unknown;
32
+ /** Whether a dot-notation path resolves to a defined value. */
33
+ has(path: string): boolean;
34
+ /** The full merged config object (flat record + resolved namespaces). */
35
+ all(): Record<string, unknown>;
36
+ private read;
37
+ /** Resolve a namespace's KEY through the container on first use, then cache. */
38
+ private resolveNamespace;
39
+ private merged;
40
+ /**
41
+ * Validate the merged config once, on first read. Deferred (not at bootstrap)
42
+ * because the merged shape needs namespace factories — and their env — which
43
+ * are only available after bootstrap on edge runtimes.
44
+ */
45
+ private ensureValidated;
46
+ }
@@ -0,0 +1,119 @@
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
+ import { Injectable } from "../container/decorators.js";
11
+ /** Prototype-pollution guard for dot-notation segments. */ const DANGEROUS_KEYS = new Set([
12
+ '__proto__',
13
+ 'constructor',
14
+ 'prototype'
15
+ ]);
16
+ function runSchema(schema, config) {
17
+ if (typeof schema === 'function') {
18
+ schema(config);
19
+ return;
20
+ }
21
+ if (schema && typeof schema.parse === 'function') {
22
+ schema.parse(config);
23
+ return;
24
+ }
25
+ throw new Error('ConfigModule `validateSchema` must be a function or expose a `parse()` method');
26
+ }
27
+ export class ConfigStore {
28
+ container;
29
+ config;
30
+ validateSchema;
31
+ namespaceKeys = new Map();
32
+ cache = new Map();
33
+ validated = false;
34
+ validationError;
35
+ constructor(container, config = {}, namespaces = [], validateSchema){
36
+ this.container = container;
37
+ this.config = config;
38
+ this.validateSchema = validateSchema;
39
+ for (const ns of namespaces){
40
+ this.namespaceKeys.set(ns.namespace, ns.KEY);
41
+ }
42
+ }
43
+ /** Read a value by dot-notation path; `undefined` if absent. */ get(path) {
44
+ this.ensureValidated();
45
+ return this.read(path);
46
+ }
47
+ /** Read a value by dot-notation path; throws if absent. */ getOrThrow(path) {
48
+ const value = this.get(path);
49
+ if (value === undefined) {
50
+ throw new Error(`Configuration key "${path}" was not found`);
51
+ }
52
+ return value;
53
+ }
54
+ /** Whether a dot-notation path resolves to a defined value. */ has(path) {
55
+ this.ensureValidated();
56
+ return this.read(path) !== undefined;
57
+ }
58
+ /** The full merged config object (flat record + resolved namespaces). */ all() {
59
+ this.ensureValidated();
60
+ return this.merged();
61
+ }
62
+ read(path) {
63
+ const [head, ...rest] = path.split('.');
64
+ if (head === undefined || DANGEROUS_KEYS.has(head)) return undefined;
65
+ let current = this.namespaceKeys.has(head) ? this.resolveNamespace(head) : this.config[head];
66
+ for (const key of rest){
67
+ if (DANGEROUS_KEYS.has(key)) return undefined;
68
+ if (current === null || current === undefined || typeof current !== 'object') {
69
+ return undefined;
70
+ }
71
+ current = current[key];
72
+ }
73
+ return current;
74
+ }
75
+ /** Resolve a namespace's KEY through the container on first use, then cache. */ resolveNamespace(namespace) {
76
+ if (this.cache.has(namespace)) return this.cache.get(namespace);
77
+ const key = this.namespaceKeys.get(namespace);
78
+ const value = key === undefined ? undefined : this.container.resolve(key);
79
+ this.cache.set(namespace, value);
80
+ return value;
81
+ }
82
+ merged() {
83
+ const out = {
84
+ ...this.config
85
+ };
86
+ for (const namespace of this.namespaceKeys.keys()){
87
+ out[namespace] = this.resolveNamespace(namespace);
88
+ }
89
+ return out;
90
+ }
91
+ /**
92
+ * Validate the merged config once, on first read. Deferred (not at bootstrap)
93
+ * because the merged shape needs namespace factories — and their env — which
94
+ * are only available after bootstrap on edge runtimes.
95
+ */ ensureValidated() {
96
+ if (this.validated) {
97
+ if (this.validationError) throw this.validationError; // fail-closed on every read
98
+ return;
99
+ }
100
+ this.validated = true; // run the schema exactly once
101
+ if (!this.validateSchema) return;
102
+ try {
103
+ runSchema(this.validateSchema, this.merged());
104
+ } catch (err) {
105
+ this.validationError = err;
106
+ throw err;
107
+ }
108
+ }
109
+ }
110
+ ConfigStore = _ts_decorate([
111
+ Injectable(),
112
+ _ts_metadata("design:type", Function),
113
+ _ts_metadata("design:paramtypes", [
114
+ typeof Pick === "undefined" ? Object : Pick,
115
+ typeof Record === "undefined" ? Object : Record,
116
+ typeof ReadonlyArray === "undefined" ? Object : ReadonlyArray,
117
+ typeof ConfigSchema === "undefined" ? Object : ConfigSchema
118
+ ])
119
+ ], ConfigStore);
@@ -1,2 +1,13 @@
1
1
  import { InjectionToken } from '../container/types';
2
2
  export declare const CONFIG_OPTIONS: InjectionToken<Record<string, unknown>>;
3
+ /**
4
+ * Ambient environment record consumed by config-namespace factories
5
+ * (`registerAs`). It is the multi-runtime substitute for stratal's
6
+ * Cloudflare-specific `env` token: core never touches `process.env`.
7
+ *
8
+ * The token self-provides `{}` by default (so factories resolve even with no
9
+ * platform binding), and stays OVERRIDABLE — a platform adapter
10
+ * (`@velajs/cloudflare`) or a future `./config-node` subpath provides a global
11
+ * `CONFIG_ENV` and that registration wins over the default.
12
+ */
13
+ export declare const CONFIG_ENV: InjectionToken<Record<string, unknown>>;
@@ -1,2 +1,14 @@
1
1
  import { InjectionToken } from "../container/types.js";
2
2
  export const CONFIG_OPTIONS = new InjectionToken('CONFIG_OPTIONS');
3
+ /**
4
+ * Ambient environment record consumed by config-namespace factories
5
+ * (`registerAs`). It is the multi-runtime substitute for stratal's
6
+ * Cloudflare-specific `env` token: core never touches `process.env`.
7
+ *
8
+ * The token self-provides `{}` by default (so factories resolve even with no
9
+ * platform binding), and stays OVERRIDABLE — a platform adapter
10
+ * (`@velajs/cloudflare`) or a future `./config-node` subpath provides a global
11
+ * `CONFIG_ENV` and that registration wins over the default.
12
+ */ export const CONFIG_ENV = new InjectionToken('CONFIG_ENV', {
13
+ factory: ()=>({})
14
+ });
@@ -1,5 +1,41 @@
1
+ import type { AnyConfigNamespace } from './register-as';
2
+ /**
3
+ * A schema for validating the fully-merged config. Kept structural so zod stays
4
+ * an OPTIONAL peer — a zod schema (has `.parse`) or a plain function both work;
5
+ * core never imports zod. Throwing rejects the config.
6
+ */
7
+ export type ConfigSchema = ((config: Record<string, unknown>) => unknown) | {
8
+ parse: (config: unknown) => unknown;
9
+ };
10
+ /**
11
+ * Options for {@link ConfigModule}.
12
+ *
13
+ * NOTE: `load` and `validateSchema` are **`forRoot`-only** — they are read
14
+ * structurally from the call-time options bag. On the `forRootAsync` path the
15
+ * options come from a DI-resolved factory, so only the flat `config` record is
16
+ * supported there (async namespaces are out of scope; declare them via
17
+ * `forRoot`).
18
+ */
1
19
  export interface ConfigModuleOptions<T extends Record<string, unknown> = Record<string, unknown>> {
2
- config: T;
20
+ /** Flat config record — the pre-namespace `config` path (still supported). */
21
+ config?: T;
22
+ /** Config namespaces created via `registerAs()`, merged under their namespace name. `forRoot`-only. */
23
+ load?: AnyConfigNamespace[];
24
+ /** Eager validator for the flat `config` record; runs at `forRoot()` call time. */
3
25
  validate?: (config: T) => T;
26
+ /** Schema validating the MERGED config (flat + namespaces); runs lazily on first read. `forRoot`-only. */
27
+ validateSchema?: ConfigSchema;
4
28
  isGlobal?: boolean;
5
29
  }
30
+ /**
31
+ * All valid dot-notation paths of a config object type.
32
+ * @example `ConfigPath<{ database: { url: string } }>` → `'database' | 'database.url'`
33
+ */
34
+ export type ConfigPath<T> = {
35
+ [K in keyof T & string]: T[K] extends Record<string, unknown> ? K | `${K}.${ConfigPath<T[K]>}` : K;
36
+ }[keyof T & string];
37
+ /**
38
+ * The value type at a dot-notation path.
39
+ * @example `ConfigPathValue<{ database: { url: string } }, 'database.url'>` → `string`
40
+ */
41
+ export type ConfigPathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? T[K] extends Record<string, unknown> ? ConfigPathValue<T[K], Rest> : never : never : P extends keyof T ? T[P] : never;
@@ -1 +1,4 @@
1
- export { };
1
+ /**
2
+ * The value type at a dot-notation path.
3
+ * @example `ConfigPathValue<{ database: { url: string } }, 'database.url'>` → `string`
4
+ */ export { };
@@ -1,4 +1,7 @@
1
1
  export { ConfigModule } from './config.module';
2
2
  export { ConfigService } from './config.service';
3
- export { CONFIG_OPTIONS } from './config.tokens';
4
- export type { ConfigModuleOptions } from './config.types';
3
+ export { ConfigStore } from './config.store';
4
+ export { CONFIG_OPTIONS, CONFIG_ENV } from './config.tokens';
5
+ export { registerAs } from './register-as';
6
+ export type { ConfigNamespace, AnyConfigNamespace, InferConfigType, ConfigType } from './register-as';
7
+ export type { ConfigModuleOptions, ConfigSchema, ConfigPath, ConfigPathValue } from './config.types';
@@ -1,3 +1,5 @@
1
1
  export { ConfigModule } from "./config.module.js";
2
2
  export { ConfigService } from "./config.service.js";
3
- export { CONFIG_OPTIONS } from "./config.tokens.js";
3
+ export { ConfigStore } from "./config.store.js";
4
+ export { CONFIG_OPTIONS, CONFIG_ENV } from "./config.tokens.js";
5
+ export { registerAs } from "./register-as.js";
@@ -0,0 +1,52 @@
1
+ import type { InjectionToken, ProviderOptions } from '../container/types';
2
+ /**
3
+ * The result of {@link registerAs}: a namespaced config factory plus the DI
4
+ * token it is provided under.
5
+ */
6
+ export interface ConfigNamespace<TKey extends string = string, TEnv = Record<string, unknown>, TConfig extends object = object> {
7
+ /** Injection token for this namespace's resolved config (`Symbol.for('vela:config:<ns>')`). */
8
+ readonly KEY: InjectionToken<TConfig>;
9
+ /** The namespace name (e.g. `'database'`). */
10
+ readonly namespace: TKey;
11
+ /** Factory receiving the ambient env and returning the namespace config. */
12
+ readonly factory: (env: TEnv) => TConfig;
13
+ /** Provider registration injecting {@link CONFIG_ENV} — spread into a module's providers. */
14
+ asProvider(): ProviderOptions<TConfig>;
15
+ }
16
+ /** Any config namespace — structural typing for the module `load` list. */
17
+ export type AnyConfigNamespace = ConfigNamespace<string, any, object>;
18
+ /**
19
+ * Create a namespaced configuration factory (NestJS `registerAs` parity).
20
+ *
21
+ * The `KEY` is minted with `Symbol.for` so it keeps a stable identity across
22
+ * module re-evaluation (HMR / repeated imports) — a fresh `InjectionToken`
23
+ * would mint a new identity each eval and break dedup. It is typed as
24
+ * `InjectionToken<TConfig>` purely for injection-site DX.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * export const dbConfig = registerAs('database', (env: Env) => ({
29
+ * url: env.DATABASE_URL,
30
+ * pool: 10,
31
+ * }));
32
+ * // ConfigModule.forRoot({ load: [dbConfig] });
33
+ * // cfg.get('database.url'); // via ConfigService
34
+ * // constructor(@Inject(dbConfig.KEY) db: InferConfigType<typeof dbConfig>) {}
35
+ * ```
36
+ */
37
+ export declare function registerAs<TKey extends string, TEnv, TConfig extends object>(namespace: TKey, factory: (env: TEnv) => TConfig): ConfigNamespace<TKey, TEnv, TConfig>;
38
+ /**
39
+ * Extract a namespace's config shape from a {@link registerAs} result.
40
+ *
41
+ * @example `type Db = InferConfigType<typeof dbConfig>` → `{ url: string; pool: number }`
42
+ */
43
+ export type InferConfigType<T> = T extends ConfigNamespace<string, any, infer C> ? C : never;
44
+ /**
45
+ * Zero-codegen typed config shape from a tuple of namespaces — pass as the
46
+ * `ConfigService` generic to type dot-notation reads.
47
+ *
48
+ * @example `ConfigService<ConfigType<[typeof dbConfig, typeof mailConfig]>>`
49
+ */
50
+ export type ConfigType<L extends readonly AnyConfigNamespace[]> = {
51
+ [N in L[number] as N['namespace']]: InferConfigType<N>;
52
+ };
@@ -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,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 { };
@@ -21,6 +21,8 @@ export interface RouteDescription {
21
21
  controller: string;
22
22
  handler: string;
23
23
  version?: number;
24
+ /** Route name (`@Get(path, { name })`) — the URL-generation / `operationId` key. */
25
+ name?: string;
24
26
  }
25
27
  export interface RouteManagerOptions {
26
28
  getClientIp?: (c: Context) => string | null;
@@ -388,7 +388,10 @@ export class RouteManager {
388
388
  path: fullPath || '/',
389
389
  controller: controller.name,
390
390
  handler: String(route.handlerName),
391
- version: versionsForPaths[pathIndex]
391
+ version: versionsForPaths[pathIndex],
392
+ ...route.name !== undefined ? {
393
+ name: route.name
394
+ } : {}
392
395
  });
393
396
  }
394
397
  }
@@ -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
+ }
@@ -0,0 +1,52 @@
1
+ import { RouteManager } from '../route.manager';
2
+ import type { RouteName, RouteParams } from '../route-map';
3
+ /** Value accepted for a path param or query entry. */
4
+ type UrlParamValue = string | number | boolean;
5
+ /** Extra options for {@link UrlGeneratorService.urlFor}. */
6
+ export interface UrlForOptions {
7
+ /** Additional query-string entries merged after any leftover params. */
8
+ query?: Record<string, UrlParamValue>;
9
+ }
10
+ /** Options for {@link UrlGeneratorService.signedUrl}. */
11
+ export interface SignedUrlGenerateOptions {
12
+ /** Time-to-live in seconds; enforced on verification via the `expires` param. */
13
+ expiresIn?: number;
14
+ /** Override the signing secret (else the `URL_SIGNING_SECRET` token / `CONFIG_ENV`). */
15
+ secret?: string;
16
+ }
17
+ /**
18
+ * Builds URLs for named routes (Laravel-flavoured `route()` ergonomics).
19
+ *
20
+ * Route descriptions are read LAZILY from the {@link RouteManager} on first use
21
+ * — post-build, so composition (global prefix + version + controller prefix) is
22
+ * already baked into each path — and it never instantiates controllers, so it
23
+ * plays nicely with lazy modules. Registered as an app-level singleton by
24
+ * `bootstrap`, so it is injectable anywhere.
25
+ */
26
+ export declare class UrlGeneratorService {
27
+ private readonly routeManager;
28
+ private readonly secretToken?;
29
+ private readonly env;
30
+ private routeMap;
31
+ constructor(routeManager: RouteManager, secretToken?: string | undefined, env?: Record<string, unknown>);
32
+ /**
33
+ * Build the path for a named route, filling `:param` placeholders from
34
+ * `params`. Params that don't match a placeholder become query-string
35
+ * entries; `opts.query` is merged after them.
36
+ *
37
+ * @throws if no route carries `name`, or a required `:param` is missing.
38
+ */
39
+ urlFor<N extends RouteName>(name: N, params?: RouteParams<N>, opts?: UrlForOptions): string;
40
+ /**
41
+ * Build a named-route URL and HMAC-sign it (path + query). The secret comes
42
+ * from `options.secret`, else the {@link URL_SIGNING_SECRET} token, else
43
+ * `CONFIG_ENV`; a descriptive error is thrown when none is available.
44
+ */
45
+ signedUrl<N extends RouteName>(name: N, params?: RouteParams<N>, options?: SignedUrlGenerateOptions): Promise<string>;
46
+ /**
47
+ * Lazily index named route descriptions. Rebuilt while empty (routes may not
48
+ * be built yet), cached once populated.
49
+ */
50
+ private routes;
51
+ }
52
+ export {};
@@ -0,0 +1,104 @@
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 { signUrl } from "../../crypto/signed-url.js";
18
+ import { RouteManager } from "../route.manager.js";
19
+ import { URL_SIGNING_SECRET, resolveSigningSecret } from "./signing-secret.js";
20
+ export class UrlGeneratorService {
21
+ routeManager;
22
+ secretToken;
23
+ env;
24
+ routeMap = null;
25
+ constructor(routeManager, secretToken, env = {}){
26
+ this.routeManager = routeManager;
27
+ this.secretToken = secretToken;
28
+ this.env = env;
29
+ }
30
+ /**
31
+ * Build the path for a named route, filling `:param` placeholders from
32
+ * `params`. Params that don't match a placeholder become query-string
33
+ * entries; `opts.query` is merged after them.
34
+ *
35
+ * @throws if no route carries `name`, or a required `:param` is missing.
36
+ */ urlFor(name, params, opts) {
37
+ const description = this.routes().get(name);
38
+ if (!description) {
39
+ throw new Error(`No route named "${String(name)}" was found. Give the route a name ` + `via \`@Get(path, { name: '${String(name)}' })\`.`);
40
+ }
41
+ const provided = {
42
+ ...params ?? {}
43
+ };
44
+ const consumed = new Set();
45
+ const path = description.path.replace(/:([A-Za-z0-9_]+)/g, (_match, key)=>{
46
+ const value = provided[key];
47
+ if (value === undefined || value === null) {
48
+ throw new Error(`Missing route param "${key}" for route "${String(name)}".`);
49
+ }
50
+ consumed.add(key);
51
+ return encodeURIComponent(String(value));
52
+ });
53
+ const query = new URLSearchParams();
54
+ for (const [key, value] of Object.entries(provided)){
55
+ if (consumed.has(key) || value === undefined || value === null) continue;
56
+ query.set(key, String(value));
57
+ }
58
+ for (const [key, value] of Object.entries(opts?.query ?? {})){
59
+ if (value === undefined || value === null) continue;
60
+ query.set(key, String(value));
61
+ }
62
+ const qs = query.toString();
63
+ return qs ? `${path}?${qs}` : path;
64
+ }
65
+ /**
66
+ * Build a named-route URL and HMAC-sign it (path + query). The secret comes
67
+ * from `options.secret`, else the {@link URL_SIGNING_SECRET} token, else
68
+ * `CONFIG_ENV`; a descriptive error is thrown when none is available.
69
+ */ async signedUrl(name, params, options = {}) {
70
+ const url = this.urlFor(name, params);
71
+ const secret = resolveSigningSecret(options.secret, this.secretToken, this.env);
72
+ return signUrl(url, secret, options.expiresIn !== undefined ? {
73
+ expiresIn: options.expiresIn
74
+ } : undefined);
75
+ }
76
+ /**
77
+ * Lazily index named route descriptions. Rebuilt while empty (routes may not
78
+ * be built yet), cached once populated.
79
+ */ routes() {
80
+ if (this.routeMap && this.routeMap.size > 0) return this.routeMap;
81
+ const map = new Map();
82
+ for (const description of this.routeManager.getRouteDescriptions()){
83
+ if (description.name && !map.has(description.name)) {
84
+ map.set(description.name, description);
85
+ }
86
+ }
87
+ this.routeMap = map;
88
+ return map;
89
+ }
90
+ }
91
+ UrlGeneratorService = _ts_decorate([
92
+ Injectable(),
93
+ _ts_param(0, Inject(RouteManager)),
94
+ _ts_param(1, Optional()),
95
+ _ts_param(1, Inject(URL_SIGNING_SECRET)),
96
+ _ts_param(2, Optional()),
97
+ _ts_param(2, Inject(CONFIG_ENV)),
98
+ _ts_metadata("design:type", Function),
99
+ _ts_metadata("design:paramtypes", [
100
+ typeof RouteManager === "undefined" ? Object : RouteManager,
101
+ String,
102
+ typeof Record === "undefined" ? Object : Record
103
+ ])
104
+ ], UrlGeneratorService);
package/dist/index.d.ts CHANGED
@@ -10,14 +10,17 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
10
10
  export type { Type, Token, InferToken, InferTokens, InjectableOptions, ProviderOptions, ModuleScope, ModuleDescription, ContainerOptions, Diagnostics, } from './container/index';
11
11
  export type { RouteDescription } from './http/route.manager';
12
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from './constants';
13
- 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, createLazyParamDecorator, applyDecorators, } from './http/index';
13
+ 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, createLazyParamDecorator, applyDecorators, UrlGeneratorService, SignedUrlGuard, SignedUrl, URL_SIGNING_SECRET, } from './http/index';
14
+ export type { RouteOptions, UrlForOptions, SignedUrlGenerateOptions, VelaRouteMap, RouteName, RouteParams, } from './http/index';
15
+ export { signUrl, verifySignedUrl } from './crypto/signed-url';
16
+ export type { SignedUrlOptions } from './crypto/signed-url';
14
17
  export { REQUEST_CONTEXT } from './http/request-context';
15
18
  export type { RequestContext } from './http/request-context';
16
19
  export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext, } from './http/ambient';
17
20
  export { Logger, LogLevel } from './services/index';
18
21
  export type { LoggerService, ContextProvider, Writer, LoggerLevelName } from './services/index';
19
- export { ConfigModule, ConfigService, CONFIG_OPTIONS } from './config/index';
20
- export type { ConfigModuleOptions } from './config/index';
22
+ export { ConfigModule, ConfigService, ConfigStore, CONFIG_OPTIONS, CONFIG_ENV, registerAs, } from './config/index';
23
+ export type { ConfigModuleOptions, ConfigSchema, ConfigNamespace, AnyConfigNamespace, InferConfigType, ConfigType, ConfigPath, ConfigPathValue, } from './config/index';
21
24
  export { HttpModule, HttpService, HTTP_MODULE_OPTIONS, HttpRequestException } from './fetch/index';
22
25
  export type { HttpModuleOptions, HttpResponse, HttpRequestConfig } from './fetch/index';
23
26
  export { CorsModule, CORS_OPTIONS } from './cors/index';
package/dist/index.js CHANGED
@@ -11,7 +11,9 @@ export { Container, Injectable, Inject, Optional, InjectionToken, ForwardRef, fo
11
11
  // Constants
12
12
  export { METADATA_KEYS, HttpMethod, ParamType, Scope } from "./constants.js";
13
13
  // HTTP Decorators
14
- 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, createLazyParamDecorator, applyDecorators } from "./http/index.js";
14
+ 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, createLazyParamDecorator, applyDecorators, UrlGeneratorService, SignedUrlGuard, SignedUrl, URL_SIGNING_SECRET } from "./http/index.js";
15
+ // Edge-safe HMAC signed-URL primitives (also re-exported from `@velajs/vela/storage`)
16
+ export { signUrl, verifySignedUrl } from "./crypto/signed-url.js";
15
17
  // Request-scoped context primitive
16
18
  export { REQUEST_CONTEXT } from "./http/request-context.js";
17
19
  // Opt-in ambient container access (ALS via hono/context-storage)
@@ -19,7 +21,7 @@ export { enableAmbientContainer, getCurrentContainer, getCurrentRequestContext }
19
21
  // Services
20
22
  export { Logger, LogLevel } from "./services/index.js";
21
23
  // Config
22
- export { ConfigModule, ConfigService, CONFIG_OPTIONS } from "./config/index.js";
24
+ export { ConfigModule, ConfigService, ConfigStore, CONFIG_OPTIONS, CONFIG_ENV, registerAs } from "./config/index.js";
23
25
  // HTTP Client
24
26
  export { HttpModule, HttpService, HTTP_MODULE_OPTIONS, HttpRequestException } from "./fetch/index.js";
25
27
  // CORS
@@ -182,7 +182,8 @@ function buildOperation(controller, route, pathString, registry) {
182
182
  if (requestBody) operation.requestBody = requestBody;
183
183
  if (docMeta?.summary) operation.summary = docMeta.summary;
184
184
  if (docMeta?.description) operation.description = docMeta.description;
185
- if (docMeta?.operationId) operation.operationId = docMeta.operationId;
185
+ const operationId = docMeta?.operationId ?? route.name;
186
+ if (operationId) operation.operationId = operationId;
186
187
  if (docMeta?.deprecated) operation.deprecated = docMeta.deprecated;
187
188
  if (mergedTags.length > 0) operation.tags = mergedTags;
188
189
  return operation;
@@ -21,6 +21,8 @@ export interface RouteDefinition {
21
21
  path: string;
22
22
  handlerName: string | symbol;
23
23
  version?: number | number[];
24
+ /** Route name for URL generation / OpenAPI operationId (`@Get(path, { name })`). */
25
+ name?: string;
24
26
  }
25
27
  export interface ParameterMetadata {
26
28
  index: number;
@@ -1,4 +1,4 @@
1
- export { signUrl, verifySignedUrl } from './signed-url';
2
- export type { SignedUrlOptions } from './signed-url';
1
+ export { signUrl, verifySignedUrl } from '../crypto/signed-url';
2
+ export type { SignedUrlOptions } from '../crypto/signed-url';
3
3
  export { expandPathTemplate, joinStoragePath } from './path-template';
4
4
  export type { StorageBody, StorageDriver, PresignMethod, UploadOptions, UploadResult, DownloadResult, PresignedUrlResult, } from './storage.types';
@@ -1,2 +1,5 @@
1
- export { signUrl, verifySignedUrl } from "./signed-url.js";
1
+ // The HMAC signed-URL util now lives in `src/crypto/` (core consumes it there
2
+ // for the URL generator + guard). Re-exported here so `@velajs/vela/storage`'s
3
+ // public API is unchanged for existing consumers.
4
+ export { signUrl, verifySignedUrl } from "../crypto/signed-url.js";
2
5
  export { expandPathTemplate, joinStoragePath } from "./path-template.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velajs/vela",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "NestJS-compatible framework for edge runtimes, powered by Hono",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -102,6 +102,7 @@
102
102
  "@vitest/runner": "^4.1.9",
103
103
  "@vitest/snapshot": "^4.1.9",
104
104
  "intl-messageformat": "^11.2.9",
105
+ "typedoc": "~0.28.19",
105
106
  "typescript": "^6.0.3",
106
107
  "unplugin-swc": "^1.5.9",
107
108
  "vitest": "^4.1.9",
@@ -112,6 +113,9 @@
112
113
  "test": "vitest run",
113
114
  "test:workers": "vitest run --config vitest.config.workers.ts",
114
115
  "test:workers:als": "vitest run --config vitest.config.workers-als.ts",
115
- "typecheck": "tsc --noEmit"
116
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.type-tests.json",
117
+ "check:skill": "node scripts/check-skill.mjs",
118
+ "docs": "typedoc",
119
+ "docs:check": "typedoc --emit none"
116
120
  }
117
121
  }
File without changes