@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.
- package/.env.example +30 -0
- package/README.md +22 -3
- package/agent-version.json +3 -1
- package/dist/cli/agents.js +111 -0
- package/dist/cli/args.js +19 -4
- package/dist/cli/auto-migrate.js +85 -0
- package/dist/cli/env-catalog.js +17 -0
- package/dist/cli/migrations.js +117 -0
- package/dist/cli/preflight.js +5 -14
- package/dist/cli/upgrade.js +55 -0
- package/dist/cli.js +121 -7
- package/dist/config/env.js +26 -0
- package/dist/config/features.js +22 -0
- package/dist/config/route-aliases.js +73 -0
- package/dist/index.js +13 -5
- package/dist/openapi/spec.js +72 -31
- package/dist/routes/agent-upgrades.js +42 -0
- package/dist/routes/heartbeat.js +8 -1
- package/dist/routes/self-update.js +19 -0
- package/dist/routes/system-updates.js +27 -0
- package/dist/sdk/node-api-client.js +4 -3
- package/dist/security/approval-assertion.js +23 -1
- package/dist/security/native-completion-registry.js +2 -0
- package/dist/server.js +5 -0
- package/dist/services/agent-upgrade-service.js +293 -0
- package/dist/services/agent-version-warnings.js +31 -0
- package/dist/services/self-update-service.js +11 -8
- package/dist/services/update-check-service.js +178 -0
- package/dist/services/webhook-service.js +13 -10
- package/dist/services/webhook-types.js +29 -0
- package/dist/utils/runtime-file.js +23 -0
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
395
|
+
const type = webhookFormat(data.type);
|
|
396
|
+
if (!WEBHOOK_FORMATS.includes(type))
|
|
394
397
|
throw new Error('Unsupported webhook format');
|
|
395
|
-
updateData['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
|
|
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
|
|
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
|
|
105
|
+
"@hivemagnus/magnus-skills": "1.1.0",
|
|
106
106
|
"prisma": "^7.10.1"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|