vpsnet-mcp 1.0.8 → 2.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.
@@ -0,0 +1,474 @@
1
+ import { z } from "zod";
2
+ export const customProjectIdSchema = z
3
+ .string()
4
+ .uuid()
5
+ .describe("Customer-owned recipe project UUID returned by list_application_recipes");
6
+ export const composeAdoptionIdSchema = z
7
+ .string()
8
+ .uuid()
9
+ .describe("Opaque Compose adoption UUID returned by the prepare operation");
10
+ export const composeProjectLabelSchema = z
11
+ .string()
12
+ .regex(/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/)
13
+ .describe("Exact unmanaged Compose project label returned by discovery");
14
+ export const customProjectNameSchema = z
15
+ .string()
16
+ .regex(/^[a-z][a-z0-9-]{1,40}$/)
17
+ .describe("Lowercase customer recipe name, 2-41 characters");
18
+ export const customProjectRevisionSchema = z
19
+ .number()
20
+ .int()
21
+ .positive()
22
+ .max(Number.MAX_SAFE_INTEGER)
23
+ .describe("Exact immutable customer recipe revision");
24
+ export const customProjectComposeSchema = z
25
+ .string()
26
+ .min(1)
27
+ .max(262144)
28
+ .describe("Docker Compose YAML. Mutable image tags are resolved once and replaced with immutable sha256 digests before worker validation.");
29
+ const customProjectVariableNameSchema = z
30
+ .string()
31
+ .regex(/^[A-Z][A-Z0-9_]{0,62}$/);
32
+ export const customProjectEnvironmentSchema = z
33
+ .record(customProjectVariableNameSchema, z.string().max(4096))
34
+ .refine((environment) => Object.keys(environment).length <= 64, {
35
+ message: "At most 64 plain environment variables may be submitted",
36
+ })
37
+ .default({})
38
+ .describe("Plain, non-secret environment values. Put secret values in the install tool's secrets field.");
39
+ export const customProjectSecretNamesSchema = z
40
+ .array(customProjectVariableNameSchema)
41
+ .max(64)
42
+ .refine((names) => new Set(names).size === names.length, {
43
+ message: "Secret names must be unique",
44
+ })
45
+ .default([])
46
+ .describe("Secret variable names only; definitions and exports never contain values");
47
+ export const customProjectRegistryCredentialIdsSchema = z
48
+ .array(z.string().uuid())
49
+ .max(8)
50
+ .refine((ids) => new Set(ids).size === ids.length, {
51
+ message: "Registry credential IDs must be unique",
52
+ })
53
+ .default([])
54
+ .describe("Owned registry credential UUIDs from list_application_registry_credentials");
55
+ export const customProjectSecretsSchema = z
56
+ .record(customProjectVariableNameSchema, z.string().max(4096))
57
+ .refine((secrets) => Object.keys(secrets).length <= 64, {
58
+ message: "At most 64 secret values may be submitted",
59
+ })
60
+ .default({})
61
+ .describe("Install-time secret values keyed by the exact declared names. Values are encrypted by VPSnet and omitted from tool results.");
62
+ export function customProjectDefinitionRequestBody(definition) {
63
+ return {
64
+ compose_yaml: definition.compose_yaml,
65
+ env: definition.env,
66
+ secret_names: definition.secret_names,
67
+ registry_credential_ids: definition.registry_credential_ids,
68
+ };
69
+ }
70
+ function record(value) {
71
+ return value && typeof value === "object" && !Array.isArray(value)
72
+ ? value
73
+ : {};
74
+ }
75
+ function boundedString(value, maxLength = 512) {
76
+ if (typeof value !== "string")
77
+ return null;
78
+ const text = value.trim().replace(/[\u0000-\u001f\u007f]/g, " ");
79
+ return text ? text.slice(0, maxLength) : null;
80
+ }
81
+ function boundedInteger(value, max) {
82
+ if ((typeof value !== "number" && typeof value !== "string")
83
+ || (typeof value === "string" && value.trim() === ""))
84
+ return null;
85
+ const number = Number(value);
86
+ return Number.isInteger(number) && number >= 0 && number <= max
87
+ ? number
88
+ : null;
89
+ }
90
+ function errorCodes(value) {
91
+ return Object.entries(record(value))
92
+ .filter(([, nested]) => nested === true)
93
+ .map(([key]) => key)
94
+ .filter((key) => /^[A-Za-z][A-Za-z0-9_]{0,100}$/.test(key))
95
+ .slice(0, 20);
96
+ }
97
+ function safeTimestamps(value) {
98
+ const timestamps = record(value);
99
+ return Object.fromEntries([
100
+ "requested_at",
101
+ "dispatched_at",
102
+ "completed_at",
103
+ "deadline_at",
104
+ "expires_at",
105
+ "updated_at",
106
+ ].map((key) => [key, boundedString(timestamps[key], 64)]));
107
+ }
108
+ function safeRevision(value, includeContent) {
109
+ const revision = record(value);
110
+ const number = boundedInteger(revision.revision, Number.MAX_SAFE_INTEGER);
111
+ if (number === null || number < 1)
112
+ return null;
113
+ const result = {
114
+ revision: number,
115
+ digest: boundedString(revision.digest, 64),
116
+ created_at: boundedString(revision.created_at, 64),
117
+ };
118
+ if (revision.current === true)
119
+ result.current = true;
120
+ if (includeContent) {
121
+ result.compose_yaml = typeof revision.compose_yaml === "string"
122
+ ? revision.compose_yaml.slice(0, 262144)
123
+ : "";
124
+ const environment = record(revision.env);
125
+ result.env = Object.fromEntries(Object.entries(environment)
126
+ .filter(([name, nested]) => /^[A-Z][A-Z0-9_]{0,62}$/.test(name) && typeof nested === "string")
127
+ .slice(0, 64)
128
+ .map(([name, nested]) => [name, String(nested).slice(0, 4096)]));
129
+ result.secret_names = Array.isArray(revision.secret_names)
130
+ ? revision.secret_names
131
+ .filter((name) => typeof name === "string" && /^[A-Z][A-Z0-9_]{0,62}$/.test(name))
132
+ .slice(0, 64)
133
+ : [];
134
+ result.registry_credential_ids = Array.isArray(revision.registry_credential_ids)
135
+ ? revision.registry_credential_ids
136
+ .filter((id) => typeof id === "string"
137
+ && /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/i.test(id))
138
+ .slice(0, 8)
139
+ : [];
140
+ }
141
+ return result;
142
+ }
143
+ function safeProject(value, includeContent) {
144
+ const project = record(value);
145
+ return {
146
+ id: boundedString(project.id, 64),
147
+ name: boundedString(project.name, 41),
148
+ state: boundedString(project.state, 32),
149
+ current_revision: safeRevision(project.current_revision, includeContent),
150
+ created_at: boundedString(project.created_at, 64),
151
+ updated_at: boundedString(project.updated_at, 64),
152
+ };
153
+ }
154
+ export function safeCustomProjectPayload(status, data) {
155
+ const payload = record(data);
156
+ if (status < 200 || status >= 300 || payload.success !== true) {
157
+ return {
158
+ success: false,
159
+ status,
160
+ error_codes: errorCodes(payload),
161
+ };
162
+ }
163
+ if (Array.isArray(payload.projects)) {
164
+ return {
165
+ success: true,
166
+ projects: payload.projects.slice(0, 16).map((project) => safeProject(project, false)),
167
+ count: boundedInteger(payload.count, 16),
168
+ limit: boundedInteger(payload.limit, 16),
169
+ };
170
+ }
171
+ if (Array.isArray(payload.revisions)) {
172
+ return {
173
+ success: true,
174
+ revisions: payload.revisions
175
+ .slice(0, 256)
176
+ .map((revision) => safeRevision(revision, false))
177
+ .filter((revision) => revision !== null),
178
+ };
179
+ }
180
+ return {
181
+ success: true,
182
+ project: safeProject(payload.project, true),
183
+ };
184
+ }
185
+ export function safeCustomProjectReceiptPayload(status, data) {
186
+ const payload = record(data);
187
+ const receipt = record(payload.receipt);
188
+ const project = record(receipt.project);
189
+ const recipe = record(receipt.recipe);
190
+ if (status < 200
191
+ || status >= 300
192
+ || payload.success !== true
193
+ || receipt.source !== "customer_project"
194
+ || recipe.format !== "compose-v3") {
195
+ return {
196
+ success: false,
197
+ status,
198
+ error_codes: errorCodes(payload),
199
+ };
200
+ }
201
+ const revision = {
202
+ revision: project.revision,
203
+ digest: project.digest,
204
+ created_at: null,
205
+ compose_yaml: recipe.compose_yaml,
206
+ env: recipe.env,
207
+ secret_names: recipe.secret_names,
208
+ registry_credential_ids: recipe.registry_credential_ids,
209
+ };
210
+ const content = safeRevision(revision, true);
211
+ return {
212
+ success: true,
213
+ receipt: {
214
+ schema_version: receipt.schema_version === 1 ? 1 : null,
215
+ source: "customer_project",
216
+ project: {
217
+ id: boundedString(project.id, 64),
218
+ name: boundedString(project.name, 41),
219
+ revision: boundedInteger(project.revision, Number.MAX_SAFE_INTEGER),
220
+ digest: boundedString(project.digest, 64),
221
+ },
222
+ recipe: {
223
+ format: "compose-v3",
224
+ compose_yaml: content?.compose_yaml ?? "",
225
+ env: content?.env ?? {},
226
+ secret_names: content?.secret_names ?? [],
227
+ registry_credential_ids: content?.registry_credential_ids ?? [],
228
+ },
229
+ },
230
+ };
231
+ }
232
+ export function safeCustomProjectInstallPayload(status, data, portalPath) {
233
+ const payload = record(data);
234
+ if (status < 200 || status >= 300 || payload.success !== true) {
235
+ return {
236
+ success: false,
237
+ status,
238
+ error_codes: errorCodes(payload),
239
+ };
240
+ }
241
+ const installation = record(payload.installation);
242
+ const action = record(payload.action);
243
+ return {
244
+ success: true,
245
+ replayed: payload.replayed === true,
246
+ installation: {
247
+ id: boundedString(installation.id, 64),
248
+ state: boundedString(installation.state, 32),
249
+ source: installation.source === "customer_project"
250
+ ? "customer_project"
251
+ : null,
252
+ project: boundedString(installation.project, 64),
253
+ revision: boundedInteger(installation.revision, Number.MAX_SAFE_INTEGER),
254
+ },
255
+ action: {
256
+ id: boundedString(action.id, 64),
257
+ state: boundedString(action.state, 32),
258
+ },
259
+ portal_handoff: {
260
+ access_path: portalPath,
261
+ reason: "application_status_and_secret_reveal",
262
+ },
263
+ };
264
+ }
265
+ export function safeCustomProjectValidationPayload(status, data) {
266
+ const payload = record(data);
267
+ const validation = record(payload.validation);
268
+ if (status < 200 || status >= 300 || payload.success !== true) {
269
+ return {
270
+ success: false,
271
+ status,
272
+ error_codes: errorCodes(payload),
273
+ };
274
+ }
275
+ const valid = validation.valid === true;
276
+ const resolvedCompose = valid && typeof validation.resolved_compose_yaml === "string"
277
+ ? validation.resolved_compose_yaml.slice(0, 262144)
278
+ : null;
279
+ const resolutions = valid && Array.isArray(validation.image_resolutions)
280
+ ? validation.image_resolutions
281
+ .slice(0, 64)
282
+ .map((value) => record(value))
283
+ .map((value) => ({
284
+ service: boundedString(value.service, 128),
285
+ source: boundedString(value.source, 512),
286
+ resolved: boundedString(value.resolved, 512),
287
+ }))
288
+ .filter((value) => value.service !== null
289
+ && value.source !== null
290
+ && value.resolved !== null)
291
+ : [];
292
+ return {
293
+ success: true,
294
+ replayed: payload.replayed === true,
295
+ validation: {
296
+ id: boundedString(validation.id, 64),
297
+ state: boundedString(validation.state, 32),
298
+ valid: typeof validation.valid === "boolean" ? validation.valid : null,
299
+ errors: Array.isArray(validation.errors)
300
+ ? validation.errors
301
+ .filter((error) => typeof error === "string")
302
+ .slice(0, 64)
303
+ .map((error) => error.slice(0, 512))
304
+ : [],
305
+ timestamps: safeTimestamps(validation.timestamps),
306
+ resolved_compose_yaml: resolvedCompose,
307
+ image_resolutions: resolutions,
308
+ },
309
+ };
310
+ }
311
+ export function safeContainerDiscoveryPayload(status, data) {
312
+ const payload = record(data);
313
+ const discovery = record(payload.discovery);
314
+ if (status < 200 || status >= 300 || payload.success !== true) {
315
+ return {
316
+ success: false,
317
+ status,
318
+ error_codes: errorCodes(payload),
319
+ };
320
+ }
321
+ const result = record(discovery.result);
322
+ const observed = record(result.discovery);
323
+ return {
324
+ success: true,
325
+ replayed: payload.replayed === true,
326
+ discovery: {
327
+ id: boundedString(discovery.id, 64),
328
+ state: boundedString(discovery.state, 32),
329
+ error_code: boundedString(discovery.error_code, 128),
330
+ result: Object.keys(observed).length > 0
331
+ ? {
332
+ protocol_version: boundedInteger(observed.protocol_version, 10),
333
+ collected_at: boundedString(observed.collected_at, 64),
334
+ available: observed.available === true,
335
+ error_code: boundedString(observed.error_code, 128),
336
+ truncated: observed.truncated === true,
337
+ containers: Array.isArray(observed.containers)
338
+ ? observed.containers.slice(0, 256).map((value) => {
339
+ const container = record(value);
340
+ return {
341
+ id: boundedString(container.id, 64),
342
+ name: boundedString(container.name, 128),
343
+ image: boundedString(container.image, 512),
344
+ state: boundedString(container.state, 32),
345
+ health: boundedString(container.health, 32),
346
+ ports: Array.isArray(container.ports)
347
+ ? container.ports.slice(0, 64).map((portValue) => {
348
+ const port = record(portValue);
349
+ return {
350
+ container_port: boundedInteger(port.container_port, 65535),
351
+ protocol: boundedString(port.protocol, 8),
352
+ host_ip: boundedString(port.host_ip, 64),
353
+ host_port: boundedInteger(port.host_port, 65535),
354
+ };
355
+ })
356
+ : [],
357
+ compose_project: boundedString(container.compose_project, 128),
358
+ compose_service: boundedString(container.compose_service, 128),
359
+ managed: container.managed === true,
360
+ managed_installation_id: boundedString(container.managed_installation_id, 64),
361
+ };
362
+ })
363
+ : [],
364
+ }
365
+ : null,
366
+ timestamps: safeTimestamps(discovery.timestamps),
367
+ },
368
+ };
369
+ }
370
+ function safeComposeAdoption(value) {
371
+ const adoption = record(value);
372
+ const candidate = record(adoption.candidate);
373
+ const confirmed = record(adoption.confirmed);
374
+ const services = Array.isArray(candidate.services)
375
+ ? candidate.services.slice(0, 16).map((value) => {
376
+ const service = record(value);
377
+ return {
378
+ name: boundedString(service.name, 63),
379
+ environment_names: Array.isArray(service.environment_names)
380
+ ? service.environment_names
381
+ .filter((name) => typeof name === "string"
382
+ && /^[A-Z][A-Z0-9_]{0,62}$/.test(name))
383
+ .slice(0, 64)
384
+ : [],
385
+ };
386
+ })
387
+ : [];
388
+ const errors = Array.isArray(adoption.error_codes)
389
+ ? adoption.error_codes
390
+ .filter((code) => typeof code === "string"
391
+ && /^[a-z][a-z0-9_]{0,95}$/.test(code))
392
+ .slice(0, 16)
393
+ : [];
394
+ return {
395
+ id: boundedString(adoption.id, 64),
396
+ state: boundedString(adoption.state, 32),
397
+ compose_project: boundedString(adoption.compose_project, 128),
398
+ eligible: typeof adoption.eligible === "boolean"
399
+ ? adoption.eligible
400
+ : null,
401
+ candidate: Object.keys(candidate).length > 0
402
+ ? {
403
+ digest: boundedString(candidate.digest, 64),
404
+ compose_yaml: typeof candidate.compose_yaml === "string"
405
+ ? candidate.compose_yaml.slice(0, 262144)
406
+ : "",
407
+ services,
408
+ container_count: boundedInteger(candidate.container_count, 16),
409
+ volume_count: boundedInteger(candidate.volume_count, 32),
410
+ }
411
+ : null,
412
+ error_codes: errors,
413
+ error_code: boundedString(adoption.error_code, 128),
414
+ confirmed: Object.keys(confirmed).length > 0
415
+ ? {
416
+ project_id: boundedString(confirmed.project_id, 64),
417
+ installation_id: boundedString(confirmed.installation_id, 64),
418
+ }
419
+ : null,
420
+ timestamps: safeTimestamps(adoption.timestamps),
421
+ };
422
+ }
423
+ export function safeComposeAdoptionPayload(status, data) {
424
+ const payload = record(data);
425
+ if (status < 200 || status >= 300 || payload.success !== true) {
426
+ return {
427
+ success: false,
428
+ status,
429
+ error_codes: errorCodes(payload),
430
+ };
431
+ }
432
+ return {
433
+ success: true,
434
+ replayed: payload.replayed === true,
435
+ adoption: safeComposeAdoption(payload.adoption),
436
+ };
437
+ }
438
+ export function safeComposeAdoptionConfirmationPayload(status, data, portalPath) {
439
+ const payload = record(data);
440
+ if (status < 200 || status >= 300 || payload.success !== true) {
441
+ return {
442
+ success: false,
443
+ status,
444
+ error_codes: errorCodes(payload),
445
+ };
446
+ }
447
+ const adoption = record(payload.adoption);
448
+ const project = record(payload.project);
449
+ const installation = record(payload.installation);
450
+ const action = record(payload.action);
451
+ return {
452
+ success: true,
453
+ replayed: payload.replayed === true,
454
+ adoption: {
455
+ id: boundedString(adoption.id, 64),
456
+ state: adoption.state === "confirmed" ? "confirmed" : null,
457
+ },
458
+ project: {
459
+ id: boundedString(project.id, 64),
460
+ },
461
+ installation: {
462
+ id: boundedString(installation.id, 64),
463
+ state: boundedString(installation.state, 32),
464
+ },
465
+ action: {
466
+ id: boundedString(action.id, 64),
467
+ state: boundedString(action.state, 32),
468
+ },
469
+ portal_handoff: {
470
+ access_path: portalPath,
471
+ reason: "application_status_and_secret_reveal",
472
+ },
473
+ };
474
+ }