@karmaniverous/jeeves-meta 0.18.0-2 → 0.18.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { sleepAsync, fetchWithTimeout, createConfigQueryHandler, atomicWrite, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createServiceCli } from '@karmaniverous/jeeves';
2
+ import { sleepAsync, fetchWithTimeout, createConfigQueryHandler, createStatusHandler, getBindAddress, fetchJson, postJson, getServiceUrl, jeevesComponentDescriptorSchema, createConfigApplyHandler, createServiceCli } from '@karmaniverous/jeeves';
3
3
  import { normalizePath, metaConfigSchema, getEndpoint, META_COMPONENT } from '@karmaniverous/jeeves-meta-core';
4
4
  import fs, { unlinkSync, writeFileSync, existsSync, readFileSync, statSync, mkdirSync, readdirSync, watchFile } from 'node:fs';
5
5
  import path, { join, dirname, relative, posix, sep, resolve } from 'node:path';
@@ -4018,24 +4018,53 @@ function registerConfigRoute(app, deps) {
4018
4018
  }
4019
4019
 
4020
4020
  /**
4021
- * POST /config/apply — apply a config patch using the runtime config path.
4021
+ * POST /config/apply — apply a config patch to the runtime config file.
4022
4022
  *
4023
- * The core SDK's `createConfigApplyHandler` derives the config path from
4024
- * `getComponentConfigDir()` which uses the npm global config root. This
4025
- * local implementation uses the actual runtime config path instead, so
4026
- * temp files are written alongside the active config file.
4023
+ * Delegates the read/merge/validate/write cycle to the core SDK's
4024
+ * `createConfigApplyHandler` (bound to the runtime config path), which
4025
+ * merges into the raw file, validates without writing the schema's parsed
4026
+ * output, and keeps the file mode (0600 for a new file). This route adds
4027
+ * request validation, `restartRequired` reporting, and keeps the resolved
4028
+ * config (which may hold secrets) out of the response.
4027
4029
  *
4028
4030
  * @module routes/configApply
4029
4031
  */
