@homeflare/distilled-proxmox-backup 0.2.0 → 0.3.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 +19 -17
- package/dist/errors.d.ts +6 -5
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/missing-resource.test.d.ts +2 -0
- package/dist/missing-resource.test.d.ts.map +1 -0
- package/dist/missing-resource.test.js +62 -0
- package/dist/missing-resource.test.js.map +1 -0
- package/dist/protocol-boundaries.test.d.ts +2 -0
- package/dist/protocol-boundaries.test.d.ts.map +1 -0
- package/dist/protocol-boundaries.test.js +50 -0
- package/dist/protocol-boundaries.test.js.map +1 -0
- package/dist/protocol-codecs.d.ts +23 -0
- package/dist/protocol-codecs.d.ts.map +1 -0
- package/dist/protocol-codecs.js +49 -0
- package/dist/protocol-codecs.js.map +1 -0
- package/dist/protocol-codecs.test.d.ts +2 -0
- package/dist/protocol-codecs.test.d.ts.map +1 -0
- package/dist/protocol-codecs.test.js +58 -0
- package/dist/protocol-codecs.test.js.map +1 -0
- package/dist/protocol-raw-root.test.d.ts +2 -0
- package/dist/protocol-raw-root.test.d.ts.map +1 -0
- package/dist/protocol-raw-root.test.js +48 -0
- package/dist/protocol-raw-root.test.js.map +1 -0
- package/dist/protocol.d.ts +1 -0
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +4 -1
- package/dist/protocol.js.map +1 -1
- package/dist/services/access.d.ts.map +1 -1
- package/dist/services/access.js +5 -16
- package/dist/services/access.js.map +1 -1
- package/dist/services/admin.d.ts.map +1 -1
- package/dist/services/admin.js +11 -14
- package/dist/services/admin.js.map +1 -1
- package/dist/services/config.d.ts +44 -4
- package/dist/services/config.d.ts.map +1 -1
- package/dist/services/config.js +56 -83
- package/dist/services/config.js.map +1 -1
- package/dist/services/nodes.d.ts.map +1 -1
- package/dist/services/nodes.js +2 -4
- package/dist/services/nodes.js.map +1 -1
- package/package.json +1 -1
- package/src/errors.ts +7 -11
- package/src/missing-resource.test.ts +98 -0
- package/src/protocol-boundaries.test.ts +60 -0
- package/src/protocol-codecs.test.ts +112 -0
- package/src/protocol-codecs.ts +75 -0
- package/src/protocol-raw-root.test.ts +84 -0
- package/src/protocol.ts +62 -58
- package/src/services/access.ts +5 -16
- package/src/services/admin.ts +13 -14
- package/src/services/config.ts +72 -95
- package/src/services/nodes.ts +2 -4
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
5
|
+
import type { HttpClientRequest } from "effect/unstable/http/HttpClientRequest";
|
|
6
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
7
|
+
import { credentials } from "./credentials.ts";
|
|
8
|
+
import * as Retry from "./retry.ts";
|
|
9
|
+
import {
|
|
10
|
+
getConfigNotificationMatcher,
|
|
11
|
+
putConfigNotificationMatcher,
|
|
12
|
+
} from "./services/config.ts";
|
|
13
|
+
|
|
14
|
+
const credential = credentials({
|
|
15
|
+
baseUrl: "https://pbs.test:8007",
|
|
16
|
+
tokenId: "test@pbs!fake",
|
|
17
|
+
secret: "fake-not-real",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const fakePbs = (body: unknown, see?: (request: HttpClientRequest) => void) =>
|
|
21
|
+
Layer.succeed(
|
|
22
|
+
HttpClient.HttpClient,
|
|
23
|
+
HttpClient.make((request) =>
|
|
24
|
+
Effect.sync(() => {
|
|
25
|
+
see?.(request);
|
|
26
|
+
return HttpClientResponse.fromWeb(
|
|
27
|
+
request,
|
|
28
|
+
Response.json({ data: body }),
|
|
29
|
+
);
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
test("PBS arrays use repeated keys and Unit data:null remains a successful write", async () => {
|
|
35
|
+
let pairs: [string, string][] = [];
|
|
36
|
+
const client = fakePbs(null, (request) => {
|
|
37
|
+
expect(request.body._tag).toBe("Uint8Array");
|
|
38
|
+
if (request.body._tag === "Uint8Array") {
|
|
39
|
+
pairs = [
|
|
40
|
+
...new URLSearchParams(new TextDecoder().decode(request.body.body)),
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
await Effect.runPromise(
|
|
45
|
+
putConfigNotificationMatcher({
|
|
46
|
+
name: "pager",
|
|
47
|
+
target: ["mail-to-root", "hook"],
|
|
48
|
+
delete: ["match-field", "match-calendar"],
|
|
49
|
+
}).pipe(Retry.none, Effect.provide(Layer.mergeAll(client, credential))),
|
|
50
|
+
);
|
|
51
|
+
expect(pairs.filter(([key]) => key === "target")).toEqual([
|
|
52
|
+
["target", "mail-to-root"],
|
|
53
|
+
["target", "hook"],
|
|
54
|
+
]);
|
|
55
|
+
expect(pairs.filter(([key]) => key === "delete")).toEqual([
|
|
56
|
+
["delete", "match-field"],
|
|
57
|
+
["delete", "match-calendar"],
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const body of [null, { comment: "secret-like-text-must-not-appear" }]) {
|
|
62
|
+
test("incomplete resource output fails without exposing response contents", async () => {
|
|
63
|
+
const error = await Effect.runPromise(
|
|
64
|
+
getConfigNotificationMatcher({ name: "pager" }).pipe(
|
|
65
|
+
Retry.none,
|
|
66
|
+
Effect.flip,
|
|
67
|
+
Effect.provide(Layer.mergeAll(fakePbs(body), credential)),
|
|
68
|
+
),
|
|
69
|
+
);
|
|
70
|
+
expect(error).toMatchObject({ _tag: "ProxmoxBackupParseError" });
|
|
71
|
+
expect(JSON.stringify(error)).not.toContain(
|
|
72
|
+
"secret-like-text-must-not-appear",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test("valid resource output preserves fields omitted from the vendor schema", async () => {
|
|
78
|
+
const result = await Effect.runPromise(
|
|
79
|
+
getConfigNotificationMatcher({ name: "pager" }).pipe(
|
|
80
|
+
Retry.none,
|
|
81
|
+
Effect.provide(
|
|
82
|
+
Layer.mergeAll(
|
|
83
|
+
fakePbs({ name: "pager", future_field: "keep-me" }),
|
|
84
|
+
credential,
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
expect(result).toMatchObject({ name: "pager", future_field: "keep-me" });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("mapped NotFound is in the operation's typed union, including a plain-text response", async () => {
|
|
93
|
+
const client = Layer.succeed(
|
|
94
|
+
HttpClient.HttpClient,
|
|
95
|
+
HttpClient.make((request) =>
|
|
96
|
+
Effect.succeed(
|
|
97
|
+
HttpClientResponse.fromWeb(
|
|
98
|
+
request,
|
|
99
|
+
new Response("matcher 'pager' not found", { status: 404 }),
|
|
100
|
+
),
|
|
101
|
+
),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
const result = await Effect.runPromise(
|
|
105
|
+
getConfigNotificationMatcher({ name: "pager" }).pipe(
|
|
106
|
+
Retry.none,
|
|
107
|
+
Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
|
|
108
|
+
Effect.provide(Layer.mergeAll(client, credential)),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
expect(result).toBeUndefined();
|
|
112
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PBS 4.2.6-1's form arrays and successful response validation.
|
|
3
|
+
*
|
|
4
|
+
* The vendor's array parameters are repeated keys (`target=a&target=b`), not the Stripe
|
|
5
|
+
* bracket-index encoding used by core's generic REST builder. Measured against the PBS
|
|
6
|
+
* notification provider's existing protocol fixtures, 2026-09-24: indexed headers/secrets
|
|
7
|
+
* disappear from its read-back, and matchers perpetually drift. Preserve core path/auth/key
|
|
8
|
+
* mapping and adapt only its top-level indexed form fields at this vendor boundary.
|
|
9
|
+
*/
|
|
10
|
+
import * as API from "@distilled.cloud/core/api";
|
|
11
|
+
import * as Effect from "effect/Effect";
|
|
12
|
+
import * as Layer from "effect/Layer";
|
|
13
|
+
import * as Schema from "effect/Schema";
|
|
14
|
+
import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest";
|
|
15
|
+
import { ProxmoxBackupParseError } from "./errors.ts";
|
|
16
|
+
|
|
17
|
+
const repeatedFormKeys = (request: HttpClientRequest.HttpClientRequest) => {
|
|
18
|
+
if (
|
|
19
|
+
request.body._tag !== "Uint8Array" ||
|
|
20
|
+
request.body.contentType !== "application/x-www-form-urlencoded"
|
|
21
|
+
)
|
|
22
|
+
return request;
|
|
23
|
+
const fields = new URLSearchParams(
|
|
24
|
+
new TextDecoder().decode(request.body.body),
|
|
25
|
+
);
|
|
26
|
+
const repeated: [string, string][] = [];
|
|
27
|
+
for (const [key, value] of fields) {
|
|
28
|
+
repeated.push([key.replace(/^([^\[\]]+)\[\d+\]$/, "$1"), value]);
|
|
29
|
+
}
|
|
30
|
+
return HttpClientRequest.bodyUrlParams(request, repeated);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Core REST decode maps wire keys but does not validate required fields. In particular,
|
|
35
|
+
* PBS data:null unwrapped to {} used to look like a real resource to a caller. Validate
|
|
36
|
+
* with the operation's generated schema: Unit operations still accept {}, while a GET
|
|
37
|
+
* missing its required name/id fails. Preserve the mapped payload after validation: vendor
|
|
38
|
+
* fields absent from the published schema must not silently disappear. Core has already
|
|
39
|
+
* decoded the payload, so validate the schema's Type side instead of re-decoding transforms.
|
|
40
|
+
* Keep server data out of the parse error: notification responses can contain header
|
|
41
|
+
* credentials, and a schema diagnostic can reproduce them.
|
|
42
|
+
*/
|
|
43
|
+
export const withPbsCodecs = (
|
|
44
|
+
base: Layer.Layer<API.Protocol>,
|
|
45
|
+
): Layer.Layer<API.Protocol> =>
|
|
46
|
+
Layer.effect(
|
|
47
|
+
API.Protocol,
|
|
48
|
+
Effect.gen(function* () {
|
|
49
|
+
const protocol = yield* API.Protocol;
|
|
50
|
+
return API.Protocol.of({
|
|
51
|
+
encode: (args) =>
|
|
52
|
+
protocol.encode(args).pipe(Effect.map(repeatedFormKeys)),
|
|
53
|
+
decode: (args) =>
|
|
54
|
+
protocol.decode(args).pipe(
|
|
55
|
+
Effect.flatMap((body) =>
|
|
56
|
+
args.config.output === undefined
|
|
57
|
+
? Effect.succeed(body)
|
|
58
|
+
: (Schema.decodeUnknownEffect(
|
|
59
|
+
Schema.toType(args.config.output),
|
|
60
|
+
)(body).pipe(
|
|
61
|
+
Effect.as(body),
|
|
62
|
+
Effect.mapError(
|
|
63
|
+
() =>
|
|
64
|
+
new ProxmoxBackupParseError({
|
|
65
|
+
body: undefined,
|
|
66
|
+
cause:
|
|
67
|
+
"Response does not match the operation's declared output schema",
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
) as Effect.Effect<unknown>),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
});
|
|
74
|
+
}),
|
|
75
|
+
).pipe(Layer.provide(base));
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import * as Effect from "effect/Effect";
|
|
3
|
+
import * as Layer from "effect/Layer";
|
|
4
|
+
import * as HttpClient from "effect/unstable/http/HttpClient";
|
|
5
|
+
import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse";
|
|
6
|
+
import { credentials } from "./credentials.ts";
|
|
7
|
+
import type { ProxmoxBackupOpContext } from "./protocol.ts";
|
|
8
|
+
import * as Retry from "./retry.ts";
|
|
9
|
+
import {
|
|
10
|
+
deleteConfigDatastore,
|
|
11
|
+
getConfigAcmeTos,
|
|
12
|
+
listConfigDatastore,
|
|
13
|
+
} from "./services/config.ts";
|
|
14
|
+
|
|
15
|
+
const run = <A, E>(
|
|
16
|
+
operation: Effect.Effect<A, E, ProxmoxBackupOpContext>,
|
|
17
|
+
data: unknown,
|
|
18
|
+
) =>
|
|
19
|
+
Effect.runPromise(
|
|
20
|
+
operation.pipe(
|
|
21
|
+
Retry.none,
|
|
22
|
+
Effect.provide(
|
|
23
|
+
Layer.mergeAll(
|
|
24
|
+
credentials({
|
|
25
|
+
baseUrl: "https://pbs.test:8007",
|
|
26
|
+
tokenId: "test@pbs!fake",
|
|
27
|
+
secret: "fake-not-real",
|
|
28
|
+
}),
|
|
29
|
+
Layer.succeed(
|
|
30
|
+
HttpClient.HttpClient,
|
|
31
|
+
HttpClient.make((request) =>
|
|
32
|
+
Effect.succeed(
|
|
33
|
+
HttpClientResponse.fromWeb(request, Response.json({ data })),
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
test("raw-root string responses retain task IDs and ACME terms URLs", async () => {
|
|
43
|
+
const upid =
|
|
44
|
+
"UPID:pbs:00000001:00000002:00000003:delete-datastore:test:root@pam:";
|
|
45
|
+
expect(await run(deleteConfigDatastore({ name: "test" }), upid)).toBe(upid);
|
|
46
|
+
const url = "https://acme.test/terms";
|
|
47
|
+
expect(await run(getConfigAcmeTos({}), url)).toBe(url);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("raw-root lists retain empty lists, map wire keys, and preserve extra fields", async () => {
|
|
51
|
+
expect(await run(listConfigDatastore({}), [])).toEqual([]);
|
|
52
|
+
const expected = [
|
|
53
|
+
{ name: "test", path: "/backup/test", keep_last: 2, future_field: "keep" },
|
|
54
|
+
];
|
|
55
|
+
expect(
|
|
56
|
+
await run(listConfigDatastore({}), [
|
|
57
|
+
{
|
|
58
|
+
name: "test",
|
|
59
|
+
path: "/backup/test",
|
|
60
|
+
"keep-last": 2,
|
|
61
|
+
future_field: "keep",
|
|
62
|
+
},
|
|
63
|
+
]),
|
|
64
|
+
).toEqual(expected);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// These operations are not Unit responses: null must not become a valid empty object.
|
|
68
|
+
for (const data of [null, {}]) {
|
|
69
|
+
test(`raw-root operations reject ${JSON.stringify(data)} instead of accepting missing output`, async () => {
|
|
70
|
+
const operations: Effect.Effect<
|
|
71
|
+
unknown,
|
|
72
|
+
unknown,
|
|
73
|
+
ProxmoxBackupOpContext
|
|
74
|
+
>[] = [
|
|
75
|
+
deleteConfigDatastore({ name: "test" }),
|
|
76
|
+
getConfigAcmeTos({}),
|
|
77
|
+
listConfigDatastore({}),
|
|
78
|
+
];
|
|
79
|
+
for (const operation of operations) {
|
|
80
|
+
const error = await run(Effect.flip(operation), data);
|
|
81
|
+
expect(error).toMatchObject({ _tag: "ProxmoxBackupParseError" });
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
package/src/protocol.ts
CHANGED
|
@@ -89,6 +89,7 @@ import {
|
|
|
89
89
|
type RestErrorEnvelope,
|
|
90
90
|
} from "@distilled.cloud/core/protocol-rest";
|
|
91
91
|
import { Credentials, type Config } from "./credentials.ts";
|
|
92
|
+
import { withPbsCodecs } from "./protocol-codecs.ts";
|
|
92
93
|
import {
|
|
93
94
|
ParameterVerificationFailed,
|
|
94
95
|
UnknownProxmoxBackupError,
|
|
@@ -132,65 +133,68 @@ const PROXMOX_BACKUP_STATUS_MAP: Record<number, new (args: any) => unknown> = {
|
|
|
132
133
|
};
|
|
133
134
|
delete (PROXMOX_BACKUP_STATUS_MAP as Record<number, unknown>)[400];
|
|
134
135
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
136
|
+
const baseProtocol: Layer.Layer<API.Protocol> = makeRestProtocol<Config>({
|
|
137
|
+
credentials: Effect.gen(function* () {
|
|
138
|
+
const resolve = yield* Credentials;
|
|
139
|
+
return yield* resolve;
|
|
140
|
+
}),
|
|
141
|
+
baseUrl: (creds) => creds.apiBaseUrl,
|
|
142
|
+
headers: (creds) => ({
|
|
143
|
+
// ⛔ COLON, NOT `=` — see the module header's Authentication section.
|
|
144
|
+
Authorization: `PBSAPIToken=${creds.tokenId}:${Redacted.value(creds.secret)}`,
|
|
145
|
+
Accept: "application/json",
|
|
146
|
+
}),
|
|
147
|
+
errorEnvelope,
|
|
148
|
+
statusMap: PROXMOX_BACKUP_STATUS_MAP,
|
|
149
|
+
// Unwrap PBS's `{"data": …}` envelope BEFORE the schema-driven decode —
|
|
150
|
+
// see the module header. `data` is `null`/absent for a Unit-output
|
|
151
|
+
// operation; `?? {}` is core's own convention for "no body".
|
|
152
|
+
transformResponse: (body) => {
|
|
153
|
+
if (
|
|
154
|
+
body !== null &&
|
|
155
|
+
typeof body === "object" &&
|
|
156
|
+
"data" in (body as Record<string, unknown>)
|
|
157
|
+
) {
|
|
158
|
+
return (body as Record<string, unknown>).data ?? {};
|
|
159
|
+
}
|
|
160
|
+
return body;
|
|
161
|
+
},
|
|
162
|
+
// Reached for a 400 (see PROXMOX_BACKUP_STATUS_MAP above) or a status
|
|
163
|
+
// with no core mapping at all. `body` is the FULL parsed failure
|
|
164
|
+
// envelope, so this is where PBS's per-field `errors` object is read.
|
|
165
|
+
//
|
|
166
|
+
// ⛔ A BARE 400 WITHOUT THE `errors` OBJECT MUST NOT FALL INTO
|
|
167
|
+
// `UnknownProxmoxBackupError` — same retry-amplification hazard
|
|
168
|
+
// PVE's identical comment explains (`ServerError` is retried
|
|
169
|
+
// automatically; a malformed request is permanent). `BadRequest`
|
|
170
|
+
// (`Category.withBadRequestError`, not retryable) is the honest
|
|
171
|
+
// answer for "PBS said 400 and gave no further structure".
|
|
172
|
+
unknownError: ({ status, message, body }) => {
|
|
173
|
+
const b =
|
|
174
|
+
body !== null && typeof body === "object"
|
|
175
|
+
? (body as Record<string, unknown>)
|
|
176
|
+
: undefined;
|
|
177
|
+
const fieldErrors = b?.errors;
|
|
178
|
+
if (status === 400) {
|
|
153
179
|
if (
|
|
154
|
-
|
|
155
|
-
typeof
|
|
156
|
-
|
|
180
|
+
fieldErrors !== null &&
|
|
181
|
+
typeof fieldErrors === "object" &&
|
|
182
|
+
!Array.isArray(fieldErrors)
|
|
157
183
|
) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
// with no core mapping at all. `body` is the FULL parsed failure
|
|
164
|
-
// envelope, so this is where PBS's per-field `errors` object is read.
|
|
165
|
-
//
|
|
166
|
-
// ⛔ A BARE 400 WITHOUT THE `errors` OBJECT MUST NOT FALL INTO
|
|
167
|
-
// `UnknownProxmoxBackupError` — same retry-amplification hazard
|
|
168
|
-
// PVE's identical comment explains (`ServerError` is retried
|
|
169
|
-
// automatically; a malformed request is permanent). `BadRequest`
|
|
170
|
-
// (`Category.withBadRequestError`, not retryable) is the honest
|
|
171
|
-
// answer for "PBS said 400 and gave no further structure".
|
|
172
|
-
unknownError: ({ status, message, body }) => {
|
|
173
|
-
const b =
|
|
174
|
-
body !== null && typeof body === "object"
|
|
175
|
-
? (body as Record<string, unknown>)
|
|
176
|
-
: undefined;
|
|
177
|
-
const fieldErrors = b?.errors;
|
|
178
|
-
if (status === 400) {
|
|
179
|
-
if (
|
|
180
|
-
fieldErrors !== null &&
|
|
181
|
-
typeof fieldErrors === "object" &&
|
|
182
|
-
!Array.isArray(fieldErrors)
|
|
183
|
-
) {
|
|
184
|
-
const errors: Record<string, string> = {};
|
|
185
|
-
for (const [k, v] of Object.entries(
|
|
186
|
-
fieldErrors as Record<string, unknown>,
|
|
187
|
-
)) {
|
|
188
|
-
if (typeof v === "string") errors[k] = v;
|
|
189
|
-
}
|
|
190
|
-
return new ParameterVerificationFailed({ message, errors });
|
|
184
|
+
const errors: Record<string, string> = {};
|
|
185
|
+
for (const [k, v] of Object.entries(
|
|
186
|
+
fieldErrors as Record<string, unknown>,
|
|
187
|
+
)) {
|
|
188
|
+
if (typeof v === "string") errors[k] = v;
|
|
191
189
|
}
|
|
192
|
-
return new
|
|
190
|
+
return new ParameterVerificationFailed({ message, errors });
|
|
193
191
|
}
|
|
194
|
-
return new
|
|
195
|
-
}
|
|
196
|
-
|
|
192
|
+
return new BadRequest({ message });
|
|
193
|
+
}
|
|
194
|
+
return new UnknownProxmoxBackupError({ status, message, body });
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
/** PBS-specific form encoding and schema validation wrap core without changing other vendors. */
|
|
199
|
+
export const ProxmoxBackupProtocol: Layer.Layer<API.Protocol> =
|
|
200
|
+
withPbsCodecs(baseProtocol);
|
package/src/services/access.ts
CHANGED
|
@@ -189,14 +189,9 @@ export const DeleteAccessTfaRequest = /*@__PURE__*/ S.suspend(() =>
|
|
|
189
189
|
S.Struct({
|
|
190
190
|
userid: S.String.pipe(T.Label()),
|
|
191
191
|
id: S.String.pipe(T.Label()),
|
|
192
|
-
password: S.optional(S.String),
|
|
192
|
+
password: S.optional(S.String.pipe(T.Query())),
|
|
193
193
|
}).pipe(
|
|
194
|
-
T.Http({
|
|
195
|
-
method: "DELETE",
|
|
196
|
-
uri: "/access/tfa/{userid}/{id}",
|
|
197
|
-
code: 200,
|
|
198
|
-
contentType: "form-urlencoded",
|
|
199
|
-
}),
|
|
194
|
+
T.Http({ method: "DELETE", uri: "/access/tfa/{userid}/{id}", code: 200 }),
|
|
200
195
|
),
|
|
201
196
|
).annotate({
|
|
202
197
|
identifier: "DeleteAccessTfaRequest",
|
|
@@ -233,14 +228,9 @@ export interface DeleteAccessUserRequest {
|
|
|
233
228
|
export const DeleteAccessUserRequest = /*@__PURE__*/ S.suspend(() =>
|
|
234
229
|
S.Struct({
|
|
235
230
|
userid: S.String.pipe(T.Label()),
|
|
236
|
-
digest: S.optional(S.String),
|
|
231
|
+
digest: S.optional(S.String.pipe(T.Query())),
|
|
237
232
|
}).pipe(
|
|
238
|
-
T.Http({
|
|
239
|
-
method: "DELETE",
|
|
240
|
-
uri: "/access/users/{userid}",
|
|
241
|
-
code: 200,
|
|
242
|
-
contentType: "form-urlencoded",
|
|
243
|
-
}),
|
|
233
|
+
T.Http({ method: "DELETE", uri: "/access/users/{userid}", code: 200 }),
|
|
244
234
|
),
|
|
245
235
|
).annotate({
|
|
246
236
|
identifier: "DeleteAccessUserRequest",
|
|
@@ -263,13 +253,12 @@ export const DeleteAccessUserTokenRequest = /*@__PURE__*/ S.suspend(() =>
|
|
|
263
253
|
S.Struct({
|
|
264
254
|
userid: S.String.pipe(T.Label()),
|
|
265
255
|
token_name: S.String.pipe(T.Label()),
|
|
266
|
-
digest: S.optional(S.String),
|
|
256
|
+
digest: S.optional(S.String.pipe(T.Query())),
|
|
267
257
|
}).pipe(
|
|
268
258
|
T.Http({
|
|
269
259
|
method: "DELETE",
|
|
270
260
|
uri: "/access/users/{userid}/token/{token-name}",
|
|
271
261
|
code: 200,
|
|
272
|
-
contentType: "form-urlencoded",
|
|
273
262
|
}),
|
|
274
263
|
),
|
|
275
264
|
).annotate({
|
package/src/services/admin.ts
CHANGED
|
@@ -324,18 +324,19 @@ export interface DeleteAdminDatastoreGroupsRequest {
|
|
|
324
324
|
export const DeleteAdminDatastoreGroupsRequest = /*@__PURE__*/ S.suspend(() =>
|
|
325
325
|
S.Struct({
|
|
326
326
|
store: S.String.pipe(T.Label()),
|
|
327
|
-
backup_id: S.String.pipe(T.
|
|
327
|
+
backup_id: S.String.pipe(T.Query("backup-id")),
|
|
328
328
|
backup_type: DeleteAdminDatastoreGroupsRequestBackupType.pipe(
|
|
329
|
-
T.
|
|
329
|
+
T.Query("backup-type"),
|
|
330
330
|
),
|
|
331
|
-
error_on_protected: S.optional(
|
|
332
|
-
|
|
331
|
+
error_on_protected: S.optional(
|
|
332
|
+
S.String.pipe(T.Query("error-on-protected")),
|
|
333
|
+
),
|
|
334
|
+
ns: S.optional(S.String.pipe(T.Query())),
|
|
333
335
|
}).pipe(
|
|
334
336
|
T.Http({
|
|
335
337
|
method: "DELETE",
|
|
336
338
|
uri: "/admin/datastore/{store}/groups",
|
|
337
339
|
code: 200,
|
|
338
|
-
contentType: "form-urlencoded",
|
|
339
340
|
}),
|
|
340
341
|
),
|
|
341
342
|
).annotate({
|
|
@@ -372,17 +373,16 @@ export const DeleteAdminDatastoreNamespaceRequest = /*@__PURE__*/ S.suspend(
|
|
|
372
373
|
() =>
|
|
373
374
|
S.Struct({
|
|
374
375
|
store: S.String.pipe(T.Label()),
|
|
375
|
-
delete_groups: S.optional(S.String.pipe(T.
|
|
376
|
+
delete_groups: S.optional(S.String.pipe(T.Query("delete-groups"))),
|
|
376
377
|
error_on_protected: S.optional(
|
|
377
|
-
S.String.pipe(T.
|
|
378
|
+
S.String.pipe(T.Query("error-on-protected")),
|
|
378
379
|
),
|
|
379
|
-
ns: S.String,
|
|
380
|
+
ns: S.String.pipe(T.Query()),
|
|
380
381
|
}).pipe(
|
|
381
382
|
T.Http({
|
|
382
383
|
method: "DELETE",
|
|
383
384
|
uri: "/admin/datastore/{store}/namespace",
|
|
384
385
|
code: 200,
|
|
385
|
-
contentType: "form-urlencoded",
|
|
386
386
|
}),
|
|
387
387
|
),
|
|
388
388
|
).annotate({
|
|
@@ -417,18 +417,17 @@ export const DeleteAdminDatastoreSnapshotsRequest = /*@__PURE__*/ S.suspend(
|
|
|
417
417
|
() =>
|
|
418
418
|
S.Struct({
|
|
419
419
|
store: S.String.pipe(T.Label()),
|
|
420
|
-
backup_id: S.String.pipe(T.
|
|
421
|
-
backup_time: S.String.pipe(T.
|
|
420
|
+
backup_id: S.String.pipe(T.Query("backup-id")),
|
|
421
|
+
backup_time: S.String.pipe(T.Query("backup-time")),
|
|
422
422
|
backup_type: DeleteAdminDatastoreSnapshotsRequestBackupType.pipe(
|
|
423
|
-
T.
|
|
423
|
+
T.Query("backup-type"),
|
|
424
424
|
),
|
|
425
|
-
ns: S.optional(S.String),
|
|
425
|
+
ns: S.optional(S.String.pipe(T.Query())),
|
|
426
426
|
}).pipe(
|
|
427
427
|
T.Http({
|
|
428
428
|
method: "DELETE",
|
|
429
429
|
uri: "/admin/datastore/{store}/snapshots",
|
|
430
430
|
code: 200,
|
|
431
|
-
contentType: "form-urlencoded",
|
|
432
431
|
}),
|
|
433
432
|
),
|
|
434
433
|
).annotate({
|