@fulmenhq/tsfulmen 0.2.2 → 0.2.4

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.
@@ -1,3 +1,33 @@
1
+ /**
2
+ * Environment variable alias utilities.
3
+ *
4
+ * Workhorse templates sometimes support both canonical nested env vars and
5
+ * convenience aliases (e.g. TUVAN_SERVER_PORT vs TUVAN_PORT).
6
+ *
7
+ * This helper standardizes alias resolution and conflict reporting.
8
+ */
9
+ interface EnvAliasConflict {
10
+ canonicalKey: string;
11
+ aliasKey: string;
12
+ canonicalValue: string;
13
+ aliasValue: string;
14
+ }
15
+ interface ResolveEnvAliasesResult {
16
+ env: Record<string, string | undefined>;
17
+ applied: Array<{
18
+ aliasKey: string;
19
+ canonicalKey: string;
20
+ }>;
21
+ conflicts: EnvAliasConflict[];
22
+ }
23
+ /**
24
+ * Resolve env var aliases into canonical keys.
25
+ *
26
+ * - If canonical key is unset and alias is set, the alias value is copied to canonical.
27
+ * - If both are set and differ, a conflict is recorded (canonical is left unchanged).
28
+ */
29
+ declare function resolveEnvAliases(env: Record<string, string | undefined>, aliasToCanonical: Record<string, string>): ResolveEnvAliasesResult;
30
+
1
31
  /**
2
32
  * Config Path API errors - implements Fulmen Config Path Standard error handling
3
33
  */
@@ -119,6 +149,13 @@ interface LoadConfigOptions {
119
149
  * Defaults to identity.app.config_name or identity.app
120
150
  */
121
151
  userConfigName?: string;
152
+ /**
153
+ * Include env var consumption metadata for diagnostics.
154
+ *
155
+ * When enabled, loadConfig() records which environment variable keys were
156
+ * consumed for prefix-based overrides.
157
+ */
158
+ includeEnvVarReport?: boolean;
122
159
  }
123
160
  /**
124
161
  * Metadata about the loaded configuration
@@ -136,6 +173,14 @@ interface ConfigMetadata {
136
173
  * Environment variable prefix used for overrides
137
174
  */
138
175
  envPrefix: string;
176
+ /**
177
+ * Environment variable keys consumed (only when includeEnvVarReport is enabled)
178
+ */
179
+ envVarsConsumed?: string[];
180
+ /**
181
+ * Count of environment variable keys consumed (only when includeEnvVarReport is enabled)
182
+ */
183
+ envVarsConsumedCount?: number;
139
184
  /**
140
185
  * List of active configuration layers ("defaults", "user", "env")
141
186
  */