4030
- /** Register the POST /config/apply route. */
4031
- function registerConfigApplyRoute(app, configPath) {
4032
+ /**
4033
+ * Snapshot the effective (schema-resolved) config currently on disk.
4034
+ *
4035
+ * @param configPath - Runtime config file path.
4036
+ * @returns The resolved config, the raw object when it does not validate,
4037
+ * or `{}` when the file is missing or unreadable.
4038
+ */
4039
+ function readEffectiveConfig(configPath) {
4040
+ let raw;
4041
+ try {
4042
+ raw = JSON.parse(readFileSync(configPath, 'utf8'));
4043
+ }
4044
+ catch {
4045
+ return {};
4046
+ }
4047
+ const parsed = serviceConfigSchema.safeParse(raw);
4048
+ if (parsed.success)
4049
+ return parsed.data;
4050
+ return typeof raw === 'object' && raw !== null && !Array.isArray(raw)
4051
+ ? raw
4052
+ : {};
4053
+ }
4054
+ /**
4055
+ * Register the POST /config/apply route.
4056
+ *
4057
+ * @param app - Fastify instance.
4058
+ * @param applyConfig - Core config apply handler bound to the runtime path.
4059
+ * @param configPath - Runtime config file path (for restart detection).
4060
+ */
4061
+ function registerConfigApplyRoute(app, applyConfig, configPath) {
4032
4062
  app.post(getEndpoint('configApply').path, async (request, reply) => {
4033
- if (!configPath) {
4063
+ if (!applyConfig || !configPath) {
4034
4064
  return reply
4035
4065
  .status(500)
4036
4066
  .send({ error: 'No runtime config path available' });
4037
4067
  }
4038
- // Validate request body
4039
4068
  const body = request.body;
4040
4069
  if (!body || typeof body !== 'object' || Array.isArray(body)) {
4041
4070
  return reply
@@ -4056,64 +4085,20 @@ function registerConfigApplyRoute(app, configPath) {
4056
4085
  .status(400)
4057
4086
  .send({ error: '`replace` must be a boolean if provided' });
4058
4087
  }
4059
- // Read existing config from the runtime config path
4060
- let existing = {};
4061
- try {
4062
- existing = JSON.parse(readFileSync(configPath, 'utf8'));
4063
- }
4064
- catch (err) {
4065
- if (err instanceof SyntaxError ||
4066
- (err instanceof Error && err.message.includes('JSON'))) {
4067
- return reply.status(400).send({
4068
- error: `Existing config file contains invalid JSON: ${err.message}`,
4069
- });
4070
- }
4071
- // File missing — start from empty
4072
- }
4073
- // Merge or replace
4074
- const merged = replace
4075
- ? { ...patch }
4076
- : { ...existing, ...patch };
4077
- // Validate against schema
4078
- const parseResult = serviceConfigSchema.safeParse(merged);
4079
- if (!parseResult.success) {
4080
- return reply.status(400).send({
4081
- error: 'Config validation failed',
4082
- issues: parseResult.error.issues,
4083
- });
4084
- }
4085
- const validatedConfig = parseResult.data;
4086
- // Write atomically — temp file lands next to the runtime config file
4087
- try {
4088
- const json = JSON.stringify(validatedConfig, null, 2) + '\n';
4089
- atomicWrite(configPath, json);
4090
- }
4091
- catch (err) {
4092
- const message = err instanceof Error ? err.message : String(err);
4093
- return reply
4094
- .status(500)
4095
- .send({ error: `Failed to write config: ${message}` });
4096
- }
4097
- // Compute whether any restart-required fields actually changed
4098
- // (compares validated output against previous config, catching both
4099
- // explicit patch keys and implicit changes via replace + schema defaults)
4100
- const validatedRecord = validatedConfig;
4101
- const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(existing[field]) !==
4102
- JSON.stringify(validatedRecord[field]));
4103
- // Apply hot-reload callback
4104
- try {
4105
- applyHotReloadedConfig(validatedConfig);
4106
- }
4107
- catch (err) {
4108
- const message = err instanceof Error ? err.message : String(err);
4109
- return reply.status(200).send({
4110
- applied: true,
4111
- warning: `Config written but callback failed: ${message}`,
4112
- restartRequired,
4113
- });
4088
+ const previous = readEffectiveConfig(configPath);
4089
+ const result = await applyConfig({
4090
+ patch: patch,
4091
+ replace,
4092
+ });
4093
+ if (result.status !== 200) {
4094
+ return reply.status(result.status).send(result.body);
4114
4095
  }
4096
+ // Never echo the resolved config: it may hold secrets (gatewayApiKey).
4097
+ const { config: next = {}, warning } = result.body;
4098
+ const restartRequired = RESTART_REQUIRED_FIELDS.some((field) => JSON.stringify(previous[field]) !== JSON.stringify(next[field]));
4115
4099
  return reply.status(200).send({
4116
4100
  applied: true,
4101
+ ...(warning ? { warning } : {}),
4117
4102
  restartRequired,
4118
4103
  });
4119
4104
  });
@@ -5070,7 +5055,7 @@ function registerRoutes(app, deps) {
5070
5055
  registerSeedRoute(app, deps);
5071
5056
  registerUnlockRoute(app, deps);
5072
5057
  registerConfigRoute(app, deps);
5073
- registerConfigApplyRoute(app, deps.configPath);
5058
+ registerConfigApplyRoute(app, deps.configApply, deps.configPath);
5074
5059
  registerQueueRoutes(app, deps);
5075
5060
  }
5076
5061
 
@@ -5254,8 +5239,10 @@ class HttpWatcherClient {
5254
5239
  *
5255
5240
  * @param config - Validated service configuration.
5256
5241
  * @param configPath - Optional path to config file for hot-reload.
5242
+ * @param configApply - Core config apply handler bound to `configPath`,
5243
+ * backing POST /config/apply.
5257
5244
  */
5258
- async function startService(config, configPath) {
5245
+ async function startService(config, configPath, configApply) {
5259
5246
  const logger = createLogger({
5260
5247
  level: config.logging.level,
5261
5248
  file: config.logging.file,
@@ -5292,6 +5279,7 @@ async function startService(config, configPath) {
5292
5279
  ready: false,
5293
5280
  executor,
5294
5281
  configPath,
5282
+ configApply,
5295
5283
  };
5296
5284
  registerConfigHotReloadRuntime({
5297
5285
  config,
@@ -5620,7 +5608,9 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
5620
5608
  },
5621
5609
  run: async (configPath) => {
5622
5610
  const config = loadServiceConfig(configPath);
5623
- await startService(config, configPath);
5611
+ // Route POST /config/apply through core's handler so the raw file is
5612
+ // merged (unknown keys kept, no defaults written, mode preserved).
5613
+ await startService(config, configPath, createConfigApplyHandler(metaDescriptor, configPath));
5624
5614
  },
5625
5615
  startCommand: (configPath) => [
5626
5616
  'node',
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "url": "https://github.com/karmaniverous/jeeves-meta/issues"
8
8
  },
9
9
  "dependencies": {
10
- "@karmaniverous/jeeves": "0.6.0-9",
10
+ "@karmaniverous/jeeves": "0.6.0",
11
11
  "@karmaniverous/jeeves-meta-core": "^0.2.2",
12
12
  "commander": "^15",
13
13
  "croner": "^10",
@@ -106,5 +106,5 @@
106
106
  "typecheck": "tsc"
107
107
  },
108
108
  "type": "module",
109
- "version": "0.18.0-2"
109
+ "version": "0.18.0"
110
110
  }