@conduitplatform/module-tools 0.17.0-alpha.6 → 0.17.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.mdx CHANGED
@@ -6,6 +6,19 @@ This library provides a framework to build micro-services in NodeJS that can be
6
6
 
7
7
  - run npm build
8
8
 
9
+ ## Logging
10
+
11
+ Modules log to stdout. When `LOKI_URL` is set, the same logs are also shipped to Loki. File logging is on unless `LOG_TO_FILE=false`.
12
+
13
+ | Variable | Default | Description |
14
+ | -------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
15
+ | `LOG_TO_FILE` | unset (on) | Set to `false` to skip file logs. Any other value, or leaving it unset, writes them. |
16
+ | `LOG_FILE_DIR` | `{tmpdir}/conduit-logs` | Directory for `combined-YYYY-MM-DD.log` and `errors-YYYY-MM-DD.log`. It is created when missing. |
17
+ | `LOG_RETENTION_DAYS` | `3` | Days of log files to keep. Older files are deleted. Non-positive or non-integer values use the default. |
18
+ | `LOG_FILE_MAX_SIZE` | `20m` | Max size of each file before a same-day size roll. Accepts `k`, `m`, or `g` suffixes, or a positive byte count. Invalid values use the default. |
19
+
20
+ Files roll once per day and are not gzipped. Metadata in file lines is single-line JSON.
21
+
9
22
  ## Features ✔️
10
23
 
11
24
  - Service registration & discovery