@@ -232,4 +277,4 @@ declare function resolveConfigPath(filename: string, searchPaths: string[], opti
232
277
  */
233
278
  declare const VERSION = "0.1.0";
234
279
 
235
- export { type AppIdentifier, type ConfigMetadata, ConfigPathError, type ConfigPathOptions, ConfigValidationError, type LoadConfigOptions, type LoadedConfig, type PlatformDirs, VERSION, type XDGBaseDirs, ensureDirExists, getAppCacheDir, getAppConfigDir, getAppDataDir, getConfigSearchPaths, getFulmenCacheDir, getFulmenConfigDir, getFulmenDataDir, getXDGBaseDirs, loadConfig, resolveConfigPath };
280
+ export { type AppIdentifier, type ConfigMetadata, ConfigPathError, type ConfigPathOptions, ConfigValidationError, type EnvAliasConflict, type LoadConfigOptions, type LoadedConfig, type PlatformDirs, type ResolveEnvAliasesResult, VERSION, type XDGBaseDirs, ensureDirExists, getAppCacheDir, getAppConfigDir, getAppDataDir, getConfigSearchPaths, getFulmenCacheDir, getFulmenConfigDir, getFulmenDataDir, getXDGBaseDirs, loadConfig, resolveConfigPath, resolveEnvAliases };
@@ -1,3 +1,4 @@
1
+ import addFormats from 'ajv-formats';
1
2
  import { spawn } from 'child_process';
2
3
  import { readFile, access, mkdir, writeFile } from 'fs/promises';
3
4
  import { parse, stringify } from 'yaml';
@@ -8,7 +9,6 @@ import Ajv from 'ajv';
8
9
  import Ajv2019 from 'ajv/dist/2019';
9
10
  import Ajv2020 from 'ajv/dist/2020';
10
11
  import AjvDraft04 from 'ajv-draft-04';
11
- import addFormats from 'ajv-formats';
12
12
  import { Readable } from 'stream';
13
13
  import picomatch from 'picomatch';
14
14
  import { suggest as suggest$1, substringSimilarity, score as score$1, normalize as normalize$1, jaro_winkler, damerau_levenshtein, osa_distance, levenshtein } from '@3leaps/string-metrics-wasm';
@@ -25,6 +25,27 @@ var __export = (target, all) => {
25
25
  for (var name in all)
26
26
  __defProp(target, name, { get: all[name], enumerable: true });
27
27
  };
28
+ function applyFulmenAjvFormats(ajv, options = {}) {
29
+ const mode = options.mode ?? "fast";
30
+ const formats = options.formats ?? DEFAULT_FORMATS;
31
+ addFormats(ajv, { mode, formats });
32
+ return ajv;
33
+ }
34
+ var DEFAULT_FORMATS;
35
+ var init_ajv_formats = __esm({
36
+ "src/schema/ajv-formats.ts"() {
37
+ DEFAULT_FORMATS = [
38
+ "date-time",
39
+ "email",
40
+ "hostname",
41
+ "ipv4",
42
+ "ipv6",
43
+ "uri",
44
+ "uri-reference",
45
+ "uuid"
46
+ ];
47
+ }
48
+ });
28
49
 
29
50
  // src/schema/errors.ts
30
51
  var errors_exports = {};
@@ -1580,10 +1601,7 @@ function createAjv(dialect) {
1580
1601
  // Enable async schema loading for YAML references
1581
1602
  loadSchema: loadReferencedSchema
1582
1603
  });
1583
- addFormats(ajv, {
1584
- mode: "fast",
1585
- formats: ["date-time", "email", "hostname", "ipv4", "ipv6", "uri", "uri-reference"]
1586
- });
1604
+ applyFulmenAjvFormats(ajv);
1587
1605
  return ajv;
1588
1606
  }
