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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn the VPSnet account API's authentication and authorisation rejections
|
|
3
|
+
* into an explanation the caller can act on.
|
|
4
|
+
*
|
|
5
|
+
* The API distinguishes these cases only by HTTP status plus a message or a
|
|
6
|
+
* boolean flag in the JSON body — there is no error-code field and no
|
|
7
|
+
* WWW-Authenticate header. Without this translation an MCP client shows a bare
|
|
8
|
+
* "Unauthorized" and the user has to guess which of six unrelated problems
|
|
9
|
+
* they actually have.
|
|
10
|
+
*/
|
|
11
|
+
function record(value) {
|
|
12
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
13
|
+
? value
|
|
14
|
+
: {};
|
|
15
|
+
}
|
|
16
|
+
function message(data) {
|
|
17
|
+
const raw = record(data).message;
|
|
18
|
+
return typeof raw === "string" ? raw : "";
|
|
19
|
+
}
|
|
20
|
+
const MANAGEMENT_KEY_FIX = "Create a management API key in the VPSnet panel under Account > API Keys "
|
|
21
|
+
+ "(scope 'full', or 'read' if you only need GET reads) and set it as "
|
|
22
|
+
+ "VPSNET_API_KEY. A key is 'vpsnet_' followed by 43 characters.";
|
|
23
|
+
/**
|
|
24
|
+
* Classify a non-2xx response. Returns null for anything that is not an
|
|
25
|
+
* authentication or authorisation rejection.
|
|
26
|
+
*/
|
|
27
|
+
export function explainAuthFailure(status, data) {
|
|
28
|
+
if (status !== 401 && status !== 403 && status !== 429) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const payload = record(data);
|
|
32
|
+
const text = message(data);
|
|
33
|
+
// An AI-scoped key (api_keys.scope='ai') is restricted to VPSnet AI
|
|
34
|
+
// inference and is refused on every authenticated account route. This is
|
|
35
|
+
// deliberate backend behaviour, not a misconfigured permission.
|
|
36
|
+
if (text.includes("restricted to VPSnet AI inference")) {
|
|
37
|
+
return {
|
|
38
|
+
code: "aiScopedApiKeyCannotManageAccount",
|
|
39
|
+
reason: "This API key is an AI-scoped key. AI-scoped keys are issued only for "
|
|
40
|
+
+ "VPSnet AI assistant inference and are deliberately walled off from "
|
|
41
|
+
+ "the whole authenticated account API, so no service, application, "
|
|
42
|
+
+ "DNS, domain, or billing tool in this server can work with it. "
|
|
43
|
+
+ "Adding scopes to it will not help — the restriction is on the key "
|
|
44
|
+
+ "type itself.",
|
|
45
|
+
fix: "This server needs a separate management API key. "
|
|
46
|
+
+ MANAGEMENT_KEY_FIX
|
|
47
|
+
+ " Keep the AI-scoped key for VPSnet AI assistant inference only.",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (text.includes("API key scope is read-only")) {
|
|
51
|
+
return {
|
|
52
|
+
code: "readOnlyApiKeyCannotWrite",
|
|
53
|
+
reason: "This API key has the read-only scope, which is limited to GET "
|
|
54
|
+
+ "requests. The tool you called performs a write.",
|
|
55
|
+
fix: "Use a full-scope API key for write operations, or restrict yourself "
|
|
56
|
+
+ "to read-only tools with this key.",
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (payload.readOnlyApiKeyForbidden === true) {
|
|
60
|
+
return {
|
|
61
|
+
code: "readOnlyApiKeyForbidden",
|
|
62
|
+
reason: "This endpoint refuses read-only API keys because the operation "
|
|
63
|
+
+ "changes state.",
|
|
64
|
+
fix: "Use a full-scope API key for this operation.",
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (payload.apiKeyScopeMissing === true) {
|
|
68
|
+
const required = typeof payload.requiredScope === "string"
|
|
69
|
+
? payload.requiredScope
|
|
70
|
+
: null;
|
|
71
|
+
return {
|
|
72
|
+
code: "apiKeyScopeMissing",
|
|
73
|
+
reason: required
|
|
74
|
+
? `The API key authenticated successfully but is missing the '${required}' scope.`
|
|
75
|
+
: "The API key authenticated successfully but is missing a scope this endpoint requires.",
|
|
76
|
+
fix: required
|
|
77
|
+
? `Add the '${required}' scope to the key in Account > API Keys, or use a key that already has it.`
|
|
78
|
+
: "Grant the endpoint's scope to the key in Account > API Keys.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (payload.apiKeyForbidden === true) {
|
|
82
|
+
return {
|
|
83
|
+
code: "apiKeyForbidden",
|
|
84
|
+
reason: "This endpoint is closed to API-key authentication entirely. API keys "
|
|
85
|
+
+ "never grant admin access.",
|
|
86
|
+
fix: "Perform this operation in the VPSnet panel with a signed-in session. "
|
|
87
|
+
+ "Do not attempt to reach it with an API key.",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (status === 429 || payload.rate_limited === true) {
|
|
91
|
+
return {
|
|
92
|
+
code: "apiKeyRateLimited",
|
|
93
|
+
reason: "The API key exceeded its request rate limit.",
|
|
94
|
+
fix: "Back off and retry after the retry_after interval.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (status === 401) {
|
|
98
|
+
// Manager::check() swallows the specific AuthException, so an unknown,
|
|
99
|
+
// revoked, expired or malformed key and a non-allowlisted source IP are
|
|
100
|
+
// genuinely indistinguishable here. Say so rather than guessing.
|
|
101
|
+
return {
|
|
102
|
+
code: "apiKeyRejected",
|
|
103
|
+
reason: "The API key was rejected at authentication. The account API returns "
|
|
104
|
+
+ "one identical 401 for every cause, so this is one of: the key does "
|
|
105
|
+
+ "not exist, was revoked, has expired, is malformed, or your source "
|
|
106
|
+
+ "IP is not on the key's IP allowlist. Note this is NOT the "
|
|
107
|
+
+ "AI-scoped-key case, which returns 403 with its own message.",
|
|
108
|
+
fix: "Check the key in Account > API Keys: confirm it is active, unexpired, "
|
|
109
|
+
+ "copied whole, and that any IP allowlist on it includes the address "
|
|
110
|
+
+ "this server calls from. " + MANAGEMENT_KEY_FIX,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
code: "apiKeyForbiddenUnclassified",
|
|
115
|
+
reason: "The account API refused this request for the authenticated key.",
|
|
116
|
+
fix: "Check the key's scopes and access settings in Account > API Keys.",
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Merge the explanation into the response payload.
|
|
121
|
+
*
|
|
122
|
+
* The machine code is also set as a boolean flag so that the strict per-feature
|
|
123
|
+
* sanitisers, which keep only `key: true` entries as error codes, still carry
|
|
124
|
+
* the signal through instead of dropping it.
|
|
125
|
+
*/
|
|
126
|
+
export function annotateAuthFailure(status, data) {
|
|
127
|
+
const failure = explainAuthFailure(status, data);
|
|
128
|
+
if (failure === null) {
|
|
129
|
+
return data;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...record(data),
|
|
133
|
+
[failure.code]: true,
|
|
134
|
+
auth_problem: {
|
|
135
|
+
code: failure.code,
|
|
136
|
+
http_status: status,
|
|
137
|
+
reason: failure.reason,
|
|
138
|
+
fix: failure.fix,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|