package/dist/index.cjs CHANGED
@@ -48,6 +48,8 @@ __export(index_exports, {
48
48
  RouteBuilder: () => RouteBuilder,
49
49
  RoutingManager: () => RoutingManager,
50
50
  addServiceToServer: () => addServiceToServer,
51
+ containsRedactedMarker: () => containsRedactedMarker,
52
+ convictConfigParser: () => convictConfigParser,
51
53
  createParsedRouterRequest: () => createParsedRouterRequest,
52
54
  createServer: () => createServer,
53
55
  exportModel: () => exportModel,
@@ -57,7 +59,12 @@ __export(index_exports, {
57
59
  linearBackoffTimeoutAsync: () => linearBackoffTimeoutAsync,
58
60
  merge: () => merge,
59
61
  readConduitPeersManifest: () => readConduitPeersManifest,
62
+ reconcileStoredModuleConfig: () => reconcileStoredModuleConfig,
63
+ redactSensitiveConfig: () => redactSensitiveConfig,
64
+ restoreRedactedSecrets: () => restoreRedactedSecrets,
60
65
  sanitizeDocumentsForExport: () => sanitizeDocumentsForExport,
66
+ stableConfigJson: () => stableConfigJson,
67
+ storedConfigsEquivalent: () => storedConfigsEquivalent,
61
68
  stripInternalFields: () => stripInternalFields,
62
69
  wrapFunctionsAsync: () => wrapFunctionsAsync,
63
70
  wrapGrpcFunctions: () => wrapGrpcFunctions,
@@ -284,11 +291,19 @@ function wrapGrpcFunctions(functions, postponeRestart, noAuth = false) {
284
291
  try {
285
292
  invoked = functions[name](call, callback);
286
293
  } catch (error) {
287
- return throwError(callback, error.message);
294
+ return throwError(
295
+ callback,
296
+ error.message,
297
+ error instanceof import_grpc_sdk3.GrpcError ? error.code : import_grpc_js2.status.INTERNAL
298
+ );
288
299
  }
289
300
  if (typeof invoked?.then === "function") {
290
301
  invoked.then().catch((error) => {
291
- return throwError(callback, error.message);
302
+ return throwError(
303
+ callback,
304
+ error.message,
305
+ error instanceof import_grpc_sdk3.GrpcError ? error.code : import_grpc_js2.status.INTERNAL
306
+ );
292
307
  });
293
308
  }
294
309
  };
@@ -1268,9 +1283,84 @@ async function setupLoki(module2, instance) {
1268
1283
  }
1269
1284
  }
1270
1285
 
1271
- // src/utilities/initializeSdk.ts
1272
- var import_winston2 = __toESM(require("winston"), 1);
1286
+ // src/logging/fileTransports.ts
1287
+ var import_fs = __toESM(require("fs"), 1);
1288
+ var import_os = __toESM(require("os"), 1);
1273
1289
  var import_path2 = __toESM(require("path"), 1);
1290
+ var import_winston2 = __toESM(require("winston"), 1);
1291
+ var import_winston_daily_rotate_file = __toESM(require("winston-daily-rotate-file"), 1);
1292
+ var DEFAULT_RETENTION_DAYS = 3;
1293
+ var DEFAULT_MAX_SIZE = "20m";
1294
+ var SIZE_STRING = /^(?:0\.)?\d+[kmg]$/i;
1295
+ function positiveIntFromEnv(raw, fallback) {
1296
+ if (raw === void 0) return fallback;
1297
+ const trimmed = raw.trim();
1298
+ if (!/^[0-9]+$/.test(trimmed)) return fallback;
1299
+ const value = Number(trimmed);
1300
+ if (!Number.isSafeInteger(value) || value <= 0) return fallback;
1301
+ return value;
1302
+ }
1303
+ function logFileMaxSize() {
1304
+ const raw = process.env.LOG_FILE_MAX_SIZE?.trim();
1305
+ if (!raw) return DEFAULT_MAX_SIZE;
1306
+ if (SIZE_STRING.test(raw)) return raw.toLowerCase();
1307
+ if (/^[0-9]+$/.test(raw)) {
1308
+ const bytes = Number(raw);
1309
+ if (Number.isSafeInteger(bytes) && bytes > 0) return bytes;
1310
+ }
1311
+ return DEFAULT_MAX_SIZE;
1312
+ }
1313
+ function formatMeta(meta) {
1314
+ if (Array.isArray(meta)) {
1315
+ if (meta.length === 0) return "";
1316
+ return meta.map((item) => JSON.stringify(item)).join(" ");
1317
+ }
1318
+ if (meta !== null && typeof meta === "object") {
1319
+ if (Object.keys(meta).length === 0) return "";
1320
+ return JSON.stringify(meta);
1321
+ }
1322
+ return "";
1323
+ }
1324
+ function createFileFormat() {
1325
+ return import_winston2.default.format.combine(
1326
+ import_winston2.default.format.errors({ stack: true }),
1327
+ import_winston2.default.format.timestamp({
1328
+ format: "YYYY-MM-DD HH:mm:ss"
1329
+ }),
1330
+ import_winston2.default.format.printf((info) => {
1331
+ const metaText = formatMeta(info.meta);
1332
+ const line = `[${info.timestamp}] [${info.level.toUpperCase()}]: ${info.message}`;
1333
+ if (info.stack) {
1334
+ return `${line} ${info.stack}`;
1335
+ }
1336
+ return metaText ? `${line} ${metaText}` : line;
1337
+ })
1338
+ );
1339
+ }
1340
+ function rotateTransport(logDir, name, level) {
1341
+ return new import_winston_daily_rotate_file.default({
1342
+ dirname: logDir,
1343
+ filename: `${name}-%DATE%.log`,
1344
+ datePattern: "YYYY-MM-DD",
1345
+ level,
1346
+ maxFiles: `${positiveIntFromEnv(process.env.LOG_RETENTION_DAYS, DEFAULT_RETENTION_DAYS)}d`,
1347
+ maxSize: logFileMaxSize(),
1348
+ zippedArchive: false,
1349
+ auditFile: import_path2.default.join(logDir, `.${name}-audit.json`),
1350
+ format: createFileFormat()
1351
+ });
1352
+ }
1353
+ function createFileLogTransports() {
1354
+ if (process.env.LOG_TO_FILE?.trim() === "false") {
1355
+ return [];
1356
+ }
1357
+ const logDir = process.env.LOG_FILE_DIR?.trim() || import_path2.default.join(import_os.default.tmpdir(), "conduit-logs");
1358
+ import_fs.default.mkdirSync(logDir, { recursive: true });
1359
+ return [
1360
+ rotateTransport(logDir, "combined", "info"),
1361
+ rotateTransport(logDir, "errors", "error")
1362
+ ];
1363
+ }
1274
1364
 
1275
1365
  // src/metrics/index.ts
1276
1366
  var import_prom_client = require("prom-client");
@@ -1659,17 +1749,9 @@ function clientMiddleware() {
1659
1749
  var initializeSdk = (coreUrl, moduleName, watchModules = true, serviceHealthStatusGetter = () => import_grpc_sdk13.HealthCheckStatus.SERVING) => {
1660
1750
  let logger;
1661
1751
  try {
1662
- logger = new ConduitLogger([
1663
- new import_winston2.default.transports.File({
1664
- filename: import_path2.default.join(__dirname, ".logs/combined.log"),
1665
- level: "info"
1666
- }),
1667
- new import_winston2.default.transports.File({
1668
- filename: import_path2.default.join(__dirname, ".logs/errors.log"),
1669
- level: "error"
1670
- })
1671
- ]);
1672
- } catch (e) {
1752
+ const fileTransports = createFileLogTransports();
1753
+ logger = fileTransports.length > 0 ? new ConduitLogger(fileTransports) : new ConduitLogger();
1754
+ } catch {
1673
1755
  logger = new ConduitLogger();
1674
1756
  }
1675
1757
  const grpcSdk = new import_grpc_sdk13.ConduitGrpcSdk(
@@ -1775,6 +1857,110 @@ var convictConfigParser = (config) => {
1775
1857
  return config;
1776
1858
  };
1777
1859
 
1860
+ // src/utilities/redactSensitiveConfig.ts
1861
+ var WELL_KNOWN_SECRET_KEYS = /^(apiKey|api_key|password|secret|privateKey|private_key)$/i;
1862
+ function isRecord(value) {
1863
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1864
+ }
1865
+ function isSchemaLeaf(value) {
1866
+ return isRecord(value) && ("default" in value || "format" in value || "type" in value) && !isRecord(value._cvtProperties);
1867
+ }
1868
+ function isSensitiveLeaf(value) {
1869
+ return isSchemaLeaf(value) && value.sensitive === true;
1870
+ }
1871
+ function unwrapSchema(schema) {
1872
+ if (isRecord(schema) && isRecord(schema._cvtProperties)) {
1873
+ return schema._cvtProperties;
1874
+ }
1875
+ return schema;
1876
+ }
1877
+ var REDACTED_MARKER = "[REDACTED]";
1878
+ function containsRedactedMarker(value) {
1879
+ if (value === REDACTED_MARKER) return true;
1880
+ if (Array.isArray(value)) {
1881
+ return value.some(containsRedactedMarker);
1882
+ }
1883
+ if (!isRecord(value)) return false;
1884
+ return Object.values(value).some(containsRedactedMarker);
1885
+ }
1886
+ function redactSensitiveConfig(config, schema) {
1887
+ if (!isRecord(config)) return config;
1888
+ const redacted = Array.isArray(config) ? [...config] : { ...config };
1889
+ const node = unwrapSchema(schema);
1890
+ for (const [key, value] of Object.entries(redacted)) {
1891
+ const childSchema = isRecord(node) ? node[key] : void 0;
1892
+ if (isSensitiveLeaf(childSchema) || WELL_KNOWN_SECRET_KEYS.test(key)) {
1893
+ if (typeof value === "string" && value.length > 0) {
1894
+ redacted[key] = REDACTED_MARKER;
1895
+ }
1896
+ continue;
1897
+ }
1898
+ if (isRecord(value) || Array.isArray(value)) {
1899
+ redacted[key] = redactSensitiveConfig(
1900
+ value,
1901
+ childSchema
1902
+ );
1903
+ }
1904
+ }
1905
+ return redacted;
1906
+ }
1907
+ function restoreRedactedSecrets(incoming, current, schema) {
1908
+ if (Array.isArray(incoming) && Array.isArray(current)) {
1909
+ return incoming.map(
1910
+ (item, index) => restoreRedactedSecrets(item, current[index], schema)
1911
+ );
1912
+ }
1913
+ if (!isRecord(incoming) || !isRecord(current)) return incoming;
1914
+ const restored = { ...incoming };
1915
+ const node = unwrapSchema(schema);
1916
+ for (const [key, value] of Object.entries(restored)) {
1917
+ const childSchema = isRecord(node) ? node[key] : void 0;
1918
+ const currentValue = current[key];
1919
+ if (isSensitiveLeaf(childSchema) || WELL_KNOWN_SECRET_KEYS.test(key)) {
1920
+ if (value === REDACTED_MARKER && typeof currentValue === "string" && currentValue.length > 0 && currentValue !== REDACTED_MARKER) {
1921
+ restored[key] = currentValue;
1922
+ }
1923
+ continue;
1924
+ }
1925
+ if ((isRecord(value) || Array.isArray(value)) && (isRecord(currentValue) || Array.isArray(currentValue))) {
1926
+ restored[key] = restoreRedactedSecrets(value, currentValue, childSchema);
1927
+ }
1928
+ }
1929
+ return restored;
1930
+ }
1931
+
1932
+ // src/utilities/reconcileModuleConfig.ts
1933
+ function isRecord2(value) {
1934
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1935
+ }
1936
+ function sortJson(value) {
1937
+ if (Array.isArray(value)) {
1938
+ return value.map(sortJson);
1939
+ }
1940
+ if (!isRecord2(value)) {
1941
+ return value;
1942
+ }
1943
+ return Object.fromEntries(
1944
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, nested]) => [key, sortJson(nested)])
1945
+ );
1946
+ }
1947
+ function stableConfigJson(value) {
1948
+ return JSON.stringify(sortJson(value));
1949
+ }
1950
+ function storedConfigsEquivalent(left, right) {
1951
+ return stableConfigJson(left) === stableConfigJson(right);
1952
+ }
1953
+ async function reconcileStoredModuleConfig(args) {
1954
+ if (storedConfigsEquivalent(args.stored, args.migrated)) {
1955
+ return { config: args.migrated, persisted: false };
1956
+ }
1957
+ if (containsRedactedMarker(args.migrated)) {
1958
+ return { config: args.migrated, persisted: false };
1959
+ }
1960
+ const persisted = await args.configureOverride(args.migrated);
1961
+ return { config: persisted, persisted: true };
1962
+ }
1963
+
1778
1964
  // src/routing/RoutingController.ts
1779
1965
  var import_grpc_js5 = require("@grpc/grpc-js");
1780
1966
  var RoutingController = class {
@@ -1827,6 +2013,9 @@ var ManagedModule = class extends ConduitServiceModule {
1827
2013
  throw new Error("Failed to initialize grpcSdk");
1828
2014
  }
1829
2015
  }
2016
+ get configInitialized() {
2017
+ return this._configInitialized;
2018
+ }
1830
2019
  get name() {
1831
2020
  return this._moduleName;
1832
2021
  }
@@ -2018,9 +2207,10 @@ var ManagedModule = class extends ConduitServiceModule {
2018
2207
  });
2019
2208
  }
2020
2209
  let config = JSON.parse(call.request.newConfig);
2021
- config = merge(this.config.getProperties(), config);
2022
- config = await this.preConfig(config);
2023
2210
  const previousConfig = this.config.getProperties();
2211
+ config = merge(previousConfig, config);
2212
+ config = restoreRedactedSecrets(config, previousConfig, this.configSchema);
2213
+ config = await this.preConfig(config);
2024
2214
  try {
2025
2215
  this.config.load(config).validate({
2026
2216
  allowed: "warn"
@@ -2096,7 +2286,21 @@ var ManagedModule = class extends ConduitServiceModule {
2096
2286
  this.configOverride
2097
2287
  );
2098
2288
  ConfigController.getInstance();
2099
- if (config) ConfigController.getInstance().config = config;
2289
+ if (config) {
2290
+ const migrated = await this.preConfig(config);
2291
+ this.config.load(migrated).validate({
2292
+ allowed: "warn"
2293
+ });
2294
+ const persistable = this.config.getProperties();
2295
+ const reconciled = await reconcileStoredModuleConfig({
2296
+ stored: config,
2297
+ migrated: persistable,
2298
+ configureOverride: (next) => this.grpcSdk.config.configure(next, convictConfigParser(configSchema), true)
2299
+ });
2300
+ config = reconciled.config;
2301
+ this.config.load(config);
2302
+ ConfigController.getInstance().config = this.config.getProperties();
2303
+ }
2100
2304
  if (!config || config.active || !config.hasOwnProperty("active"))
2101
2305
  await this.onConfig();
2102
2306
  }
@@ -2141,6 +2345,8 @@ var ModuleError = class extends import_grpc_sdk15.GrpcError {
2141
2345
  RouteBuilder,
2142
2346
  RoutingManager,
2143
2347
  addServiceToServer,
2348
+ containsRedactedMarker,
2349
+ convictConfigParser,
2144
2350
  createParsedRouterRequest,
2145
2351
  createServer,
2146
2352
  exportModel,
@@ -2150,7 +2356,12 @@ var ModuleError = class extends import_grpc_sdk15.GrpcError {
2150
2356
  linearBackoffTimeoutAsync,
2151
2357
  merge,
2152
2358
  readConduitPeersManifest,
2359
+ reconcileStoredModuleConfig,
2360
+ redactSensitiveConfig,
2361
+ restoreRedactedSecrets,
2153
2362
  sanitizeDocumentsForExport,
2363
+ stableConfigJson,
2364
+ storedConfigsEquivalent,
2154
2365
  stripInternalFields,
2155
2366
  wrapFunctionsAsync,
2156
2367
  wrapGrpcFunctions,