1589
1607
  async function getAjv(dialect) {
@@ -1829,6 +1847,7 @@ var ajvInstances, metaschemaReady, schemaCache;
1829
1847
  var init_validator = __esm({
1830
1848
  "src/schema/validator.ts"() {
1831
1849
  init_telemetry();
1850
+ init_ajv_formats();
1832
1851
  init_errors();
1833
1852
  init_registry();
1834
1853
  init_utils();
@@ -3788,6 +3807,224 @@ var init_capabilities2 = __esm({
3788
3807
  }
3789
3808
  });
3790
3809
 
3810
+ // src/foundry/signals/config-reload-endpoint.ts
3811
+ function createConfigReloadEndpoint(options) {
3812
+ const { loader, validator, onReload: onReload2, auth, rateLimit, logger, telemetry } = options;
3813
+ return async (payload, req) => {
3814
+ const correlationId = payload.correlation_id ?? generateCorrelationId();
3815
+ const authResult = await auth(req);
3816
+ if (!authResult.authenticated) {
3817
+ if (logger) {
3818
+ logger.warn("Config reload endpoint: authentication failed", {
3819
+ correlation_id: correlationId,
3820
+ reason: authResult.reason
3821
+ });
3822
+ }
3823
+ if (telemetry) {
3824
+ telemetry.emit("fulmen.config.http_endpoint.auth_failed", {
3825
+ correlation_id: correlationId
3826
+ });
3827
+ }
3828
+ return {
3829
+ status: "error",
3830
+ error: "authentication_failed",
3831
+ message: authResult.reason || "Authentication required",
3832
+ statusCode: 401
3833
+ };
3834
+ }
3835
+ const identity = authResult.identity || "unknown";
3836
+ if (rateLimit) {
3837
+ const rateLimitResult = await rateLimit(identity);
3838
+ if (!rateLimitResult.allowed) {
3839
+ if (logger) {
3840
+ logger.warn("Config reload endpoint: rate limit exceeded", {
3841
+ correlation_id: correlationId,
3842
+ identity
3843
+ });
3844
+ }
3845
+ if (telemetry) {
3846
+ telemetry.emit("fulmen.config.http_endpoint.rate_limited", {
3847
+ correlation_id: correlationId
3848
+ });
3849
+ }
3850
+ return {
3851
+ status: "error",
3852
+ error: "rate_limit_exceeded",
3853
+ message: "Rate limit exceeded. Please try again later.",
3854
+ statusCode: 429
3855
+ };
3856
+ }
3857
+ }
3858
+ if (telemetry) {
3859
+ telemetry.emit("fulmen.config.http_endpoint.reload_requested", {
3860
+ correlation_id: correlationId
3861
+ });
3862
+ }
3863
+ try {
3864
+ const config = await loader();
3865
+ if (validator) {
3866
+ const validation = await validator(config);
3867
+ if (!validation.valid) {
3868
+ if (logger) {
3869
+ logger.warn("Config reload endpoint: validation failed", {
3870
+ correlation_id: correlationId,
3871
+ error_count: validation.errors?.length ?? 0
3872
+ });
3873
+ }
3874
+ if (telemetry) {
3875
+ telemetry.emit("fulmen.config.http_endpoint.reload_rejected", {
3876
+ correlation_id: correlationId,
3877
+ reason: "validation_failed"
3878
+ });
3879
+ }
3880
+ return {
3881
+ status: "error",
3882
+ error: "validation_failed",
3883
+ message: "Configuration validation failed",
3884
+ validation_errors: validation.errors,
3885
+ statusCode: 422
3886
+ };
3887
+ }
3888
+ }
3889
+ if (onReload2) {
3890
+ await onReload2(config);
3891
+ }
3892
+ if (telemetry) {
3893
+ telemetry.emit("fulmen.config.http_endpoint.reload_accepted", {
3894
+ correlation_id: correlationId
3895
+ });
3896
+ }
3897
+ if (logger) {
3898
+ logger.info("Config reload endpoint: reload accepted", {
3899
+ correlation_id: correlationId,
3900
+ reason: payload.reason
3901
+ });
3902
+ }
3903
+ return {
3904
+ status: "reloaded",
3905
+ correlation_id: correlationId,
3906
+ message: "Configuration reloaded",
3907
+ statusCode: 200
3908
+ };
3909
+ } catch (error) {
3910
+ if (logger) {
3911
+ logger.warn("Config reload endpoint: reload failed", {
3912
+ correlation_id: correlationId,
3913
+ error: error instanceof Error ? error.message : String(error)
3914
+ });
3915
+ }
3916
+ if (telemetry) {
3917
+ telemetry.emit("fulmen.config.http_endpoint.reload_error", {
3918
+ correlation_id: correlationId,
3919
+ error_type: error instanceof Error ? error.constructor.name : "unknown"
3920
+ });
3921
+ }
3922
+ return {
3923
+ status: "error",
3924
+ error: "reload_failed",
3925
+ message: error instanceof Error ? error.message : String(error),
3926
+ statusCode: 500
3927
+ };
3928
+ }
3929
+ };
3930
+ }
3931
+ function generateCorrelationId() {
3932
+ return `cfg-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
3933
+ }
3934
+ var init_config_reload_endpoint = __esm({
3935
+ "src/foundry/signals/config-reload-endpoint.ts"() {
3936
+ }
3937
+ });
3938
+
3939
+ // src/appidentity/runtime.ts
3940
+ function detectRuntime() {
3941
+ const versions = process.versions;
3942
+ if (typeof versions.bun === "string" && versions.bun.length > 0) {
3943
+ return { name: "bun", version: versions.bun };
3944
+ }
3945
+ if (typeof versions.node === "string" && versions.node.length > 0) {
3946
+ return { name: "node", version: versions.node };
3947
+ }
3948
+ return { name: "unknown" };
3949
+ }
3950
+ function buildRuntimeInfo(options = {}) {
3951
+ const runtime = detectRuntime();
3952
+ const serviceName = options.serviceName ?? options.identity?.app.binary_name ?? "unknown-service";
3953
+ const vendor = options.vendor ?? options.identity?.app.vendor;
3954
+ return {
3955
+ service: {
3956
+ name: serviceName,
3957
+ vendor,
3958
+ version: options.version
3959
+ },
3960
+ runtime,
3961
+ platform: {
3962
+ os: process.platform,
3963
+ arch: process.arch
3964
+ }
3965
+ };
3966
+ }
3967
+ var init_runtime = __esm({
3968
+ "src/appidentity/runtime.ts"() {
3969
+ }
3970
+ });
3971
+
3972
+ // src/foundry/signals/control-discovery-endpoint.ts
3973
+ function createControlDiscoveryEndpoint(options) {
3974
+ const { identity, version, endpoints, auth, authSummary, logger, telemetry } = options;
3975
+ return async (req) => {
3976
+ if (auth) {
3977
+ const authResult = await auth(req);
3978
+ if (!authResult.authenticated) {
3979
+ if (logger) {
3980
+ logger.warn("Control discovery endpoint: authentication failed", {
3981
+ reason: authResult.reason
3982
+ });
3983
+ }
3984
+ if (telemetry) {
3985
+ telemetry.emit("fulmen.control.discovery.auth_failed", {
3986
+ service: identity.app.binary_name
3987
+ });
3988
+ }
3989
+ return {
3990
+ status: "error",
3991
+ error: "authentication_failed",
3992
+ message: authResult.reason || "Authentication required",
3993
+ statusCode: 401
3994
+ };
3995
+ }
3996
+ }
3997
+ if (telemetry) {
3998
+ telemetry.emit("fulmen.control.discovery.served", {
3999
+ service: identity.app.binary_name
4000
+ });
4001
+ }
4002
+ const runtime = buildRuntimeInfo({ identity, version });
4003
+ return {
4004
+ status: "ok",
4005
+ service: {
4006
+ name: identity.app.binary_name,
4007
+ vendor: identity.app.vendor,
4008
+ version
4009
+ },
4010
+ runtime: {
4011
+ name: runtime.runtime.name,
4012
+ version: runtime.runtime.version,
4013
+ platform: runtime.platform.os,
4014
+ arch: runtime.platform.arch
4015
+ },
4016
+ auth_summary: authSummary,
4017
+ endpoints,
4018
+ statusCode: 200
4019
+ };
4020
+ };
4021
+ }
4022
+ var init_control_discovery_endpoint = __esm({
4023
+ "src/foundry/signals/control-discovery-endpoint.ts"() {
4024
+ init_runtime();
4025
+ }
4026
+ });
4027
+
3791
4028
  // src/foundry/signals/convenience.ts
3792
4029
  async function onShutdown(manager, handler, options = {}) {
3793
4030
  await manager.register("SIGTERM", handler, options);
@@ -4047,7 +4284,7 @@ var init_guards = __esm({
4047
4284
  function createSignalEndpoint(options) {
4048
4285
  const { manager, auth, rateLimit, logger, telemetry, allowedSignals } = options;
4049
4286
  return async (payload, req) => {
4050
- const correlationId = payload.correlation_id ?? generateCorrelationId();
4287
+ const correlationId = payload.correlation_id ?? generateCorrelationId2();
4051
4288
  const authResult = await auth(req);
4052
4289
  if (!authResult.authenticated) {
4053
4290
  if (logger) {
@@ -4170,7 +4407,7 @@ function normalizeSignalName(signal) {
4170
4407
  }
4171
4408
  return `SIG${upper}`;
4172
4409
  }
4173
- function generateCorrelationId() {
4410
+ function generateCorrelationId2() {
4174
4411
  return `sig-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
4175
4412
  }
4176
4413
  function createBearerTokenAuth(expectedToken) {
@@ -4637,6 +4874,8 @@ var init_signals = __esm({
4637
4874
  "src/foundry/signals/index.ts"() {
4638
4875
  init_capabilities2();
4639
4876
  init_catalog();
4877
+ init_config_reload_endpoint();
4878
+ init_control_discovery_endpoint();
4640
4879
  init_convenience();
4641
4880
  init_double_tap();
4642
4881
  init_guards();
@@ -4782,7 +5021,9 @@ __export(foundry_exports, {
4782
5021
  clearMimeTypeCache: () => clearMimeTypeCache,
4783
5022
  clearPatternCache: () => clearPatternCache,
4784
5023
  createBearerTokenAuth: () => createBearerTokenAuth,
5024
+ createConfigReloadEndpoint: () => createConfigReloadEndpoint,
4785
5025
  createConfigReloadHandler: () => createConfigReloadHandler,
5026
+ createControlDiscoveryEndpoint: () => createControlDiscoveryEndpoint,
4786
5027
  createDoubleTapTracker: () => createDoubleTapTracker,
4787
5028
  createSignalEndpoint: () => createSignalEndpoint,
4788
5029
  createSignalManager: () => createSignalManager,
@@ -5838,6 +6079,7 @@ var init_cli = __esm({
5838
6079
  // src/schema/index.ts
5839
6080
  var init_schema = __esm({
5840
6081
  "src/schema/index.ts"() {
6082
+ init_ajv_formats();
5841
6083
  init_cli();
5842
6084
  init_errors();
5843
6085
  init_export();
@@ -5849,6 +6091,32 @@ var init_schema = __esm({
5849
6091
  }
5850
6092
  });
5851
6093
 
6094
+ // src/config/env-alias.ts
6095
+ function resolveEnvAliases(env, aliasToCanonical) {
6096
+ const out = { ...env };
6097
+ const applied = [];
6098
+ const conflicts = [];
6099
+ for (const [aliasKey, canonicalKey] of Object.entries(aliasToCanonical)) {
6100
+ const aliasValue = env[aliasKey];
6101
+ if (aliasValue === void 0 || aliasValue === "") continue;
6102
+ const canonicalValue = env[canonicalKey];
6103
+ if (canonicalValue === void 0 || canonicalValue === "") {
6104
+ out[canonicalKey] = aliasValue;
6105
+ applied.push({ aliasKey, canonicalKey });
6106
+ continue;
6107
+ }
6108
+ if (canonicalValue !== aliasValue) {
6109
+ conflicts.push({
6110
+ canonicalKey,
6111
+ aliasKey,
6112
+ canonicalValue,
6113
+ aliasValue
6114
+ });
6115
+ }
6116
+ }
6117
+ return { env: out, applied, conflicts };
6118
+ }
6119
+
5852
6120
  // src/config/errors.ts
5853
6121
  var ConfigPathError = class _ConfigPathError extends Error {
5854
6122
  constructor(message, cause) {
@@ -6200,6 +6468,33 @@ function parseEnvVars(prefix) {
6200
6468
  }
6201
6469
  return config;
6202
6470
  }
6471
+ function parseEnvVarsWithReport(prefix) {
6472
+ const config = {};
6473
+ const consumedKeys = [];
6474
+ const prefixWithSeparator = `${prefix}_`;
6475
+ for (const [key, value] of Object.entries(process.env)) {
6476
+ if (!value) continue;
6477
+ if (key.startsWith(prefixWithSeparator)) {
6478
+ consumedKeys.push(key);
6479
+ const keyWithoutPrefix = key.slice(prefixWithSeparator.length);
6480
+ const parts = keyWithoutPrefix.split("_").filter((p) => p.length > 0);
6481
+ let current = config;
6482
+ for (let i = 0; i < parts.length; i++) {
6483
+ const part = parts[i].toLowerCase();
6484
+ if (i === parts.length - 1) {
6485
+ current[part] = parseEnvValue(value);
6486
+ } else {
6487
+ if (!current[part] || typeof current[part] !== "object") {
6488
+ current[part] = {};
6489
+ }
6490
+ current = current[part];
6491
+ }
6492
+ }
6493
+ }
6494
+ }
6495
+ consumedKeys.sort();
6496
+ return { config, consumedKeys };
6497
+ }
6203
6498
  async function parseConfigFile(path) {
6204
6499
  const content = await readFile(path, "utf-8");
6205
6500
  const ext = extname(path).toLowerCase();
@@ -6233,8 +6528,10 @@ async function loadConfig(options) {
6233
6528
  activeLayers.push("user");
6234
6529
  }
6235
6530
  const envPrefix = options.envPrefix || (identity.app ? identity.app.toUpperCase().replace(/-/g, "_") : "APP");
6236
- const envConfig = parseEnvVars(envPrefix);
6237
- if (Object.keys(envConfig).length > 0) {
6531
+ const includeEnvVarReport = options.includeEnvVarReport === true;
6532
+ const envVars = includeEnvVarReport ? parseEnvVarsWithReport(envPrefix) : null;
6533
+ const envConfig = includeEnvVarReport ? envVars?.config : parseEnvVars(envPrefix);
6534
+ if (envConfig && typeof envConfig === "object" && Object.keys(envConfig).length > 0) {
6238
6535
  mergedConfig = deepMerge(mergedConfig, envConfig);
6239
6536
  activeLayers.push("env");
6240
6537
  }
@@ -6263,6 +6560,8 @@ async function loadConfig(options) {
6263
6560
  defaultsPath,
6264
6561
  userConfigPath,
6265
6562
  envPrefix,
6563
+ envVarsConsumed: includeEnvVarReport ? envVars?.consumedKeys : void 0,
6564
+ envVarsConsumedCount: includeEnvVarReport ? envVars?.consumedKeys.length : void 0,
6266
6565
  activeLayers,
6267
6566
  schema: {
6268
6567
  path: options.schemaPath || null,
@@ -6275,6 +6574,6 @@ async function loadConfig(options) {
6275
6574
  // src/config/index.ts
6276
6575
  var VERSION2 = "0.1.0";
6277
6576
 
6278
- export { ConfigPathError, ConfigValidationError, VERSION2 as VERSION, ensureDirExists, getAppCacheDir, getAppConfigDir, getAppDataDir, getConfigSearchPaths, getFulmenCacheDir, getFulmenConfigDir, getFulmenDataDir, getXDGBaseDirs, loadConfig, resolveConfigPath };
6577
+ export { ConfigPathError, ConfigValidationError, VERSION2 as VERSION, ensureDirExists, getAppCacheDir, getAppConfigDir, getAppDataDir, getConfigSearchPaths, getFulmenCacheDir, getFulmenConfigDir, getFulmenDataDir, getXDGBaseDirs, loadConfig, resolveConfigPath, resolveEnvAliases };
6279
6578
  //# sourceMappingURL=index.js.map
6280
6579
  //# sourceMappingURL=index.js.map