@velajs/vela 1.14.0 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/application.d.ts +10 -1
  3. package/dist/application.js +12 -0
  4. package/dist/config/config.module.d.ts +8 -1
  5. package/dist/config/config.module.js +89 -42
  6. package/dist/config/config.service.d.ts +17 -4
  7. package/dist/config/config.service.js +18 -20
  8. package/dist/config/config.store.d.ts +46 -0
  9. package/dist/config/config.store.js +119 -0
  10. package/dist/config/config.tokens.d.ts +11 -0
  11. package/dist/config/config.tokens.js +12 -0
  12. package/dist/config/config.types.d.ts +37 -1
  13. package/dist/config/config.types.js +4 -1
  14. package/dist/config/index.d.ts +5 -2
  15. package/dist/config/index.js +3 -1
  16. package/dist/config/register-as.d.ts +52 -0
  17. package/dist/config/register-as.js +39 -0
  18. package/dist/container/container.d.ts +8 -1
  19. package/dist/container/container.js +39 -1
  20. package/dist/container/index.d.ts +2 -2
  21. package/dist/container/index.js +1 -1
  22. package/dist/container/types.d.ts +23 -0
  23. package/dist/container/types.js +6 -1
  24. package/dist/{storage → crypto}/signed-url.js +5 -0
  25. package/dist/factory/bootstrap.js +10 -0
  26. package/dist/http/decorators.d.ts +18 -9
  27. package/dist/http/decorators.js +4 -1
  28. package/dist/http/index.d.ts +4 -0
  29. package/dist/http/index.js +2 -0
  30. package/dist/http/route-map.d.ts +30 -0
  31. package/dist/http/route-map.js +27 -0
  32. package/dist/http/route.manager.d.ts +24 -0
  33. package/dist/http/route.manager.js +28 -1
  34. package/dist/http/types.d.ts +2 -0
  35. package/dist/http/url/index.d.ts +4 -0
  36. package/dist/http/url/index.js +3 -0
  37. package/dist/http/url/signed-url.guard.d.ts +27 -0
  38. package/dist/http/url/signed-url.guard.js +62 -0
  39. package/dist/http/url/signing-secret.d.ts +20 -0
  40. package/dist/http/url/signing-secret.js +24 -0
  41. package/dist/http/url/url-generator.service.d.ts +52 -0
  42. package/dist/http/url/url-generator.service.js +104 -0
  43. package/dist/index.d.ts +9 -5
  44. package/dist/index.js +5 -3
  45. package/dist/openapi/document.js +2 -1
  46. package/dist/registry/types.d.ts +2 -0
  47. package/dist/storage/index.d.ts +2 -2
  48. package/dist/storage/index.js +4 -1
  49. package/package.json +6 -2
  50. /package/dist/{storage → crypto}/signed-url.d.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.15.0 (2026-07-04)
4
+
5
+ Introspection seams for `@velajs/cli` (roadmap phase 3, CLI introspection):
6
+ serializable, zero-instantiation views of the app the CLI renders instead of
7
+ re-deriving framework internals.
8
+
9
+ ### Added
10
+
11
+ - **`app.describeRoutes(): RouteDescription[]`** — every explicit controller
12
+ route exactly as `build()` registered it: method AS DECLARED (`@Head()`
13
+ reports HEAD even though Hono serves it under GET), fully composed path
14
+ (global prefix + version segment + controller prefix + route path),
15
+ controller name, handler name, version. Contributed routes
16
+ (`RouteContributor`/CRUD) mount directly on Hono and are observable via
17
+ `getHonoApp().routes`.
18
+ - **`app.getGlobalPrefix()`** — the prefix in effect ('' when none); also
19
+ what `openapi` tooling threads into `createOpenApiDocument`.
20
+ - **`Container.getModuleDescriptions(): ModuleDescription[]`** — the loaded
21
+ module graph (moduleId, imports, isGlobal, lazy, provider/export token
22
+ labels), load order plus the `__root__` bucket; reads registration state
23
+ only — safe pre/post bootstrap, never constructs, never claims lazy
24
+ modules.
25
+ - **`describeToken(token)`** — the token-label helper vela's own errors use,
26
+ exported for tooling.
27
+
3
28
  ## 1.14.0 (2026-07-04)
4
29
 
5
30
  First-party `QueueModule` (roadmap phase 3, "the openness proof"): a whole
@@ -2,7 +2,7 @@ import type { Hono } from 'hono';
2
2
  import type { Container } from './container/container';
3
3
  import type { Token } from './container/types';
4
4
  import { EntrypointRegistry } from './entrypoint/entrypoint.registry';
5
- import type { RouteManager } from './http/route.manager';
5
+ import type { RouteDescription, RouteManager } from './http/route.manager';
6
6
  import type { MountOpenApiOptions } from './openapi/types';
7
7
  import type { FilterType, GuardType, InterceptorType, PipeType } from './registry/types';
8
8
  export declare class VelaApplication {
@@ -25,6 +25,15 @@ export declare class VelaApplication {
25
25
  setInstances(instances: unknown[]): void;
26
26
  get<T>(token: Token<T>): T;
27
27
  getHonoApp(): Hono;
28
+ /**
29
+ * Every explicit controller route as the framework registered it (fully
30
+ * composed paths — the `vela route list` seam). Requires built routes;
31
+ * contributed routes (`@velajs/crud`) mount directly on Hono and are
32
+ * observable via `getHonoApp().routes` instead.
33
+ */
34
+ describeRoutes(): RouteDescription[];
35
+ /** The global route prefix in effect ('' when none). */
36
+ getGlobalPrefix(): string;
28
37
  useGlobalPipes(...pipes: PipeType[]): this;
29
38
  useGlobalGuards(...guards: GuardType[]): this;
30
39
  useGlobalInterceptors(...interceptors: InterceptorType[]): this;
@@ -63,6 +63,18 @@ export class VelaApplication {
63
63
  getHonoApp() {
64
64
  return this.getApp();
65
65
  }
66
+ /**
67
+ * Every explicit controller route as the framework registered it (fully
68
+ * composed paths — the `vela route list` seam). Requires built routes;
69
+ * contributed routes (`@velajs/crud`) mount directly on Hono and are
70
+ * observable via `getHonoApp().routes` instead.
71
+ */ describeRoutes() {
72
+ this.getApp(); // same routes-not-built guard as every HTTP accessor
73
+ return this.routeManager.getRouteDescriptions();
74
+ }
75
+ /** The global route prefix in effect ('' when none). */ getGlobalPrefix() {
76
+ return this.routeManager.getGlobalPrefix();
77
+ }
66
78
  // Pipeline components — applied at request time, no rebuild needed
67
79
  useGlobalPipes(...pipes) {
68
80
  this.routeManager.useGlobalPipes(...pipes);
@@ -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
+ };