vpsnet-mcp 1.0.9 → 2.1.1

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,810 @@
1
+ import { z } from "zod";
2
+ export const applicationLogTailLinesSchema = z
3
+ .number()
4
+ .int()
5
+ .min(1)
6
+ .max(500)
7
+ .optional()
8
+ .describe("Maximum log lines to return; defaults to 200 and cannot exceed 500");
9
+ export const applicationLogMaxBytesSchema = z
10
+ .number()
11
+ .int()
12
+ .min(1024)
13
+ .max(131072)
14
+ .optional()
15
+ .describe("Maximum log size in bytes; defaults to 65536 and cannot exceed 131072");
16
+ export const applicationLogServiceSchema = z
17
+ .string()
18
+ .regex(/^[a-z0-9][a-z0-9_-]{0,62}$/)
19
+ .optional()
20
+ .describe("Optional exact Compose service whose recent logs should be returned");
21
+ export const applicationActionSchema = z
22
+ .enum([
23
+ "reconcile",
24
+ "repair",
25
+ "restart",
26
+ "start",
27
+ "stop",
28
+ "uninstall",
29
+ "update",
30
+ ])
31
+ .describe("Supported lifecycle action. Update applies the backend-selected newer immutable release. Selective data restore is a separate revision-bound operation.");
32
+ export const applicationActionIdSchema = z
33
+ .string()
34
+ .uuid()
35
+ .describe("Exact action UUID from the current installation latest_action.id");
36
+ export const applicationExpectedVersionSchema = z
37
+ .string()
38
+ .max(96)
39
+ .regex(/^[\x21-\x7E]{1,96}$/)
40
+ .describe("Exact version copied from the current advertised update capability; this is an execution precondition, not a caller-selected target");
41
+ const externalHttpsUrlSchema = z.string().max(500).refine((value) => {
42
+ const addressMatch = /^https:\/\/[^/?#]+(\/[^?#]*)?$/i.exec(value);
43
+ if (value !== value.trim() ||
44
+ /[^\x21-\x7E]/.test(value) ||
45
+ value.includes("?") ||
46
+ value.includes("#") ||
47
+ !addressMatch) {
48
+ return false;
49
+ }
50
+ try {
51
+ const url = new URL(value);
52
+ const hostname = url.hostname.toLowerCase();
53
+ const labels = hostname.split(".");
54
+ const path = addressMatch[1] || "/";
55
+ return (url.protocol === "https:" &&
56
+ hostname.length <= 253 &&
57
+ labels.length >= 2 &&
58
+ labels.every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label)) &&
59
+ !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname) &&
60
+ !hostname.includes(":") &&
61
+ !url.username &&
62
+ !url.password &&
63
+ (url.port === "" || url.port === "443") &&
64
+ path.length <= 500 &&
65
+ (path === "/" || !path.endsWith("/")) &&
66
+ !path.includes("//") &&
67
+ !path.includes("\\") &&
68
+ !/[^A-Za-z0-9._~!$&'()*+,;=:@/-]/.test(path) &&
69
+ !path.split("/").some((segment) => segment === "." || segment === ".."));
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }, "External access must be a credential-free HTTPS URL without a query or fragment");
75
+ export const applicationSingleAccessSchema = z.discriminatedUnion("mode", [
76
+ z.object({ mode: z.literal("platform_https") }).strict(),
77
+ z.object({ mode: z.literal("private") }).strict(),
78
+ z.object({ mode: z.literal("public_http") }).strict(),
79
+ z.object({ mode: z.literal("external_https"), url: externalHttpsUrlSchema }).strict(),
80
+ z.object({
81
+ mode: z.literal("managed_https"),
82
+ zone_id: z.number().int().positive(),
83
+ subdomain: z
84
+ .string()
85
+ .max(190)
86
+ .regex(/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/),
87
+ approve_dns: z.literal(true),
88
+ }).strict(),
89
+ ]);
90
+ const applicationAccessEndpointSchema = z.object({
91
+ key: z
92
+ .string()
93
+ .max(96)
94
+ .regex(/^[a-z0-9][a-z0-9_-]{0,95}$/),
95
+ access: applicationSingleAccessSchema,
96
+ }).strict();
97
+ export const applicationAccessSchema = z.union([
98
+ applicationSingleAccessSchema,
99
+ z.object({
100
+ schema_version: z.literal(2),
101
+ endpoints: z
102
+ .array(applicationAccessEndpointSchema)
103
+ .min(1)
104
+ .max(64)
105
+ .superRefine((endpoints, context) => {
106
+ const keys = new Set();
107
+ endpoints.forEach((endpoint, index) => {
108
+ if (keys.has(endpoint.key)) {
109
+ context.addIssue({
110
+ code: z.ZodIssueCode.custom,
111
+ message: "Every endpoint key must occur exactly once",
112
+ path: [index, "key"],
113
+ });
114
+ }
115
+ keys.add(endpoint.key);
116
+ });
117
+ }),
118
+ }).strict(),
119
+ ]);
120
+ export const applicationRevisionSchema = z
121
+ .number()
122
+ .int()
123
+ .min(0)
124
+ .max(Number.MAX_SAFE_INTEGER)
125
+ .describe("Current installation revision returned by get_application_installation");
126
+ export const applicationResourceCpuPercentSchema = z
127
+ .number()
128
+ .int()
129
+ .min(1)
130
+ .max(6400)
131
+ .nullable()
132
+ .optional()
133
+ .describe("Optional display threshold as a percentage of one CPU core");
134
+ export const applicationResourceMemoryMiBSchema = z
135
+ .number()
136
+ .int()
137
+ .min(1)
138
+ .max(1048576)
139
+ .nullable()
140
+ .optional()
141
+ .describe("Optional application memory display threshold in MiB");
142
+ export const applicationResourceNetworkMiBPerMinuteSchema = z
143
+ .number()
144
+ .int()
145
+ .min(1)
146
+ .max(1048576)
147
+ .nullable()
148
+ .optional()
149
+ .describe("Optional combined RX and TX display threshold in MiB per minute");
150
+ export const applicationResourceRestartDeltaSchema = z
151
+ .number()
152
+ .int()
153
+ .min(1)
154
+ .max(1000)
155
+ .nullable()
156
+ .optional()
157
+ .describe("Optional restart display threshold between comparable samples");
158
+ export const applicationResourceEmailEnabledSchema = z
159
+ .boolean()
160
+ .optional()
161
+ .describe("Send one account email when a configured threshold is reached and one when it recovers");
162
+ export function applicationResourceThresholdRequestBody(input) {
163
+ return {
164
+ cpuPercent: input.cpuPercent ?? null,
165
+ emailEnabled: input.emailEnabled ?? false,
166
+ memoryMiB: input.memoryMiB ?? null,
167
+ networkMiBPerMinute: input.networkMiBPerMinute ?? null,
168
+ restartDelta: input.restartDelta ?? null,
169
+ };
170
+ }
171
+ export const applicationDataRestorePointIdSchema = z
172
+ .string()
173
+ .uuid()
174
+ .describe("Opaque eligible backup-point UUID returned by list_application_restore_points");
175
+ export const applicationDataRestoreIdSchema = z
176
+ .string()
177
+ .uuid()
178
+ .describe("Opaque selective restore UUID returned by restore_application_data");
179
+ export const applicationDataRestoreQuoteTokenSchema = z
180
+ .string()
181
+ .min(32)
182
+ .max(190)
183
+ .regex(/^[A-Za-z0-9_.:-]+$/)
184
+ .describe("Exact short-lived quote token returned by quote_application_data_restore");
185
+ export function applicationAccessConfigurationRequestBody(access, expectedRevision) {
186
+ return { access, expectedRevision };
187
+ }
188
+ /**
189
+ * Generated credentials are always handed to the owner in the VPSnet panel,
190
+ * never returned to this client.
191
+ *
192
+ * The alternative, `inline`, requires a `revealReceipt`: a caller-generated
193
+ * token that asserts "I have received this credential and stored it", after
194
+ * which it is never shown again. An assistant cannot truthfully make that
195
+ * assertion on a user's behalf — its context is transient and is not a place a
196
+ * customer can retrieve a password from later — so acknowledging one-time
197
+ * delivery here would destroy the customer's only copy of the credential.
198
+ * Requesting `portal` keeps the one-time guarantee intact and hands the single
199
+ * reveal to the person who can actually store it.
200
+ */
201
+ export const APPLICATION_SECRET_DELIVERY = "portal";
202
+ export function applicationInstallRequestBody(params) {
203
+ return {
204
+ application: params.application,
205
+ // Omitted on purpose when the caller did not pick one: the catalog then
206
+ // resolves the application's own published channel. Most published
207
+ // applications do not ship on a channel named "stable", so a hardcoded
208
+ // default here rejected them.
209
+ ...(params.releaseChannel !== undefined
210
+ ? { releaseChannel: params.releaseChannel }
211
+ : {}),
212
+ variables: params.variables,
213
+ secretDelivery: APPLICATION_SECRET_DELIVERY,
214
+ ...(params.acknowledgeRuntimeRestart
215
+ ? { acknowledgeRuntimeRestart: true }
216
+ : {}),
217
+ ...(params.access ? { access: params.access } : {}),
218
+ };
219
+ }
220
+ export function applicationLifecycleRequestBody(action, acknowledgeDataLoss, expectedUpdate) {
221
+ if (action === "update") {
222
+ if (!expectedUpdate?.blueprintVersion || !expectedUpdate.upstreamVersion) {
223
+ throw new Error("An exact advertised update release is required.");
224
+ }
225
+ return {
226
+ action,
227
+ expected_blueprint_version: expectedUpdate.blueprintVersion,
228
+ expected_upstream_version: expectedUpdate.upstreamVersion,
229
+ };
230
+ }
231
+ return action === "uninstall"
232
+ ? { action, acknowledgeDataLoss: acknowledgeDataLoss === true }
233
+ : { action };
234
+ }
235
+ export function applicationDataRestoreRequestBody(params) {
236
+ return {
237
+ orderNo: params.orderNo,
238
+ backupPointId: params.backupPointId,
239
+ expectedRevision: params.expectedRevision,
240
+ acknowledgeDataReplacement: true,
241
+ acknowledgeRestoreCharge: true,
242
+ quoteToken: params.quoteToken,
243
+ };
244
+ }
245
+ export function applicationDataRestoreQuoteRequestBody(params) {
246
+ return {
247
+ orderNo: params.orderNo,
248
+ backupPointId: params.backupPointId,
249
+ expectedRevision: params.expectedRevision,
250
+ };
251
+ }
252
+ export function applicationActionCancellationRequestBody() {
253
+ return {};
254
+ }
255
+ function record(value) {
256
+ return value && typeof value === "object" && !Array.isArray(value)
257
+ ? value
258
+ : {};
259
+ }
260
+ function boundedString(value, maxLength = 512) {
261
+ if (typeof value !== "string")
262
+ return null;
263
+ const text = value.trim().replace(/[\u0000-\u001f\u007f]/g, " ");
264
+ return text ? text.slice(0, maxLength) : null;
265
+ }
266
+ function boundedInteger(value, max) {
267
+ if ((typeof value !== "number" && typeof value !== "string")
268
+ || (typeof value === "string" && value.trim() === ""))
269
+ return null;
270
+ const number = Number(value);
271
+ return Number.isInteger(number) && number >= 0 && number <= max
272
+ ? number
273
+ : null;
274
+ }
275
+ function timestamp(value) {
276
+ const text = boundedString(value, 64);
277
+ return text && Number.isFinite(Date.parse(text)) ? text : null;
278
+ }
279
+ function uuidV4(value) {
280
+ const id = boundedString(value, 36);
281
+ return id
282
+ && /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/.test(id)
283
+ ? id
284
+ : null;
285
+ }
286
+ const APPLICATION_DATA_RESTORE_STATES = new Set([
287
+ "awaiting_payment",
288
+ "queued",
289
+ "running",
290
+ "awaiting_reply",
291
+ "needs_attention",
292
+ "succeeded",
293
+ "failed",
294
+ "cancelled",
295
+ ]);
296
+ const APPLICATION_DATA_RESTORE_STAGES = new Set([
297
+ "awaiting_payment",
298
+ "queued",
299
+ "verifying",
300
+ "restoring",
301
+ "retrying",
302
+ "needs_attention",
303
+ "completed",
304
+ "failed",
305
+ "cancelled",
306
+ ]);
307
+ function safeApplicationDataRestore(value) {
308
+ const restore = record(value);
309
+ const id = uuidV4(restore.id);
310
+ const installationId = uuidV4(restore.installationId);
311
+ const backupPointId = uuidV4(restore.backupPointId);
312
+ const state = boundedString(restore.state, 32);
313
+ const progress = record(restore.progress);
314
+ const percent = boundedInteger(progress.percent, 100);
315
+ const stage = boundedString(progress.stage, 32);
316
+ const createdAt = timestamp(restore.createdAt);
317
+ if (!id
318
+ || !installationId
319
+ || !backupPointId
320
+ || !state
321
+ || !APPLICATION_DATA_RESTORE_STATES.has(state)
322
+ || percent === null
323
+ || !stage
324
+ || !APPLICATION_DATA_RESTORE_STAGES.has(stage)
325
+ || !createdAt) {
326
+ return null;
327
+ }
328
+ const restoreMode = boundedString(restore.restoreMode, 16);
329
+ const restoredBytes = restore.restoredBytes === null
330
+ ? null
331
+ : boundedInteger(restore.restoredBytes, Number.MAX_SAFE_INTEGER);
332
+ const restoredItems = restore.restoredItems === null
333
+ ? null
334
+ : boundedInteger(restore.restoredItems, 100000);
335
+ return {
336
+ id,
337
+ installation_id: installationId,
338
+ backup_point_id: backupPointId,
339
+ state,
340
+ progress: { percent, stage },
341
+ restore_mode: restoreMode === "direct" || restoreMode === "staged"
342
+ ? restoreMode
343
+ : null,
344
+ restored_bytes: restoredBytes,
345
+ restored_items: restoredItems,
346
+ health_status: boundedString(restore.healthStatus, 64),
347
+ error_code: boundedString(restore.errorCode, 96),
348
+ restore_request_id: boundedInteger(restore.restoreRequestId, Number.MAX_SAFE_INTEGER),
349
+ price_ex_vat: boundedMoney(restore.priceExVat),
350
+ total_charged: boundedMoney(restore.totalCharged),
351
+ created_at: createdAt,
352
+ completed_at: timestamp(restore.completedAt),
353
+ };
354
+ }
355
+ function boundedMoney(value) {
356
+ const amount = numericValue(value);
357
+ return amount !== null && amount >= 0 && amount <= 1_000_000
358
+ ? Math.round(amount * 100) / 100
359
+ : null;
360
+ }
361
+ function boundedBalance(value) {
362
+ const amount = numericValue(value);
363
+ return amount !== null
364
+ && amount >= -1_000_000
365
+ && amount <= 1_000_000
366
+ ? Math.round(amount * 100) / 100
367
+ : null;
368
+ }
369
+ function numericValue(value) {
370
+ if ((typeof value !== "number" && typeof value !== "string")
371
+ || (typeof value === "string" && value.trim() === ""))
372
+ return null;
373
+ const number = Number(value);
374
+ return Number.isFinite(number) ? number : null;
375
+ }
376
+ function safeApplicationDataRestoreQuote(value) {
377
+ const quote = record(value);
378
+ const priceExVat = boundedMoney(quote.price_ex_vat);
379
+ const vatRate = numericValue(quote.vat_rate);
380
+ const totalCharged = boundedMoney(quote.total_charged);
381
+ const balance = boundedBalance(quote.balance);
382
+ if (priceExVat === null
383
+ || vatRate === null
384
+ || vatRate < 0
385
+ || vatRate > 100
386
+ || totalCharged === null
387
+ || balance === null
388
+ || typeof quote.balance_sufficient !== "boolean") {
389
+ return null;
390
+ }
391
+ return {
392
+ price_ex_vat: priceExVat,
393
+ vat_rate: Math.round(vatRate * 100) / 100,
394
+ total_charged: totalCharged,
395
+ balance,
396
+ balance_sufficient: quote.balance_sufficient,
397
+ };
398
+ }
399
+ export function safeApplicationDataRestorePointsPayload(status, data) {
400
+ const payload = record(data);
401
+ if (status < 200 || status >= 300 || payload.success !== true) {
402
+ return {
403
+ success: false,
404
+ status,
405
+ error_codes: errorCodes(payload),
406
+ };
407
+ }
408
+ const points = Array.isArray(payload.points)
409
+ ? payload.points.slice(0, 100).flatMap((value) => {
410
+ const point = record(value);
411
+ const id = uuidV4(point.id);
412
+ const capturedAt = timestamp(point.capturedAt);
413
+ const expiresAt = timestamp(point.expiresAt);
414
+ const sizeBytes = boundedInteger(point.sizeBytes, Number.MAX_SAFE_INTEGER);
415
+ return id
416
+ && capturedAt
417
+ && expiresAt
418
+ && sizeBytes !== null
419
+ && point.consistency === "application"
420
+ ? [{
421
+ id,
422
+ captured_at: capturedAt,
423
+ expires_at: expiresAt,
424
+ size_bytes: sizeBytes,
425
+ consistency: "application",
426
+ }]
427
+ : [];
428
+ })
429
+ : [];
430
+ const quote = safeApplicationDataRestoreQuote(payload.quote);
431
+ if (quote === null) {
432
+ return {
433
+ success: false,
434
+ status,
435
+ error_codes: ["applicationDataRestoreQuoteInvalid"],
436
+ };
437
+ }
438
+ return {
439
+ success: true,
440
+ points,
441
+ quote,
442
+ active_restore: payload.activeRestore === null
443
+ ? null
444
+ : safeApplicationDataRestore(payload.activeRestore),
445
+ };
446
+ }
447
+ export function safeApplicationDataRestoreQuotePayload(status, data) {
448
+ const payload = record(data);
449
+ const quote = safeApplicationDataRestoreQuote(payload.quote);
450
+ const quoteToken = boundedString(payload.quoteToken, 190);
451
+ const quoteExpiresAt = timestamp(payload.quoteExpiresAt);
452
+ if (status < 200
453
+ || status >= 300
454
+ || payload.success !== true
455
+ || quote === null
456
+ || !quoteToken
457
+ || !/^[A-Za-z0-9_.:-]{32,190}$/.test(quoteToken)
458
+ || !quoteExpiresAt) {
459
+ return {
460
+ success: false,
461
+ status,
462
+ error_codes: errorCodes(payload),
463
+ };
464
+ }
465
+ return {
466
+ success: true,
467
+ quote,
468
+ quote_token: quoteToken,
469
+ quote_expires_at: quoteExpiresAt,
470
+ };
471
+ }
472
+ export function safeApplicationDataRestorePayload(status, data) {
473
+ const payload = record(data);
474
+ const restore = safeApplicationDataRestore(payload.restore);
475
+ if (status < 200
476
+ || status >= 300
477
+ || payload.success !== true
478
+ || restore === null) {
479
+ return {
480
+ success: false,
481
+ status,
482
+ error_codes: errorCodes(payload),
483
+ };
484
+ }
485
+ return {
486
+ success: true,
487
+ ...(typeof payload.replayed === "boolean"
488
+ ? { replayed: payload.replayed }
489
+ : {}),
490
+ restore,
491
+ };
492
+ }
493
+ function composeServiceName(value) {
494
+ const name = boundedString(value, 63);
495
+ return name && /^[a-z0-9][a-z0-9_-]{0,62}$/.test(name) ? name : null;
496
+ }
497
+ function safeRegistryHost(value) {
498
+ const registry = boundedString(value, 253);
499
+ if (!registry || registry !== registry.toLowerCase()) {
500
+ return null;
501
+ }
502
+ const match = /^([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+)(?::([1-9][0-9]{0,4}))?$/.exec(registry);
503
+ if (!match) {
504
+ return null;
505
+ }
506
+ const hostname = match[1];
507
+ const port = match[2];
508
+ if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(hostname)
509
+ || [".localhost", ".local", ".invalid", ".test", ".example"].some((suffix) => hostname.endsWith(suffix))
510
+ || (port !== undefined && Number(port) > 65535)) {
511
+ return null;
512
+ }
513
+ return registry;
514
+ }
515
+ const PRIVATE_KEY_BLOCK = /-----BEGIN ([A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?)-----[\s\S]*?-----END \1-----/gi;
516
+ const TRUNCATED_PRIVATE_KEY_PREFIX = /^[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/i;
517
+ const TRUNCATED_PRIVATE_KEY_SUFFIX = /-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*$/i;
518
+ const BEARER_TOKEN = /\b(Bearer\s+)[A-Za-z0-9._~+\/-]+=*/gi;
519
+ const BASIC_TOKEN = /\b(Basic\s+)[A-Za-z0-9+/]+={0,2}/gi;
520
+ const JWT_TOKEN = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g;
521
+ const URL_CREDENTIALS = /([a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:)[^\s/@]+(@)/gi;
522
+ const KNOWN_TOKEN = /\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16})\b/g;
523
+ const MANAGED_INFERENCE_TOKEN = /\bvpsnet_mi_\d{1,10}_[A-Za-z0-9]{8,64}_[A-Za-z0-9_-]{43,128}\b/g;
524
+ const SECRET_ASSIGNMENT = /((?:["']?(?:password|passwd|pwd|secret|token|api[_-]?key|authorization|cookie|credential|private[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|session[_-]?token)["']?)\s*(?:=|:)\s*)(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|[^\s,;}\]]+)/gi;
525
+ function redactApplicationLogText(value) {
526
+ let text = typeof value === "string" ? value : String(value ?? "");
527
+ text = text.replace(PRIVATE_KEY_BLOCK, "[redacted]");
528
+ text = text.replace(TRUNCATED_PRIVATE_KEY_PREFIX, "[redacted]");
529
+ text = text.replace(TRUNCATED_PRIVATE_KEY_SUFFIX, "[redacted]");
530
+ text = text.replace(BEARER_TOKEN, "$1[redacted]");
531
+ text = text.replace(BASIC_TOKEN, "$1[redacted]");
532
+ text = text.replace(JWT_TOKEN, "[redacted]");
533
+ text = text.replace(URL_CREDENTIALS, "$1[redacted]$2");
534
+ text = text.replace(KNOWN_TOKEN, "[redacted]");
535
+ text = text.replace(MANAGED_INFERENCE_TOKEN, "[redacted]");
536
+ return text.replace(SECRET_ASSIGNMENT, "$1[redacted]");
537
+ }
538
+ function safePortalPath(value) {
539
+ return /^\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]{1,511}$/.test(value)
540
+ ? value
541
+ : null;
542
+ }
543
+ function errorCodes(value) {
544
+ return Object.entries(record(value))
545
+ .filter(([, nested]) => nested === true)
546
+ .map(([key]) => key)
547
+ .filter((key) => /^[A-Za-z][A-Za-z0-9_]{0,100}$/.test(key))
548
+ .slice(0, 20);
549
+ }
550
+ /**
551
+ * Project the backend's machine-readable remediation block so a failure says
552
+ * what to send instead of only which code was returned. It is a whitelist: only
553
+ * field names, accepted values and translation keys survive, never a credential
554
+ * value, a receipt or a free-form server string.
555
+ */
556
+ function safeApplicationErrorDetail(value) {
557
+ const detail = record(value);
558
+ if (Object.keys(detail).length === 0) {
559
+ return null;
560
+ }
561
+ const resolution = Array.isArray(detail.resolution)
562
+ ? detail.resolution.slice(0, 8).map((entry) => {
563
+ const step = record(entry);
564
+ // Two resolution shapes exist: the variable-form ({field,value,
565
+ // requires}, e.g. secret-delivery) and the action-form ({action,
566
+ // installation,description_key}, e.g. the already-installed 409's
567
+ // uninstall/repair steps and the port-conflict holder). Carry both,
568
+ // dropping only keys that are absent, so an agent actually receives
569
+ // the actionable step instead of a blanked {field:null,value:null}.
570
+ const mapped = {};
571
+ const field = boundedString(step.field, 64);
572
+ if (field !== null) {
573
+ mapped.field = field;
574
+ mapped.value = typeof step.value === "boolean" || typeof step.value === "number"
575
+ ? step.value
576
+ : boundedString(step.value, 64);
577
+ mapped.requires = Array.isArray(step.requires)
578
+ ? step.requires
579
+ .slice(0, 8)
580
+ .map((name) => boundedString(name, 64))
581
+ .filter((name) => name !== null)
582
+ : [];
583
+ }
584
+ const action = boundedString(step.action, 64);
585
+ if (action !== null) {
586
+ mapped.action = action;
587
+ }
588
+ const installation = boundedString(step.installation, 64);
589
+ if (installation !== null) {
590
+ mapped.installation = installation;
591
+ }
592
+ const descriptionKey = boundedString(step.description_key, 190);
593
+ if (descriptionKey !== null) {
594
+ mapped.description_key = descriptionKey;
595
+ }
596
+ return mapped;
597
+ })
598
+ : [];
599
+ const credentials = Array.isArray(detail.credentials)
600
+ ? detail.credentials
601
+ .slice(0, 32)
602
+ .map((name) => boundedString(name, 96))
603
+ .filter((name) => name !== null)
604
+ : [];
605
+ return {
606
+ code: boundedString(detail.code, 100),
607
+ message_key: boundedString(detail.message_key, 190),
608
+ reason: boundedString(detail.reason, 96),
609
+ credentials,
610
+ resolution,
611
+ };
612
+ }
613
+ export function safeApplicationMutationPayload(status, data, portalPath) {
614
+ const payload = record(data);
615
+ const codes = errorCodes(payload);
616
+ const reveal = record(payload.secret_reveal);
617
+ const existingHandoff = record(payload.portal_handoff);
618
+ const delivery = record(payload.secret_delivery);
619
+ const detail = safeApplicationErrorDetail(payload.error_detail);
620
+ const revealRequired = Object.keys(reveal).length > 0
621
+ || existingHandoff.required === true
622
+ || delivery.pending_reveal === true
623
+ || detail?.reason === "generated_credentials"
624
+ || codes.some((code) => /(?:secret.*(?:reveal|delivery)|generated.*credential)/i.test(code));
625
+ const accessPath = safePortalPath(portalPath);
626
+ const portalHandoff = revealRequired && accessPath
627
+ ? {
628
+ required: true,
629
+ reason: "generated_credentials",
630
+ access_path: accessPath,
631
+ }
632
+ : null;
633
+ if (status < 200 || status >= 300 || Object.keys(payload).length === 0) {
634
+ return {
635
+ success: false,
636
+ status,
637
+ error_codes: codes,
638
+ error_detail: detail,
639
+ portal_handoff: portalHandoff,
640
+ };
641
+ }
642
+ const installation = record(payload.installation);
643
+ const action = record(payload.action);
644
+ return {
645
+ success: payload.success === true,
646
+ replayed: payload.replayed === true,
647
+ installation: {
648
+ id: boundedString(installation.id),
649
+ state: boundedString(installation.state),
650
+ application: boundedString(installation.application),
651
+ release_channel: boundedString(installation.release_channel),
652
+ upstream_version: boundedString(installation.upstream_version),
653
+ },
654
+ action: {
655
+ id: boundedString(action.id),
656
+ type: boundedString(action.type),
657
+ state: boundedString(action.state),
658
+ },
659
+ secret_delivery: {
660
+ mode: boundedString(delivery.mode, 32),
661
+ pending_reveal: delivery.pending_reveal === true,
662
+ },
663
+ portal_handoff: portalHandoff,
664
+ };
665
+ }
666
+ export function safeApplicationRegistryCredentialPayload(status, data) {
667
+ const payload = record(data);
668
+ if (status < 200 || status >= 300 || payload.success !== true) {
669
+ return {
670
+ success: false,
671
+ status,
672
+ error_codes: errorCodes(payload),
673
+ };
674
+ }
675
+ const credentials = Array.isArray(payload.credentials)
676
+ ? payload.credentials.slice(0, 8).map((value) => {
677
+ const credential = record(value);
678
+ const registry = safeRegistryHost(credential.registry);
679
+ return {
680
+ id: boundedString(credential.id, 64),
681
+ registry,
682
+ username: boundedString(credential.username, 128),
683
+ created_at: boundedString(credential.created_at, 64),
684
+ updated_at: boundedString(credential.updated_at, 64),
685
+ rotated_at: boundedString(credential.rotated_at, 64),
686
+ token_present: credential.token_present === true,
687
+ };
688
+ })
689
+ : [];
690
+ return {
691
+ success: true,
692
+ credentials,
693
+ };
694
+ }
695
+ export function applicationUpdateCandidateMatches(data, expected) {
696
+ const installation = record(record(data).installation);
697
+ const actions = Array.isArray(installation.available_actions)
698
+ ? installation.available_actions
699
+ : [];
700
+ return actions.some((value) => {
701
+ const action = record(value);
702
+ const release = record(action.release);
703
+ return action.type === "update"
704
+ && release.blueprint_version === expected.blueprintVersion
705
+ && release.upstream_version === expected.upstreamVersion;
706
+ });
707
+ }
708
+ export function applicationActionCancellationIsAdvertised(data, actionId) {
709
+ const installation = record(record(data).installation);
710
+ const latestAction = record(installation.latest_action);
711
+ return latestAction.id === actionId && latestAction.cancellable === true;
712
+ }
713
+ function safeHealthResult(value) {
714
+ const source = record(value);
715
+ if (Object.keys(source).length === 0)
716
+ return null;
717
+ return {
718
+ protocol_version: boundedInteger(source.protocol_version, 10),
719
+ installation_id: boundedString(source.installation_id, 128),
720
+ status: boundedString(source.status, 32),
721
+ checked_at: timestamp(source.checked_at),
722
+ unexpected_containers: boundedInteger(source.unexpected_containers, 256),
723
+ services: Array.isArray(source.services)
724
+ ? source.services.slice(0, 16).map((value) => {
725
+ const service = record(value);
726
+ return {
727
+ name: boundedString(service.name, 63),
728
+ status: boundedString(service.status, 32),
729
+ health: boundedString(service.health, 32),
730
+ containers: boundedInteger(service.containers, 256),
731
+ running: boundedInteger(service.running, 256),
732
+ healthy: boundedInteger(service.healthy, 256),
733
+ unhealthy: boundedInteger(service.unhealthy, 256),
734
+ starting: boundedInteger(service.starting, 256),
735
+ restarting: boundedInteger(service.restarting, 256),
736
+ exited: boundedInteger(service.exited, 256),
737
+ };
738
+ })
739
+ : [],
740
+ };
741
+ }
742
+ function safeLogsResult(value) {
743
+ const source = record(value);
744
+ if (Object.keys(source).length === 0)
745
+ return null;
746
+ const raw = typeof source.content === "string" ? source.content.slice(0, 131072) : "";
747
+ const content = redactApplicationLogText(raw);
748
+ return {
749
+ protocol_version: boundedInteger(source.protocol_version, 10),
750
+ installation_id: boundedString(source.installation_id, 128),
751
+ collected_at: timestamp(source.collected_at),
752
+ tail_lines: boundedInteger(source.tail_lines, 500),
753
+ max_bytes: boundedInteger(source.max_bytes, 131072),
754
+ line_count: boundedInteger(source.line_count, 1_000_000),
755
+ byte_count: boundedInteger(source.byte_count, 131072),
756
+ truncated: source.truncated === true,
757
+ content: content.slice(0, 24000),
758
+ content_truncated_for_model: content.length > 24000,
759
+ };
760
+ }
761
+ function safeInspectionResult(value, expectedKind) {
762
+ const source = record(value);
763
+ if (Object.keys(source).length === 0)
764
+ return null;
765
+ return expectedKind === "health"
766
+ ? {
767
+ health: safeHealthResult(source.health),
768
+ worker_node: boundedString(source.worker_node, 128),
769
+ worker_release: boundedString(source.worker_release, 128),
770
+ }
771
+ : {
772
+ logs: safeLogsResult(source.logs),
773
+ worker_node: boundedString(source.worker_node, 128),
774
+ worker_release: boundedString(source.worker_release, 128),
775
+ };
776
+ }
777
+ export function safeApplicationInspectionPayload(status, data, expectedKind) {
778
+ const payload = record(data);
779
+ const inspection = record(payload.inspection);
780
+ const result = record(inspection.result);
781
+ const parameters = record(inspection.parameters);
782
+ const timestamps = record(inspection.timestamps);
783
+ return {
784
+ success: status >= 200 && status < 300 && payload.success === true,
785
+ status,
786
+ replayed: payload.replayed === true,
787
+ error_codes: errorCodes(payload),
788
+ inspection: {
789
+ id: boundedString(inspection.id, 128),
790
+ installation_id: boundedString(inspection.installation_id, 128),
791
+ kind: inspection.kind === expectedKind ? expectedKind : null,
792
+ state: boundedString(inspection.state, 32),
793
+ parameters: {
794
+ tail_lines: boundedInteger(parameters.tail_lines, 500),
795
+ max_bytes: boundedInteger(parameters.max_bytes, 131072),
796
+ service: composeServiceName(parameters.service),
797
+ },
798
+ result: safeInspectionResult(result, expectedKind),
799
+ error_code: boundedString(inspection.error_code, 128),
800
+ timestamps: {
801
+ requested_at: timestamp(timestamps.requested_at),
802
+ dispatched_at: timestamp(timestamps.dispatched_at),
803
+ completed_at: timestamp(timestamps.completed_at),
804
+ deadline_at: timestamp(timestamps.deadline_at),
805
+ expires_at: timestamp(timestamps.expires_at),
806
+ updated_at: timestamp(timestamps.updated_at),
807
+ },
808
+ },
809
+ };
810
+ }