@hivemagnus/magnus-contracts 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/dist/index.d.ts CHANGED
@@ -407,3 +407,4 @@ export declare const securityInventoryExportSchema: z.ZodObject<{
407
407
  }, z.core.$strip>;
408
408
  export type SecurityInventoryTarget = z.infer<typeof securityInventoryTargetSchema>;
409
409
  export type SecurityInventoryExport = z.infer<typeof securityInventoryExportSchema>;
410
+ export * from './release.js';
package/dist/index.js CHANGED
@@ -165,3 +165,4 @@ export const securityInventoryTargetSchema = z.object({
165
165
  export const securityInventoryExportSchema = z.object({
166
166
  data: z.array(securityInventoryTargetSchema).max(10000),
167
167
  });
168
+ export * from './release.js';
@@ -0,0 +1,202 @@
1
+ import { z } from 'zod';
2
+ export interface ParsedVersion {
3
+ major: number;
4
+ minor: number;
5
+ patch: number;
6
+ prerelease: string[];
7
+ }
8
+ export declare function parseVersion(version: string): ParsedVersion | null;
9
+ export declare function isVersion(version: string): boolean;
10
+ /** Returns -1, 0 or 1. Throws on anything that is not `X.Y.Z[-pre]`. */
11
+ export declare function compareVersions(a: string, b: string): number;
12
+ /** Supports `^X.Y.Z`, `X.Y.Z` and `||` unions, the forms Magnus declares in `engines`. */
13
+ export declare function satisfiesRange(version: string, range: string): boolean;
14
+ export type UpgradeKind = 'same' | 'patch' | 'minor' | 'major' | 'downgrade';
15
+ export declare function classifyUpgrade(from: string, to: string): UpgradeKind;
16
+ export interface UpgradeDecision {
17
+ allowed: boolean;
18
+ kind: UpgradeKind;
19
+ reason?: string;
20
+ }
21
+ /**
22
+ * The one rule every automatic path shares: never downgrade (that is a
23
+ * rollback) and never cross a major version unless an operator asked for it.
24
+ */
25
+ export declare function checkUpgradeAllowed(from: string, to: string, options?: {
26
+ allowMajor?: boolean;
27
+ }): UpgradeDecision;
28
+ /** Highest version in `versions` that is newer than `current`, optionally limited to its major. */
29
+ export declare function latestVersion(versions: Iterable<string>, options?: {
30
+ sameMajorAs?: string;
31
+ includePrerelease?: boolean;
32
+ }): string | null;
33
+ export declare const releaseEnvChangeSchema: z.ZodObject<{
34
+ name: z.ZodString;
35
+ component: z.ZodEnum<{
36
+ server: "server";
37
+ ui: "ui";
38
+ agent: "agent";
39
+ ardent: "ardent";
40
+ }>;
41
+ secret: z.ZodDefault<z.ZodBoolean>;
42
+ description: z.ZodString;
43
+ }, z.core.$strip>;
44
+ export declare const migrationImpactSchema: z.ZodEnum<{
45
+ fast: "fast";
46
+ slow: "slow";
47
+ "one-way": "one-way";
48
+ }>;
49
+ export declare const releaseMigrationSchema: z.ZodObject<{
50
+ name: z.ZodString;
51
+ impact: z.ZodEnum<{
52
+ fast: "fast";
53
+ slow: "slow";
54
+ "one-way": "one-way";
55
+ }>;
56
+ note: z.ZodDefault<z.ZodString>;
57
+ }, z.core.$strip>;
58
+ export declare const releaseManifestSchema: z.ZodObject<{
59
+ schema: z.ZodLiteral<1>;
60
+ version: z.ZodString;
61
+ releasedAt: z.ZodOptional<z.ZodString>;
62
+ engines: z.ZodObject<{
63
+ node: z.ZodString;
64
+ }, z.core.$strip>;
65
+ components: z.ZodRecord<z.ZodString, z.ZodString>;
66
+ breaking: z.ZodDefault<z.ZodBoolean>;
67
+ minUpgradeFrom: z.ZodOptional<z.ZodString>;
68
+ engineProtocol: z.ZodNumber;
69
+ newRequiredEnv: z.ZodDefault<z.ZodArray<z.ZodObject<{
70
+ name: z.ZodString;
71
+ component: z.ZodEnum<{
72
+ server: "server";
73
+ ui: "ui";
74
+ agent: "agent";
75
+ ardent: "ardent";
76
+ }>;
77
+ secret: z.ZodDefault<z.ZodBoolean>;
78
+ description: z.ZodString;
79
+ }, z.core.$strip>>>;
80
+ newOptionalEnv: z.ZodDefault<z.ZodArray<z.ZodObject<{
81
+ name: z.ZodString;
82
+ component: z.ZodEnum<{
83
+ server: "server";
84
+ ui: "ui";
85
+ agent: "agent";
86
+ ardent: "ardent";
87
+ }>;
88
+ secret: z.ZodDefault<z.ZodBoolean>;
89
+ description: z.ZodString;
90
+ }, z.core.$strip>>>;
91
+ removedEnv: z.ZodDefault<z.ZodArray<z.ZodObject<{
92
+ name: z.ZodString;
93
+ component: z.ZodString;
94
+ note: z.ZodDefault<z.ZodString>;
95
+ }, z.core.$strip>>>;
96
+ migrations: z.ZodDefault<z.ZodArray<z.ZodObject<{
97
+ name: z.ZodString;
98
+ impact: z.ZodEnum<{
99
+ fast: "fast";
100
+ slow: "slow";
101
+ "one-way": "one-way";
102
+ }>;
103
+ note: z.ZodDefault<z.ZodString>;
104
+ }, z.core.$strip>>>;
105
+ agent: z.ZodObject<{
106
+ selfUpgradeProtocol: z.ZodNumber;
107
+ minLauncher: z.ZodNumber;
108
+ }, z.core.$strip>;
109
+ summary: z.ZodDefault<z.ZodString>;
110
+ notesUrl: z.ZodOptional<z.ZodString>;
111
+ }, z.core.$strip>;
112
+ export type ReleaseManifest = z.infer<typeof releaseManifestSchema>;
113
+ export type ReleaseEnvChange = z.infer<typeof releaseEnvChangeSchema>;
114
+ export type ReleaseMigration = z.infer<typeof releaseMigrationSchema>;
115
+ export type MigrationImpact = z.infer<typeof migrationImpactSchema>;
116
+ export interface UpgradePath {
117
+ from: string;
118
+ to: string;
119
+ releases: ReleaseManifest[];
120
+ breaking: boolean;
121
+ newRequiredEnv: ReleaseEnvChange[];
122
+ newOptionalEnv: ReleaseEnvChange[];
123
+ removedEnv: ReleaseManifest['removedEnv'];
124
+ migrations: Array<ReleaseMigration & {
125
+ version: string;
126
+ }>;
127
+ /** Migrations an operator must confirm: slow or impossible to undo without a restore. */
128
+ attention: Array<ReleaseMigration & {
129
+ version: string;
130
+ }>;
131
+ blockedBy?: string;
132
+ }
133
+ /**
134
+ * Merges the manifests of every release in (from, to]. Env names added and
135
+ * later removed inside the range are dropped. A release whose
136
+ * `minUpgradeFrom` is newer than `from` blocks the path.
137
+ */
138
+ export declare function summarizeUpgradePath(manifests: Iterable<ReleaseManifest>, from: string, to: string): UpgradePath;
139
+ /** Reads the manifests embedded as `magnusRelease` in a full npm packument. */
140
+ export declare function manifestsFromPackument(packument: unknown): ReleaseManifest[];
141
+ export declare const AGENT_SELF_UPGRADE_PROTOCOL = 1;
142
+ export declare const agentUpgradeStateSchema: z.ZodEnum<{
143
+ failed: "failed";
144
+ idle: "idle";
145
+ staged: "staged";
146
+ trial: "trial";
147
+ confirmed: "confirmed";
148
+ reverted: "reverted";
149
+ }>;
150
+ export declare const agentUpgradeReportSchema: z.ZodObject<{
151
+ state: z.ZodEnum<{
152
+ failed: "failed";
153
+ idle: "idle";
154
+ staged: "staged";
155
+ trial: "trial";
156
+ confirmed: "confirmed";
157
+ reverted: "reverted";
158
+ }>;
159
+ from: z.ZodOptional<z.ZodNullable<z.ZodString>>;
160
+ to: z.ZodOptional<z.ZodNullable<z.ZodString>>;
161
+ requestId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
162
+ reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
163
+ attempt: z.ZodOptional<z.ZodNumber>;
164
+ updatedAt: z.ZodOptional<z.ZodString>;
165
+ }, z.core.$strip>;
166
+ export declare const agentSelfUpgradeStatusSchema: z.ZodObject<{
167
+ protocol: z.ZodNumber;
168
+ launcherProtocol: z.ZodNullable<z.ZodNumber>;
169
+ layout: z.ZodEnum<{
170
+ versioned: "versioned";
171
+ legacy: "legacy";
172
+ container: "container";
173
+ }>;
174
+ enabled: z.ZodBoolean;
175
+ reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
176
+ upgrade: z.ZodOptional<z.ZodNullable<z.ZodObject<{
177
+ state: z.ZodEnum<{
178
+ failed: "failed";
179
+ idle: "idle";
180
+ staged: "staged";
181
+ trial: "trial";
182
+ confirmed: "confirmed";
183
+ reverted: "reverted";
184
+ }>;
185
+ from: z.ZodOptional<z.ZodNullable<z.ZodString>>;
186
+ to: z.ZodOptional<z.ZodNullable<z.ZodString>>;
187
+ requestId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
188
+ reason: z.ZodOptional<z.ZodNullable<z.ZodString>>;
189
+ attempt: z.ZodOptional<z.ZodNumber>;
190
+ updatedAt: z.ZodOptional<z.ZodString>;
191
+ }, z.core.$strip>>>;
192
+ }, z.core.$strip>;
193
+ export declare const agentUpgradeRequestSchema: z.ZodObject<{
194
+ version: z.ZodString;
195
+ integrity: z.ZodString;
196
+ requestId: z.ZodString;
197
+ packageName: z.ZodDefault<z.ZodString>;
198
+ }, z.core.$strip>;
199
+ export type AgentUpgradeState = z.infer<typeof agentUpgradeStateSchema>;
200
+ export type AgentUpgradeReport = z.infer<typeof agentUpgradeReportSchema>;
201
+ export type AgentSelfUpgradeStatus = z.infer<typeof agentSelfUpgradeStatusSchema>;
202
+ export type AgentUpgradeRequest = z.infer<typeof agentUpgradeRequestSchema>;
@@ -0,0 +1,238 @@
1
+ import { z } from 'zod';
2
+ // Release metadata shared by the upgrade tooling (`@hivemagnus/magnus`), the
3
+ // server's update check and the node agent's self-upgrade. Only plain
4
+ // `X.Y.Z[-pre]` versions and `^`/exact/`||` ranges are supported, which is all
5
+ // Magnus itself publishes or declares.
6
+ const VERSION_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?$/;
7
+ export function parseVersion(version) {
8
+ const match = VERSION_PATTERN.exec(version.trim().replace(/^v/, ''));
9
+ if (!match)
10
+ return null;
11
+ return {
12
+ major: Number(match[1]),
13
+ minor: Number(match[2]),
14
+ patch: Number(match[3]),
15
+ prerelease: match[4] ? match[4].split('.') : [],
16
+ };
17
+ }
18
+ export function isVersion(version) {
19
+ return parseVersion(version) !== null;
20
+ }
21
+ function requireVersion(version) {
22
+ const parsed = parseVersion(version);
23
+ if (!parsed)
24
+ throw new Error(`Not a release version: ${version}`);
25
+ return parsed;
26
+ }
27
+ function comparePrerelease(a, b) {
28
+ if (a.length === 0 || b.length === 0)
29
+ return a.length === b.length ? 0 : a.length === 0 ? 1 : -1;
30
+ for (let i = 0; i < Math.max(a.length, b.length); i++) {
31
+ if (a[i] === undefined)
32
+ return -1;
33
+ if (b[i] === undefined)
34
+ return 1;
35
+ const an = /^\d+$/.test(a[i]) ? Number(a[i]) : null;
36
+ const bn = /^\d+$/.test(b[i]) ? Number(b[i]) : null;
37
+ if (an !== null && bn !== null) {
38
+ if (an !== bn)
39
+ return an < bn ? -1 : 1;
40
+ }
41
+ else if (an !== null || bn !== null) {
42
+ return an !== null ? -1 : 1;
43
+ }
44
+ else if (a[i] !== b[i]) {
45
+ return a[i] < b[i] ? -1 : 1;
46
+ }
47
+ }
48
+ return 0;
49
+ }
50
+ /** Returns -1, 0 or 1. Throws on anything that is not `X.Y.Z[-pre]`. */
51
+ export function compareVersions(a, b) {
52
+ const left = requireVersion(a);
53
+ const right = requireVersion(b);
54
+ for (const key of ['major', 'minor', 'patch']) {
55
+ if (left[key] !== right[key])
56
+ return left[key] < right[key] ? -1 : 1;
57
+ }
58
+ return comparePrerelease(left.prerelease, right.prerelease);
59
+ }
60
+ /** Supports `^X.Y.Z`, `X.Y.Z` and `||` unions, the forms Magnus declares in `engines`. */
61
+ export function satisfiesRange(version, range) {
62
+ const candidate = parseVersion(version);
63
+ if (!candidate)
64
+ return false;
65
+ return range.split('||').map((part) => part.trim()).some((part) => {
66
+ if (part === '*' || part === '')
67
+ return true;
68
+ const caret = part.startsWith('^');
69
+ const floor = parseVersion(caret ? part.slice(1) : part);
70
+ if (!floor)
71
+ return false;
72
+ if (!caret)
73
+ return compareVersions(version, part) === 0;
74
+ if (compareVersions(version, part.slice(1)) < 0)
75
+ return false;
76
+ if (candidate.prerelease.length > 0)
77
+ return false;
78
+ if (floor.major > 0)
79
+ return candidate.major === floor.major;
80
+ if (floor.minor > 0)
81
+ return candidate.major === 0 && candidate.minor === floor.minor;
82
+ return candidate.major === 0 && candidate.minor === 0 && candidate.patch === floor.patch;
83
+ });
84
+ }
85
+ export function classifyUpgrade(from, to) {
86
+ const order = compareVersions(from, to);
87
+ if (order === 0)
88
+ return 'same';
89
+ if (order > 0)
90
+ return 'downgrade';
91
+ const a = requireVersion(from);
92
+ const b = requireVersion(to);
93
+ if (a.major !== b.major)
94
+ return 'major';
95
+ if (a.minor !== b.minor)
96
+ return 'minor';
97
+ return 'patch';
98
+ }
99
+ /**
100
+ * The one rule every automatic path shares: never downgrade (that is a
101
+ * rollback) and never cross a major version unless an operator asked for it.
102
+ */
103
+ export function checkUpgradeAllowed(from, to, options = {}) {
104
+ const kind = classifyUpgrade(from, to);
105
+ if (kind === 'downgrade')
106
+ return { allowed: false, kind, reason: `${to} is older than ${from}; use rollback instead` };
107
+ if (kind === 'major' && !options.allowMajor) {
108
+ return { allowed: false, kind, reason: `${from} -> ${to} crosses a major version; pass --allow-major after reading UPGRADING.md` };
109
+ }
110
+ return { allowed: true, kind };
111
+ }
112
+ /** Highest version in `versions` that is newer than `current`, optionally limited to its major. */
113
+ export function latestVersion(versions, options = {}) {
114
+ const major = options.sameMajorAs ? requireVersion(options.sameMajorAs).major : null;
115
+ let best = null;
116
+ for (const version of versions) {
117
+ const parsed = parseVersion(version);
118
+ if (!parsed)
119
+ continue;
120
+ if (!options.includePrerelease && parsed.prerelease.length > 0)
121
+ continue;
122
+ if (major !== null && parsed.major !== major)
123
+ continue;
124
+ if (best === null || compareVersions(version, best) > 0)
125
+ best = version;
126
+ }
127
+ return best;
128
+ }
129
+ const versionString = z.string().refine(isVersion, 'must be an X.Y.Z version');
130
+ export const releaseEnvChangeSchema = z.object({
131
+ name: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
132
+ component: z.enum(['server', 'ui', 'agent', 'ardent']),
133
+ secret: z.boolean().default(false),
134
+ description: z.string().min(1),
135
+ });
136
+ export const migrationImpactSchema = z.enum(['fast', 'slow', 'one-way']);
137
+ export const releaseMigrationSchema = z.object({
138
+ name: z.string().min(1),
139
+ impact: migrationImpactSchema,
140
+ note: z.string().default(''),
141
+ });
142
+ export const releaseManifestSchema = z.object({
143
+ schema: z.literal(1),
144
+ version: versionString,
145
+ releasedAt: z.string().optional(),
146
+ engines: z.object({ node: z.string().min(1) }),
147
+ components: z.record(z.string(), versionString),
148
+ breaking: z.boolean().default(false),
149
+ minUpgradeFrom: versionString.optional(),
150
+ engineProtocol: z.number().int().positive(),
151
+ newRequiredEnv: z.array(releaseEnvChangeSchema).default([]),
152
+ newOptionalEnv: z.array(releaseEnvChangeSchema).default([]),
153
+ removedEnv: z.array(z.object({ name: z.string(), component: z.string(), note: z.string().default('') })).default([]),
154
+ migrations: z.array(releaseMigrationSchema).default([]),
155
+ agent: z.object({ selfUpgradeProtocol: z.number().int().nonnegative(), minLauncher: z.number().int().nonnegative() }),
156
+ summary: z.string().default(''),
157
+ notesUrl: z.string().url().optional(),
158
+ });
159
+ /**
160
+ * Merges the manifests of every release in (from, to]. Env names added and
161
+ * later removed inside the range are dropped. A release whose
162
+ * `minUpgradeFrom` is newer than `from` blocks the path.
163
+ */
164
+ export function summarizeUpgradePath(manifests, from, to) {
165
+ const releases = [...manifests]
166
+ .filter((m) => compareVersions(m.version, from) > 0 && compareVersions(m.version, to) <= 0)
167
+ .sort((a, b) => compareVersions(a.version, b.version));
168
+ const required = new Map();
169
+ const optional = new Map();
170
+ const removed = [];
171
+ const migrations = [];
172
+ let blockedBy;
173
+ for (const release of releases) {
174
+ if (release.minUpgradeFrom && compareVersions(from, release.minUpgradeFrom) < 0 && !blockedBy) {
175
+ blockedBy = `${release.version} requires upgrading from ${release.minUpgradeFrom} or later first`;
176
+ }
177
+ for (const env of release.newRequiredEnv)
178
+ required.set(env.name, env);
179
+ for (const env of release.newOptionalEnv)
180
+ optional.set(env.name, env);
181
+ for (const env of release.removedEnv) {
182
+ required.delete(env.name);
183
+ optional.delete(env.name);
184
+ removed.push(env);
185
+ }
186
+ for (const migration of release.migrations)
187
+ migrations.push({ ...migration, version: release.version });
188
+ }
189
+ return {
190
+ from,
191
+ to,
192
+ releases,
193
+ breaking: releases.some((r) => r.breaking),
194
+ newRequiredEnv: [...required.values()],
195
+ newOptionalEnv: [...optional.values()],
196
+ removedEnv: removed,
197
+ migrations,
198
+ attention: migrations.filter((m) => m.impact !== 'fast'),
199
+ ...(blockedBy ? { blockedBy } : {}),
200
+ };
201
+ }
202
+ /** Reads the manifests embedded as `magnusRelease` in a full npm packument. */
203
+ export function manifestsFromPackument(packument) {
204
+ const versions = packument?.versions ?? {};
205
+ const manifests = [];
206
+ for (const entry of Object.values(versions)) {
207
+ const parsed = releaseManifestSchema.safeParse(entry?.magnusRelease);
208
+ if (parsed.success)
209
+ manifests.push(parsed.data);
210
+ }
211
+ return manifests;
212
+ }
213
+ // Agent self-upgrade wire contract.
214
+ export const AGENT_SELF_UPGRADE_PROTOCOL = 1;
215
+ export const agentUpgradeStateSchema = z.enum(['idle', 'staged', 'trial', 'confirmed', 'reverted', 'failed']);
216
+ export const agentUpgradeReportSchema = z.object({
217
+ state: agentUpgradeStateSchema,
218
+ from: z.string().nullable().optional(),
219
+ to: z.string().nullable().optional(),
220
+ requestId: z.string().max(128).nullable().optional(),
221
+ reason: z.string().max(500).nullable().optional(),
222
+ attempt: z.number().int().nonnegative().optional(),
223
+ updatedAt: z.string().optional(),
224
+ });
225
+ export const agentSelfUpgradeStatusSchema = z.object({
226
+ protocol: z.number().int().nonnegative(),
227
+ launcherProtocol: z.number().int().nonnegative().nullable(),
228
+ layout: z.enum(['versioned', 'legacy', 'container']),
229
+ enabled: z.boolean(),
230
+ reason: z.string().max(300).nullable().optional(),
231
+ upgrade: agentUpgradeReportSchema.nullable().optional(),
232
+ });
233
+ export const agentUpgradeRequestSchema = z.object({
234
+ version: versionString,
235
+ integrity: z.string().regex(/^sha512-[A-Za-z0-9+/]+={0,2}$/),
236
+ requestId: z.string().min(1).max(128),
237
+ packageName: z.string().default('@hivemagnus/magnus-agent'),
238
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hivemagnus/magnus-contracts",
3
- "version": "1.0.2",
3
+ "version": "1.1.0",
4
4
  "description": "Shared Zod schemas and TypeScript types for the Magnus server, UI and node agent wire contracts.",
5
5
  "keywords": [
6
6
  "magnus",