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.
- package/README.md +493 -20
- package/build/api-configuration.js +59 -0
- package/build/api-path.js +26 -0
- package/build/api.js +184 -10
- package/build/application-contract.js +810 -0
- package/build/auth-failure.js +141 -0
- package/build/certificate-contract.js +1621 -0
- package/build/custom-project-contract.js +474 -0
- package/build/file-restore-contract.js +247 -0
- package/build/index.js +3682 -175
- package/build/service-rescue-contract.js +97 -0
- package/build/temp-vm-contract.js +286 -0
- package/build/tool-result.js +49 -0
- package/package.json +23 -6
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Free backup file browsing for a service restore point.
|
|
4
|
+
*
|
|
5
|
+
* Browsing and folder search are read-only and never charge the account. The
|
|
6
|
+
* paid selected-path restore flow (quote -> confirm) is intentionally not part
|
|
7
|
+
* of this module; see README for what shipping it would require.
|
|
8
|
+
*/
|
|
9
|
+
/** Largest server-selected directory page. There is no client-side limit parameter. */
|
|
10
|
+
export const FILE_BROWSE_MAX_PAGE_SIZE = 1000;
|
|
11
|
+
/** Older nodes select this page size and older cached API results omit pageSize. */
|
|
12
|
+
export const FILE_BROWSE_LEGACY_PAGE_SIZE = 200;
|
|
13
|
+
/** ServiceFileRestoreContract::MAX_PAGE_OFFSET */
|
|
14
|
+
export const FILE_BROWSE_MAX_OFFSET = 1_000_000;
|
|
15
|
+
/** ServiceFileRestoreContract::MAX_FILTER_LENGTH */
|
|
16
|
+
export const FILE_BROWSE_MAX_FILTER_LENGTH = 128;
|
|
17
|
+
export const fileBrowsePointIdSchema = z
|
|
18
|
+
.number()
|
|
19
|
+
.int()
|
|
20
|
+
.positive()
|
|
21
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
22
|
+
.describe("Backup point ID returned by list_restore_file_points");
|
|
23
|
+
export const fileBrowseIdSchema = z
|
|
24
|
+
.string()
|
|
25
|
+
.uuid()
|
|
26
|
+
.describe("Browse UUID returned by browse_restore_files");
|
|
27
|
+
export const fileBrowseDirectoryEntryIdSchema = z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^[a-f0-9]{64}$/)
|
|
30
|
+
.describe("Opaque id of a type=directory entry from the source browse result. Filesystem paths are never accepted.");
|
|
31
|
+
export const fileBrowseOffsetSchema = z
|
|
32
|
+
.number()
|
|
33
|
+
.int()
|
|
34
|
+
.min(0)
|
|
35
|
+
.max(FILE_BROWSE_MAX_OFFSET)
|
|
36
|
+
.describe(`Page offset within the same directory listing; copy nextOffset from the previous result. The server selects a page size of ${FILE_BROWSE_LEGACY_PAGE_SIZE} or ${FILE_BROWSE_MAX_PAGE_SIZE} entries.`);
|
|
37
|
+
export const fileBrowseFilterSchema = z
|
|
38
|
+
.string()
|
|
39
|
+
.min(1)
|
|
40
|
+
.max(FILE_BROWSE_MAX_FILTER_LENGTH)
|
|
41
|
+
.refine((value) => value.trim() !== "" &&
|
|
42
|
+
value.includes("/") === false &&
|
|
43
|
+
/[\u0000-\u001f\u007f]/.test(value) === false, "Filter must be 1-128 characters with no path separator and no control characters")
|
|
44
|
+
.describe("Case-insensitive substring matched against entry NAMES in the browsed directory only. It never matches full paths and never searches subdirectories. Requires node search capability; check searchAvailable from list_restore_file_points first.");
|
|
45
|
+
const fileBrowseEntryTypeSchema = z.enum([
|
|
46
|
+
"file",
|
|
47
|
+
"directory",
|
|
48
|
+
"symlink",
|
|
49
|
+
"unsupported",
|
|
50
|
+
]);
|
|
51
|
+
const fileBrowseStateSchema = z.enum([
|
|
52
|
+
"queued",
|
|
53
|
+
"running",
|
|
54
|
+
"awaiting_reply",
|
|
55
|
+
"succeeded",
|
|
56
|
+
"failed",
|
|
57
|
+
"expired",
|
|
58
|
+
"needs_attention",
|
|
59
|
+
]);
|
|
60
|
+
/** Browse states that are still in flight and should be polled again. */
|
|
61
|
+
export const FILE_BROWSE_PENDING_STATES = [
|
|
62
|
+
"queued",
|
|
63
|
+
"running",
|
|
64
|
+
"awaiting_reply",
|
|
65
|
+
];
|
|
66
|
+
function record(value) {
|
|
67
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
68
|
+
? value
|
|
69
|
+
: {};
|
|
70
|
+
}
|
|
71
|
+
function boundedString(value, maxLength = 512) {
|
|
72
|
+
if (typeof value !== "string")
|
|
73
|
+
return null;
|
|
74
|
+
const text = value.trim().replace(/[\u0000-\u001f\u007f]/g, " ");
|
|
75
|
+
return text ? text.slice(0, maxLength) : null;
|
|
76
|
+
}
|
|
77
|
+
function boundedInteger(value, max) {
|
|
78
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= max
|
|
79
|
+
? value
|
|
80
|
+
: null;
|
|
81
|
+
}
|
|
82
|
+
function timestamp(value) {
|
|
83
|
+
const text = boundedString(value, 64);
|
|
84
|
+
return text && Number.isFinite(Date.parse(text)) ? text : null;
|
|
85
|
+
}
|
|
86
|
+
function errorCodes(value) {
|
|
87
|
+
return Object.entries(record(value))
|
|
88
|
+
.filter(([, nested]) => nested === true)
|
|
89
|
+
.map(([key]) => key)
|
|
90
|
+
.filter((key) => /^[A-Za-z][A-Za-z0-9_]{0,100}$/.test(key))
|
|
91
|
+
.slice(0, 20);
|
|
92
|
+
}
|
|
93
|
+
function entryType(value) {
|
|
94
|
+
const parsed = fileBrowseEntryTypeSchema.safeParse(value);
|
|
95
|
+
return parsed.success ? parsed.data : null;
|
|
96
|
+
}
|
|
97
|
+
function browseState(value) {
|
|
98
|
+
const parsed = fileBrowseStateSchema.safeParse(value);
|
|
99
|
+
return parsed.success ? parsed.data : null;
|
|
100
|
+
}
|
|
101
|
+
function safeBrowseResult(value) {
|
|
102
|
+
const source = record(value);
|
|
103
|
+
if (Object.keys(source).length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
const directory = record(source.directory);
|
|
106
|
+
const entries = Array.isArray(source.entries) ? source.entries : [];
|
|
107
|
+
const pageSize = source.pageSize === FILE_BROWSE_MAX_PAGE_SIZE
|
|
108
|
+
? FILE_BROWSE_MAX_PAGE_SIZE
|
|
109
|
+
: FILE_BROWSE_LEGACY_PAGE_SIZE;
|
|
110
|
+
return {
|
|
111
|
+
directory: { path: boundedString(directory.path, 4096) },
|
|
112
|
+
entries: entries.slice(0, pageSize).map((value) => {
|
|
113
|
+
const entry = record(value);
|
|
114
|
+
return {
|
|
115
|
+
id: boundedString(entry.id, 64),
|
|
116
|
+
name: boundedString(entry.name, 255),
|
|
117
|
+
type: entryType(entry.type),
|
|
118
|
+
size_bytes: boundedInteger(entry.size_bytes, Number.MAX_SAFE_INTEGER),
|
|
119
|
+
modified_at: timestamp(entry.modified_at),
|
|
120
|
+
};
|
|
121
|
+
}),
|
|
122
|
+
offset: boundedInteger(source.offset, FILE_BROWSE_MAX_OFFSET),
|
|
123
|
+
pageSize,
|
|
124
|
+
nextOffset: boundedInteger(source.nextOffset, FILE_BROWSE_MAX_OFFSET),
|
|
125
|
+
truncated: source.truncated === true,
|
|
126
|
+
scanned: boundedInteger(source.scanned, Number.MAX_SAFE_INTEGER),
|
|
127
|
+
// Entries whose names the backup contract cannot represent (for example
|
|
128
|
+
// filenames that are not valid UTF-8) are skipped, not failed. Surfacing
|
|
129
|
+
// the count keeps a short page honest.
|
|
130
|
+
skipped: boundedInteger(source.skipped, Number.MAX_SAFE_INTEGER),
|
|
131
|
+
// 'complete' means the directory was read end to end, so this listing --
|
|
132
|
+
// including an empty search -- is definitive. 'partial' means the scan
|
|
133
|
+
// stopped before the directory did: the listing is a lower bound, and
|
|
134
|
+
// nothing can be concluded about what is absent from it. Never tell a
|
|
135
|
+
// user a file does not exist in the backup off a partial listing.
|
|
136
|
+
listingStatus: source.listingStatus === "complete" || source.listingStatus === "partial"
|
|
137
|
+
? source.listingStatus
|
|
138
|
+
: null,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function safeBrowse(value) {
|
|
142
|
+
const source = record(value);
|
|
143
|
+
if (Object.keys(source).length === 0)
|
|
144
|
+
return null;
|
|
145
|
+
const state = browseState(source.state);
|
|
146
|
+
const result = state === "succeeded" ? safeBrowseResult(source.result) : null;
|
|
147
|
+
return {
|
|
148
|
+
id: boundedString(source.id, 36),
|
|
149
|
+
backupPointId: boundedInteger(source.backupPointId, Number.MAX_SAFE_INTEGER),
|
|
150
|
+
state,
|
|
151
|
+
pending: state !== null && FILE_BROWSE_PENDING_STATES.includes(state),
|
|
152
|
+
result,
|
|
153
|
+
errorCode: boundedString(source.errorCode, 96),
|
|
154
|
+
createdAt: timestamp(source.createdAt),
|
|
155
|
+
completedAt: timestamp(source.completedAt),
|
|
156
|
+
expiresAt: timestamp(source.expiresAt),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
export function safeFileBrowsePayload(status, data) {
|
|
160
|
+
const payload = record(data);
|
|
161
|
+
const browse = safeBrowse(payload.browse);
|
|
162
|
+
const success = status >= 200 && status < 300 && payload.success === true;
|
|
163
|
+
return {
|
|
164
|
+
success,
|
|
165
|
+
status,
|
|
166
|
+
replayed: payload.replayed === true,
|
|
167
|
+
error_codes: errorCodes(payload),
|
|
168
|
+
browse,
|
|
169
|
+
// A queued browse carries no listing yet. Poll get_restore_file_browse
|
|
170
|
+
// until state is succeeded; never present a pending browse as a result.
|
|
171
|
+
next_step: browse && browse.pending === true
|
|
172
|
+
? "Poll get_restore_file_browse with this browse id until state is succeeded."
|
|
173
|
+
: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
export function safeFileBrowsePointsPayload(status, data) {
|
|
177
|
+
const payload = record(data);
|
|
178
|
+
const rows = Array.isArray(payload.points) ? payload.points : [];
|
|
179
|
+
const success = status >= 200 && status < 300 && payload.success === true;
|
|
180
|
+
return {
|
|
181
|
+
success,
|
|
182
|
+
status,
|
|
183
|
+
error_codes: errorCodes(payload),
|
|
184
|
+
available: payload.available === true,
|
|
185
|
+
// Fails closed to false whenever the node's worker does not report the
|
|
186
|
+
// folder-search capability. Never send a filter when this is false.
|
|
187
|
+
searchAvailable: payload.searchAvailable === true,
|
|
188
|
+
points: rows.slice(0, 200).map((value) => {
|
|
189
|
+
const point = record(value);
|
|
190
|
+
return {
|
|
191
|
+
id: boundedInteger(point.id, Number.MAX_SAFE_INTEGER),
|
|
192
|
+
backupTime: timestamp(point.backupTime),
|
|
193
|
+
expiresAt: timestamp(point.expiresAt),
|
|
194
|
+
retentionKind: boundedString(point.retentionKind, 32),
|
|
195
|
+
consistencyLevel: boundedString(point.consistencyLevel, 32),
|
|
196
|
+
sizeBytes: boundedInteger(point.sizeBytes, Number.MAX_SAFE_INTEGER),
|
|
197
|
+
};
|
|
198
|
+
}),
|
|
199
|
+
activeRestore: record(payload.activeRestore).id === undefined
|
|
200
|
+
? null
|
|
201
|
+
: { id: boundedString(record(payload.activeRestore).id, 36) },
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Build the exact browse request body.
|
|
206
|
+
*
|
|
207
|
+
* `filter` is omitted entirely when absent so that workers which decode with
|
|
208
|
+
* DisallowUnknownFields keep plain directory browsing working.
|
|
209
|
+
*/
|
|
210
|
+
export function fileBrowseRequestBody(input) {
|
|
211
|
+
const body = {
|
|
212
|
+
backupPointId: input.backupPointId,
|
|
213
|
+
offset: input.offset ?? 0,
|
|
214
|
+
};
|
|
215
|
+
if (input.sourceBrowseId !== undefined) {
|
|
216
|
+
body.sourceBrowseId = input.sourceBrowseId;
|
|
217
|
+
}
|
|
218
|
+
if (input.directoryEntryId !== undefined) {
|
|
219
|
+
body.directoryEntryId = input.directoryEntryId;
|
|
220
|
+
}
|
|
221
|
+
if (input.filter !== undefined) {
|
|
222
|
+
body.filter = input.filter;
|
|
223
|
+
}
|
|
224
|
+
return body;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Client-side guard for the backend's positional rules, so an obviously
|
|
228
|
+
* invalid combination never becomes a wasted 422 round trip.
|
|
229
|
+
*/
|
|
230
|
+
export function fileBrowseRequestRejection(input) {
|
|
231
|
+
const offset = input.offset ?? 0;
|
|
232
|
+
if (input.sourceBrowseId === undefined) {
|
|
233
|
+
if (input.directoryEntryId !== undefined) {
|
|
234
|
+
return "serviceFileBrowseRequestInvalid: directoryEntryId requires sourceBrowseId";
|
|
235
|
+
}
|
|
236
|
+
if (offset !== 0) {
|
|
237
|
+
return "serviceFileBrowseRequestInvalid: a root browse cannot start at a non-zero offset";
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (input.directoryEntryId !== undefined && offset !== 0) {
|
|
241
|
+
return "serviceFileBrowseRequestInvalid: entering a directory must start at offset 0";
|
|
242
|
+
}
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
export function readSearchAvailable(data) {
|
|
246
|
+
return record(data).searchAvailable === true;
|
|
247
|
+
}
|