@hivemagnus/magnus-server 1.0.1 → 1.0.3
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 +10 -0
- package/README.md +15 -0
- package/agent-version.json +1 -1
- package/dist/cli/env-catalog.js +3 -0
- package/dist/config/env.js +26 -0
- package/dist/config/route-aliases.js +73 -0
- package/dist/db/generated/prisma/internal/class.js +3 -3
- package/dist/db/generated/prisma/internal/prismaNamespace.js +4 -4
- package/dist/index.js +4 -2
- package/dist/sdk/node-api-client.js +2 -2
- package/dist/security/approval-assertion.js +23 -1
- package/dist/services/webhook-service.js +13 -10
- package/dist/services/webhook-types.js +29 -0
- package/package.json +19 -19
package/.env.example
CHANGED
|
@@ -86,6 +86,13 @@ CORS_ORIGIN=https://magnus.example.com:3000
|
|
|
86
86
|
# Optional integrations — each is off until configured
|
|
87
87
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
88
88
|
|
|
89
|
+
# ── Interoperating with older clients (optional) ──
|
|
90
|
+
# JSON object mapping legacy /api/v1 path prefixes to current ones. Requests are
|
|
91
|
+
# rewritten before routing, so auth, scopes and audit see the current path.
|
|
92
|
+
# MAGNUS_ROUTE_ALIASES_JSON={"/api/v1/resource-leases/old-name/admission":"/api/v1/resource-leases/admission"}
|
|
93
|
+
# Legacy webhook type names mapped to a current format (generic, slack, discord, mattermost).
|
|
94
|
+
# MAGNUS_WEBHOOK_TYPE_ALIASES=legacy-chat=mattermost
|
|
95
|
+
|
|
89
96
|
# ── Hive IAM: verify RS256 access tokens issued by Hive IAM ──
|
|
90
97
|
# When set, JWT_SECRET is no longer required for user tokens.
|
|
91
98
|
# IAM_CORE_URL=https://iam.example.com
|
|
@@ -139,6 +146,9 @@ CORS_ORIGIN=https://magnus.example.com:3000
|
|
|
139
146
|
# APPROVAL_SIGNER_PUBLIC_KEY_PATH=/etc/magnus-server/approval-signer.pub
|
|
140
147
|
# APPROVAL_TELEGRAM_ACTOR_SHA256=
|
|
141
148
|
# APPROVAL_IPHONE_ACTOR_SHA256=
|
|
149
|
+
# Signature domain prefixes accepted for approval decisions; the first is preferred.
|
|
150
|
+
# Add a legacy signer domain (for example legacy.approval) to interoperate with older signers.
|
|
151
|
+
# MAGNUS_APPROVAL_ASSERTION_DOMAINS=magnus.approval-assertion
|
|
142
152
|
# Read-only digests for an external observability collector and the fleet-check reporter.
|
|
143
153
|
# MAGNUS_COLLECTOR_TOKEN_SHA256=
|
|
144
154
|
# MAGNUS_FLEET_CHECK_TOKEN_SHA256=
|
package/README.md
CHANGED
|
@@ -132,6 +132,21 @@ Unknown destinations and changed keys fail before authentication. Multiple
|
|
|
132
132
|
pins permit a reviewed host-key rotation; remove the old pin afterward.
|
|
133
133
|
This setting does not enable the retired browser deployment routes.
|
|
134
134
|
|
|
135
|
+
## Interoperating with older components
|
|
136
|
+
|
|
137
|
+
Every switch below is off or set to current behaviour by default. Set them only
|
|
138
|
+
while older clients, signers or scripts still talk to this server.
|
|
139
|
+
|
|
140
|
+
| Variable | Default | Purpose |
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| `MAGNUS_APPROVAL_ASSERTION_DOMAINS` | `magnus.approval-assertion` | Comma-separated signature domain prefixes accepted for signed approval decisions (the signed message starts with `<prefix>.v<N>`). Add a legacy signer domain, for example `magnus.approval-assertion,legacy.approval`. The first entry is preferred. Allowed characters: `a-z`, `0-9`, `.` and `-`. |
|
|
143
|
+
| `MAGNUS_ROUTE_ALIASES_JSON` | unset | JSON object mapping legacy `/api/v1` path prefixes to current ones, for example `{"/api/v1/resource-leases/old-name/admission": "/api/v1/resource-leases/admission"}`. Requests are rewritten before routing, so authentication, endpoint scopes, rate limits and audit see the current path. Aliases cannot chain or contain `.`/`..` segments. |
|
|
144
|
+
| `MAGNUS_WEBHOOK_TYPE_ALIASES` | unset | Comma-separated `legacy=format` pairs, for example `legacy-chat=mattermost`, so webhook endpoints stored or submitted under an older type name keep their delivery format. New and edited endpoints store the current format. |
|
|
145
|
+
|
|
146
|
+
Older Hermes Agent plugins also need `MAGNUS_PLUGIN_API_ENABLED=true`, and an
|
|
147
|
+
older approval bridge needs `MAGNUS_APPROVAL_BRIDGE_CONSUMER`. See the
|
|
148
|
+
[changelog](../../CHANGELOG.md) for renamed variables.
|
|
149
|
+
|
|
135
150
|
## Develop
|
|
136
151
|
|
|
137
152
|
From the repository root:
|
package/agent-version.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.3"}
|
package/dist/cli/env-catalog.js
CHANGED
|
@@ -3,6 +3,8 @@ export const ENV_CATALOG = [
|
|
|
3
3
|
// ── Core ──
|
|
4
4
|
{ name: 'NODE_ENV', group: 'core', default: 'development', description: 'development, production or test. Production enforces TLS, DATABASE_URL and strict CORS.' },
|
|
5
5
|
{ name: 'LOG_LEVEL', group: 'core', default: 'info', description: 'debug, info, warn, error or fatal.' },
|
|
6
|
+
{ name: 'MAGNUS_ROUTE_ALIASES_JSON', group: 'core', description: 'Optional JSON object mapping legacy /api/v1 path prefixes to current ones, for clients built against older route names. Requests are rewritten before routing, so auth and audit see the current path. Unset means no aliases.' },
|
|
7
|
+
{ name: 'MAGNUS_WEBHOOK_TYPE_ALIASES', group: 'core', description: 'Optional comma-separated legacy=format pairs (format: generic, slack, discord or mattermost) so webhook endpoints stored or submitted under an older type name keep their delivery format.' },
|
|
6
8
|
{ name: 'MAGNUS_HOME', group: 'paths', default: '~/.magnus', description: 'Base directory for server-owned data (audit logs, credential rotation state, backups).' },
|
|
7
9
|
{ name: 'MAGNUS_ENV_FILE', group: 'paths', description: 'Env file loaded at startup (set by `--env-file`). Variables already in the environment win.' },
|
|
8
10
|
{ name: 'AUDIT_LOG_DIR', group: 'paths', default: '$MAGNUS_HOME/audit-logs', description: 'Directory for the append-only audit log files.' },
|
|
@@ -81,6 +83,7 @@ export const ENV_CATALOG = [
|
|
|
81
83
|
{ name: 'APPROVAL_SIGNER_PUBLIC_KEY_PEM', group: 'plugins', description: 'Ed25519 public key (PEM) that signs approval decisions. _B64 (DER) and _PATH variants are also accepted.' },
|
|
82
84
|
{ name: 'APPROVAL_TELEGRAM_ACTOR_SHA256', group: 'plugins', description: 'Actor binding digest for decisions from the Telegram channel.' },
|
|
83
85
|
{ name: 'APPROVAL_IPHONE_ACTOR_SHA256', group: 'plugins', description: 'Actor binding digest for decisions from the iPhone channel.' },
|
|
86
|
+
{ name: 'MAGNUS_APPROVAL_ASSERTION_DOMAINS', group: 'plugins', default: 'magnus.approval-assertion', description: 'Comma-separated signature domain prefixes accepted for approval decisions (signed message is <prefix>.v<N>). The first is preferred; add a legacy signer domain to keep older signers working.' },
|
|
84
87
|
{ name: 'MAGNUS_COLLECTOR_TOKEN_SHA256', group: 'plugins', description: 'SHA-256 digest of a read-only external observability collector credential.' },
|
|
85
88
|
{ name: 'MAGNUS_FLEET_CHECK_TOKEN_SHA256', group: 'plugins', description: 'SHA-256 digest of the fleet-check reporter credential.' },
|
|
86
89
|
// ── Hermes registry ──
|
package/dist/config/env.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { parseRouteAliases } from './route-aliases.js';
|
|
4
|
+
import { webhookTypeAliases } from '../services/webhook-types.js';
|
|
3
5
|
// ─── Shared environment schema — validated with Zod ───
|
|
4
6
|
const portSchema = z.coerce.number().int().min(1).max(65535);
|
|
5
7
|
const booleanEnvSchema = z.enum(['true', 'false']).default('false').transform((value) => value === 'true');
|
|
@@ -48,6 +50,10 @@ export const envSchema = z.object({
|
|
|
48
50
|
APPROVAL_TELEGRAM_ACTOR_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
49
51
|
APPROVAL_IPHONE_ACTOR_SHA256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
|
|
50
52
|
MAGNUS_APPROVAL_BRIDGE_CONSUMER: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/i).optional(),
|
|
53
|
+
// Accepted approval-assertion signature domain prefixes (comma-separated); first is preferred.
|
|
54
|
+
MAGNUS_APPROVAL_ASSERTION_DOMAINS: z.string()
|
|
55
|
+
.regex(/^\s*[a-z0-9](?:[a-z0-9.-]{0,98}[a-z0-9])?\s*(?:,\s*[a-z0-9](?:[a-z0-9.-]{0,98}[a-z0-9])?\s*)*$/, 'must list domain prefixes made of a-z, 0-9, "." and "-", separated by commas')
|
|
56
|
+
.optional(),
|
|
51
57
|
// Security Fusion: off (default) | local (in-process) | remote (external security console).
|
|
52
58
|
SECURITY_FUSION_MODE: z.enum(['off', 'local', 'remote']).default('off'),
|
|
53
59
|
SECURITY_FUSION_API_URL: z.string().url().optional(),
|
|
@@ -151,7 +157,27 @@ export const envSchema = z.object({
|
|
|
151
157
|
HERMES_HIVEMEM_WORKSPACE_ID: z.string().optional(),
|
|
152
158
|
HERMES_HIVEMEM_DEFAULT_SCOPE: z.enum(['private', 'shared']).optional(),
|
|
153
159
|
HERMES_OPENAI_RUNTIME: z.enum(['auto', 'codex_app_server']).optional(),
|
|
160
|
+
// Optional legacy-path -> current-path prefix map for older clients (see config/route-aliases.ts).
|
|
161
|
+
MAGNUS_ROUTE_ALIASES_JSON: z.string().optional(),
|
|
162
|
+
// Optional legacy webhook type names: comma-separated legacy=format pairs.
|
|
163
|
+
MAGNUS_WEBHOOK_TYPE_ALIASES: z.string()
|
|
164
|
+
.regex(/^\s*(?:[a-z][a-z0-9_-]{0,31}\s*=\s*(?:generic|slack|discord|mattermost)\s*(?:,\s*|$))*$/, 'must list legacy=format pairs, where format is generic, slack, discord or mattermost')
|
|
165
|
+
.optional(),
|
|
154
166
|
}).superRefine((env, ctx) => {
|
|
167
|
+
try {
|
|
168
|
+
webhookTypeAliases(env.MAGNUS_WEBHOOK_TYPE_ALIASES);
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['MAGNUS_WEBHOOK_TYPE_ALIASES'],
|
|
172
|
+
message: error instanceof Error ? error.message : 'MAGNUS_WEBHOOK_TYPE_ALIASES is invalid' });
|
|
173
|
+
}
|
|
174
|
+
try {
|
|
175
|
+
parseRouteAliases(env.MAGNUS_ROUTE_ALIASES_JSON);
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ['MAGNUS_ROUTE_ALIASES_JSON'],
|
|
179
|
+
message: error instanceof Error ? error.message : 'MAGNUS_ROUTE_ALIASES_JSON is invalid' });
|
|
180
|
+
}
|
|
155
181
|
if (env.HEALTH_SNAPSHOT_FULL_FIDELITY_HOURS > env.HEALTH_SNAPSHOT_RETENTION_DAYS * 24) {
|
|
156
182
|
ctx.addIssue({
|
|
157
183
|
code: z.ZodIssueCode.custom,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional request path aliases for clients built against older route names.
|
|
3
|
+
*
|
|
4
|
+
* `MAGNUS_ROUTE_ALIASES_JSON` is a JSON object that maps a legacy path prefix
|
|
5
|
+
* to the current one, for example
|
|
6
|
+
* `{"/api/v1/old/prefix": "/api/v1/new/prefix"}`. A request whose path equals a
|
|
7
|
+
* legacy prefix, or continues it after a `/`, is rewritten before routing, so
|
|
8
|
+
* authentication, endpoint scopes, rate limits and audit all see the current
|
|
9
|
+
* path. Unset (the default) means no aliases.
|
|
10
|
+
*/
|
|
11
|
+
const segment = '[A-Za-z0-9._~-]+';
|
|
12
|
+
const aliasPath = new RegExp(`^/api/v1(?:/${segment})+$`);
|
|
13
|
+
function validPath(path) {
|
|
14
|
+
return aliasPath.test(path) && !path.split('/').some(part => part === '.' || part === '..');
|
|
15
|
+
}
|
|
16
|
+
/** Parse and validate the alias map; throws a descriptive error when it is malformed. */
|
|
17
|
+
export function parseRouteAliases(raw) {
|
|
18
|
+
if (raw === undefined || raw.trim() === '')
|
|
19
|
+
return [];
|
|
20
|
+
let value;
|
|
21
|
+
try {
|
|
22
|
+
value = JSON.parse(raw);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON must be a JSON object');
|
|
26
|
+
}
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
28
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON must be a JSON object');
|
|
29
|
+
const entries = Object.entries(value);
|
|
30
|
+
if (entries.length > 64)
|
|
31
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON may define at most 64 aliases');
|
|
32
|
+
const aliases = [];
|
|
33
|
+
for (const [from, to] of entries) {
|
|
34
|
+
if (typeof to !== 'string' || !validPath(from) || !validPath(to))
|
|
35
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON keys and values must be /api/v1/... paths without "." or ".." segments');
|
|
36
|
+
if (from === to)
|
|
37
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON maps a path to itself');
|
|
38
|
+
aliases.push({ from, to });
|
|
39
|
+
}
|
|
40
|
+
for (const { to } of aliases)
|
|
41
|
+
if (aliases.some(alias => to === alias.from || to.startsWith(`${alias.from}/`)))
|
|
42
|
+
throw new Error('MAGNUS_ROUTE_ALIASES_JSON targets must not be aliases themselves');
|
|
43
|
+
// Longest prefix first, so a more specific alias wins.
|
|
44
|
+
return aliases.sort((a, b) => b.from.length - a.from.length);
|
|
45
|
+
}
|
|
46
|
+
/** The current path for `path`, or null when no alias applies. */
|
|
47
|
+
export function resolveRouteAlias(path, aliases) {
|
|
48
|
+
for (const { from, to } of aliases) {
|
|
49
|
+
if (path === from)
|
|
50
|
+
return to;
|
|
51
|
+
if (path.startsWith(`${from}/`))
|
|
52
|
+
return to + path.slice(from.length);
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
/** Wrap a fetch handler so legacy paths reach their current routes. Returns `fetch` unchanged without aliases. */
|
|
57
|
+
export function withRouteAliases(fetch, aliases) {
|
|
58
|
+
if (!aliases.length)
|
|
59
|
+
return fetch;
|
|
60
|
+
return (request, env, context) => {
|
|
61
|
+
const url = new URL(request.url);
|
|
62
|
+
const target = resolveRouteAlias(url.pathname, aliases);
|
|
63
|
+
if (target === null)
|
|
64
|
+
return fetch(request, env, context);
|
|
65
|
+
url.pathname = target;
|
|
66
|
+
const init = {
|
|
67
|
+
method: request.method, headers: request.headers, redirect: request.redirect, signal: request.signal,
|
|
68
|
+
...(request.body ? { body: request.body, duplex: 'half' } : {}),
|
|
69
|
+
};
|
|
70
|
+
return fetch(new Request(url, init), env, context);
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=route-aliases.js.map
|