@hivemagnus/magnus-server 1.0.2 → 1.1.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,4 +1,5 @@
1
1
  import { recordMonitorPulse } from './monitor-pulse.js';
2
+ import { WEBHOOK_FORMATS, webhookFormat } from './webhook-types.js';
2
3
  import { redactUnknown } from '../utils/redact.js';
3
4
  import { createHmac, randomBytes, createCipheriv } from 'node:crypto';
4
5
  import { nanoid } from 'nanoid';
@@ -67,7 +68,7 @@ export async function reconcileWebhookDeliveries() {
67
68
  // replaced; failed jobs are finalized by the worker's failure bookkeeping.
68
69
  await addDurableJob(getWebhookQueue(), 'deliver', createWebhookJobPayload(delivery.endpointId), {
69
70
  jobId: delivery.id,
70
- attempts: delivery.endpoint.type === 'mattermost' ? 1 : Math.max(1, Math.min(11, delivery.endpoint.retries + 1)),
71
+ attempts: webhookFormat(delivery.endpoint.type) === 'mattermost' ? 1 : Math.max(1, Math.min(11, delivery.endpoint.retries + 1)),
71
72
  backoff: { type: 'exponential', delay: 1000 },
72
73
  removeOnComplete: { age: 86400, count: 10000 }, removeOnFail: { age: 604800, count: 10000 },
73
74
  }, ['completed']);
@@ -254,9 +255,9 @@ export function startWebhookWorker() {
254
255
  const extraHeaders = parseStoredHeaders(endpoint.id, endpoint.headers);
255
256
  const shouldEncrypt = extraHeaders['x-magnus-encrypt'] === 'true';
256
257
  delete extraHeaders['x-magnus-encrypt'];
257
- const { body: rawBody, contentType } = formatPayload(endpoint.type, delivery.event, delivery.payload);
258
+ const { body: rawBody, contentType } = formatPayload(webhookFormat(endpoint.type), delivery.event, delivery.payload);
258
259
  // Optionally encrypt the payload with AES-256-GCM
259
- const encrypted = shouldEncrypt && secret !== null && endpoint.type === 'generic';
260
+ const encrypted = shouldEncrypt && secret !== null && webhookFormat(endpoint.type) === 'generic';
260
261
  const body = encrypted ? encryptPayload(rawBody, secret) : rawBody;
261
262
  const headers = {
262
263
  ...extraHeaders,
@@ -269,7 +270,7 @@ export function startWebhookWorker() {
269
270
  if (secret) {
270
271
  headers['X-Magnus-Signature'] = `sha256=${signPayload(body, secret)}`;
271
272
  }
272
- if (endpoint.type === 'mattermost' && !(await claimMattermostAttempt(deliveryId)))
273
+ if (webhookFormat(endpoint.type) === 'mattermost' && !(await claimMattermostAttempt(deliveryId)))
273
274
  return;
274
275
  try {
275
276
  const res = await postWebhook(webhookUrl(endpoint), { headers, body, timeoutMs: endpoint.timeoutMs });
@@ -356,7 +357,8 @@ export async function getWebhookEndpoint(id) {
356
357
  export async function createWebhookEndpoint(data) {
357
358
  const db = getDb();
358
359
  const id = nanoid();
359
- if (!['generic', 'slack', 'discord', 'mattermost'].includes(data.type ?? 'generic'))
360
+ const type = webhookFormat(data.type ?? 'generic');
361
+ if (!WEBHOOK_FORMATS.includes(type))
360
362
  throw new Error('Unsupported webhook format');
361
363
  await validateWebhookTarget(data.url);
362
364
  const headers = normalizeHeaders(data.headers ?? {});
@@ -366,7 +368,7 @@ export async function createWebhookEndpoint(data) {
366
368
  id,
367
369
  name: data.name,
368
370
  url: encryptWebhookCredential(data.url, id, 'url'),
369
- type: data.type ?? 'generic',
371
+ type,
370
372
  secret: data.secret === undefined ? null : encryptWebhookCredential(data.secret, id, 'secret'),
371
373
  events: JSON.stringify(data.events ?? []),
372
374
  headers: encryptWebhookCredential(JSON.stringify(headers), id, 'headers'),
@@ -390,9 +392,10 @@ export async function updateWebhookEndpoint(id, data) {
390
392
  updateData['url'] = encryptWebhookCredential(data.url, id, 'url');
391
393
  }
392
394
  if (data.type !== undefined) {
393
- if (!['generic', 'slack', 'discord', 'mattermost'].includes(data.type))
395
+ const type = webhookFormat(data.type);
396
+ if (!WEBHOOK_FORMATS.includes(type))
394
397
  throw new Error('Unsupported webhook format');
395
- updateData['type'] = data.type;
398
+ updateData['type'] = type;
396
399
  }
397
400
  if (data.secret !== undefined) {
398
401
  updateData['secret'] = data.secret === null ? null : encryptWebhookCredential(data.secret, id, 'secret');
@@ -442,12 +445,12 @@ export async function testWebhookEndpoint(id) {
442
445
  message: 'This is a test webhook delivery from Magnus',
443
446
  timestamp: new Date().toISOString(),
444
447
  };
445
- const { body: rawBody, contentType } = formatPayload(endpoint.type, 'webhook.test', JSON.stringify(testPayload));
448
+ const { body: rawBody, contentType } = formatPayload(webhookFormat(endpoint.type), 'webhook.test', JSON.stringify(testPayload));
446
449
  const secret = encryptedSecret(endpoint);
447
450
  const extraHeaders = parseStoredHeaders(endpoint.id, endpoint.headers);
448
451
  const shouldEncrypt = extraHeaders['x-magnus-encrypt'] === 'true';
449
452
  delete extraHeaders['x-magnus-encrypt'];
450
- const encrypted = shouldEncrypt && secret !== null && endpoint.type === 'generic';
453
+ const encrypted = shouldEncrypt && secret !== null && webhookFormat(endpoint.type) === 'generic';
451
454
  const body = encrypted ? encryptPayload(rawBody, secret) : rawBody;
452
455
  const headers = {
453
456
  ...extraHeaders,
@@ -0,0 +1,29 @@
1
+ export const WEBHOOK_FORMATS = ['generic', 'slack', 'discord', 'mattermost'];
2
+ /**
3
+ * Legacy webhook type names mapped to current formats, from
4
+ * `MAGNUS_WEBHOOK_TYPE_ALIASES` (comma-separated `legacy=format` pairs). Lets
5
+ * endpoints stored or submitted under an older type name keep their format.
6
+ */
7
+ export function webhookTypeAliases(raw = process.env['MAGNUS_WEBHOOK_TYPE_ALIASES']) {
8
+ if (raw === undefined || raw.trim() === '')
9
+ return {};
10
+ const aliases = {};
11
+ for (const pair of raw.split(',').map(item => item.trim()).filter(Boolean)) {
12
+ const [from, to, extra] = pair.split('=').map(item => item.trim());
13
+ if (extra !== undefined || !from || !to || !/^[a-z][a-z0-9_-]{0,31}$/.test(from)
14
+ || WEBHOOK_FORMATS.includes(from) || !WEBHOOK_FORMATS.includes(to))
15
+ throw new Error('MAGNUS_WEBHOOK_TYPE_ALIASES must list legacy=format pairs, where format is generic, slack, discord or mattermost');
16
+ aliases[from] = to;
17
+ }
18
+ return aliases;
19
+ }
20
+ /** The current delivery format for a stored or submitted webhook type. */
21
+ export function webhookFormat(type) {
22
+ let aliases = {};
23
+ try {
24
+ aliases = webhookTypeAliases();
25
+ }
26
+ catch { /* Startup validation reports a malformed list. */ }
27
+ return Object.hasOwn(aliases, type) ? aliases[type] : type;
28
+ }
29
+ //# sourceMappingURL=webhook-types.js.map
@@ -0,0 +1,23 @@
1
+ import { mkdirSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { magnusHome } from './magnus-home.js';
4
+ /**
5
+ * `run/server-runtime.json` tells upgrade tooling which version is actually
6
+ * serving (the public health endpoints deliberately omit the version).
7
+ */
8
+ export function runtimeFilePath(env = process.env) {
9
+ return env['MAGNUS_RUNTIME_FILE']?.trim() || join(magnusHome(env), 'run', 'server-runtime.json');
10
+ }
11
+ export function writeRuntimeFile(details, env = process.env) {
12
+ const path = runtimeFilePath(env);
13
+ try {
14
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
15
+ const temporary = `${path}.${process.pid}.tmp`;
16
+ writeFileSync(temporary, `${JSON.stringify({ ...details, pid: process.pid, startedAt: new Date().toISOString() })}\n`, { mode: 0o600 });
17
+ renameSync(temporary, path);
18
+ }
19
+ catch (error) {
20
+ console.warn(`[startup] could not write ${path}: ${error instanceof Error ? error.message : String(error)}`);
21
+ }
22
+ }
23
+ //# sourceMappingURL=runtime-file.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hivemagnus/magnus-server",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Magnus server: the fleet control plane API (Hono, PostgreSQL, Redis) for nodes running magnus-agent, with optional Hive IAM, Hive Router, HiveMem, Hermes Agent and external security console integrations.",
5
5
  "keywords": [
6
6
  "magnus",
@@ -84,7 +84,7 @@
84
84
  "prepublishOnly": "npm run db:generate && npm run clean && npm run build"
85
85
  },
86
86
  "dependencies": {
87
- "@hivemagnus/magnus-contracts": "1.0.2",
87
+ "@hivemagnus/magnus-contracts": "1.1.0",
88
88
  "@hono/node-server": "^2.1.5",
89
89
  "@hono/swagger-ui": "^0.6.2",
90
90
  "@prisma/adapter-pg": "7.10.1",
@@ -102,7 +102,7 @@
102
102
  "ws": "^8.22.0",
103
103
  "yaml": "2.9.1",
104
104
  "zod": "^4.6.7",
105
- "@hivemagnus/magnus-skills": "1.0.2",
105
+ "@hivemagnus/magnus-skills": "1.1.0",
106
106
  "prisma": "^7.10.1"
107
107
  },
108
108
  "devDependencies": {