@serve.zone/interfaces 32.69.0 → 32.71.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.
package/changelog.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 2026-11-05 - 32.71.0
4
+
5
+ ### Features
6
+
7
+ - All-or-nothing backup restore refusals (AGL-1501, Linus 2026-11-05 ruling): `data.backupRestoreRefusalNames` freezes the three reasons an in-place restore is refused before it writes anything: `restore-snapshot-unavailable` (`missing`, a non-empty list of `data.IBackupRestoreMissingSnapshot`: `snapshotId`, `capability` (`data.TBackupResourceType`) and an optional `resourceName`), `restore-preflight-unsupported` (the node's Corestore offers no restore check) and `restore-relay-contract-behind` (`relayInterfacesVersion`, a release version or `null`, below `minimumInterfacesVersion`). `data.TBackupRestoreRefusal` is discriminated by `name`; `data.validateBackupRestoreRefusal`, `data.isBackupRestoreRefusal` and `data.readBackupRestoreRefusal` judge untrusted `errorData` against each exact schema, and `data.buildBackupRestoreSnapshotUnavailableRefusal`, `data.buildBackupRestorePreflightUnsupportedRefusal` and `data.buildBackupRestoreRelayContractBehindRefusal` build them; bounds are `data.backupRestoreRefusalContract`. A restore event (`data.IBackupRestoreEvent`) adds the status `refused`, meaning nothing was written, with its `refusal`; `data.validateBackupRestoreEvent` judges only `status` and `refusal`, requiring `refusal` exactly when the status is `refused`. Additive: every existing restore event stays valid, including the unjudged `resourceTypes` and null `errorText` Cloudly already stores. Spec: `test.backuprestorerefusal`.
8
+
9
+ ## 2026-11-05 - 32.70.0
10
+
11
+ ### Features
12
+
13
+ - Partial restore of an incomplete backup (Linus 2026-11-05 ruling): a `failed` backup record can name its failure's kind, `data.IBackupRecord.failure` (`data.TBackupFailure`): `incomplete` with `uncaptured`, each resource its service declares that the backup did not capture (`data.TBackupUncapturedResource`: a volume by mount path, always with reason `volume-capture-unavailable`, or a binding by id, always with reason `binding-not-snapshotted`; `data.TBackupUncapturedReason` names either), or `archive-unreadable`. `requests.backup.IReq_Any_Cloudly_RestoreServiceBackup` takes `confirmPartialRestore`, which an administrator sets to restore what an `incomplete` backup captured: `resourceTypes` is required and names only captured types, nothing in `uncaptured` is restored, so the service then holds data from two points in time, and the backup stays `failed`; it is refused for any other backup. `data.readPartialRestoreResourceTypes` answers the types such a restore may name: those the backup holds a snapshot of, except `volume` (a `volume` snapshot captures no service volume) and the type of any uncaptured resource; none for any other backup. A restore event (`data.IBackupRestoreEvent`) states the `resourceTypes` it was limited to and `partial` for such a restore. Additive: no validator reads these shapes. Spec: `test.backuppartialrestore`.
14
+
3
15
  ## 2026-11-05 - 32.69.0
4
16
 
5
17
  ### Features
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.69.0',
6
+ version: '32.71.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  };
9
9
  //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFNBQVM7SUFDbEIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
@@ -1,3 +1,4 @@
1
+ import type { TBackupRestoreRefusal } from './backuprestorerefusal.js';
1
2
  import type { ICorestoreDatabaseAllocationReference } from './corestore.js';
2
3
  export type TBackupStatus = 'pending' | 'running' | 'replicating' | 'replicated' | 'ready' | 'failed' | 'restoring' | 'restored';
3
4
  export type TBackupTrigger = 'manual' | 'scheduled';
@@ -48,10 +49,62 @@ export interface IBackupReplicationResult {
48
49
  totalSize: number;
49
50
  completedAt: number;
50
51
  }
52
+ /**
53
+ * One resource a service declares that a backup of it did not capture, with why; each kind of
54
+ * resource has its own reason.
55
+ *
56
+ * `volume-capture-unavailable`: a service volume is a node-local storage claim on its Pallet node,
57
+ * which no relay can capture yet; a `volume` snapshot is a Corestore Docker volume and captures no
58
+ * service volume. `binding-not-snapshotted`: the backup holds no snapshot of the Corestore resource
59
+ * the binding names.
60
+ */
61
+ export type TBackupUncapturedResource = {
62
+ type: 'volume';
63
+ mountPath: string;
64
+ reason: 'volume-capture-unavailable';
65
+ } | {
66
+ type: 'database' | 'objectstorage';
67
+ bindingId: string;
68
+ reason: 'binding-not-snapshotted';
69
+ };
70
+ /** Why a resource a service declares is not in one of its backups (`TBackupUncapturedResource`). */
71
+ export type TBackupUncapturedReason = TBackupUncapturedResource['reason'];
72
+ /**
73
+ * The named kind of a `failed` backup's failure; a failure without one is named only in `errorText`.
74
+ *
75
+ * `incomplete`: the backup did not capture every resource its service declares, and `uncaptured`
76
+ * names each one it missed. What it did capture is restored only by a partial restore an
77
+ * administrator confirms (`IReq_Any_Cloudly_RestoreServiceBackup` `confirmPartialRestore`), which
78
+ * restores none of `uncaptured`, and the backup stays `failed`.
79
+ * `archive-unreadable`: its stored archive is missing or damaged, so it restores nothing.
80
+ */
81
+ export type TBackupFailure = {
82
+ kind: 'incomplete';
83
+ uncaptured: TBackupUncapturedResource[];
84
+ } | {
85
+ kind: 'archive-unreadable';
86
+ };
87
+ /**
88
+ * One restore of a backup, appended to its `restoreHistory`; `validateBackupRestoreEvent` judges its
89
+ * `status` and `refusal`.
90
+ *
91
+ * `refused`: the restore was refused before it wrote anything (`refusal` says why), so the service
92
+ * keeps the data it had. `failed`: it failed after it began writing.
93
+ */
51
94
  export interface IBackupRestoreEvent {
95
+ /** When the restore ended, or when it was refused. */
52
96
  restoredAt: number;
53
- status: 'restored' | 'failed';
97
+ status: 'restored' | 'failed' | 'refused';
54
98
  errorText?: string;
99
+ /** The resource types the restore was limited to; absent when it restored every snapshot. */
100
+ resourceTypes?: TBackupResourceType[];
101
+ /**
102
+ * A confirmed partial restore of an `incomplete` backup: it restored only `resourceTypes`, and
103
+ * none of the backup's `failure.uncaptured`.
104
+ */
105
+ partial?: true;
106
+ /** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
107
+ refusal?: TBackupRestoreRefusal;
55
108
  }
56
109
  /**
57
110
  * Durable retention hold owned by one isolated restore. An entry is active
@@ -88,6 +141,8 @@ export interface IBackupRecord {
88
141
  completedAt?: number;
89
142
  requestedBy?: string;
90
143
  errorText?: string;
144
+ /** The named kind of a `failed` backup's failure (`TBackupFailure`). */
145
+ failure?: TBackupFailure;
91
146
  restoreHistory?: IBackupRestoreEvent[];
92
147
  /**
93
148
  * Authoritative isolated-restore holds. Cloudly atomically appends the
@@ -97,6 +152,17 @@ export interface IBackupRecord {
97
152
  restoreHolds?: IBackupRestoreHold[];
98
153
  tags?: Record<string, string>;
99
154
  }
155
+ /**
156
+ * The resource types a partial restore of a backup may name (`confirmPartialRestore`).
157
+ *
158
+ * For a `failed` backup whose failure is `incomplete`: every type it holds a snapshot of, except
159
+ * `volume` (a `volume` snapshot is a Corestore Docker volume and captures no service volume) and the
160
+ * type of any resource it did not capture, so a partial restore never restores a snapshot standing
161
+ * for a resource the backup missed. For every other backup: none. Database before object storage.
162
+ */
163
+ export declare const readPartialRestoreResourceTypes: (backupArg: Pick<IBackupRecord, 'status' | 'failure'> & {
164
+ snapshots: ReadonlyArray<Pick<TBackupSnapshot, 'type'>>;
165
+ }) => Array<Exclude<TBackupResourceType, 'volume'>>;
100
166
  /** Retention policy understood by containerarchive prune. */
101
167
  export interface IArchiveRetentionPolicy {
102
168
  keepLast?: number;
@@ -1,2 +1,16 @@
1
- export {};
2
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9iYWNrdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiJ9
1
+ /**
2
+ * The resource types a partial restore of a backup may name (`confirmPartialRestore`).
3
+ *
4
+ * For a `failed` backup whose failure is `incomplete`: every type it holds a snapshot of, except
5
+ * `volume` (a `volume` snapshot is a Corestore Docker volume and captures no service volume) and the
6
+ * type of any resource it did not capture, so a partial restore never restores a snapshot standing
7
+ * for a resource the backup missed. For every other backup: none. Database before object storage.
8
+ */
9
+ export const readPartialRestoreResourceTypes = (backupArg) => {
10
+ const failure = backupArg.failure;
11
+ if (backupArg.status !== 'failed' || failure?.kind !== 'incomplete')
12
+ return [];
13
+ const uncapturedTypes = new Set(failure.uncaptured.map((resourceArg) => resourceArg.type));
14
+ return ['database', 'objectstorage'].filter((typeArg) => (!uncapturedTypes.has(typeArg) && backupArg.snapshots.some((snapshotArg) => snapshotArg.type === typeArg)));
15
+ };
16
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9iYWNrdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBK0tBOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRyxDQUM3QyxTQUFrSCxFQUNuRSxFQUFFO0lBQ2pELE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUM7SUFDbEMsSUFBSSxTQUFTLENBQUMsTUFBTSxLQUFLLFFBQVEsSUFBSSxPQUFPLEVBQUUsSUFBSSxLQUFLLFlBQVk7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUMvRSxNQUFNLGVBQWUsR0FBRyxJQUFJLEdBQUcsQ0FBc0IsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ2hILE9BQVEsQ0FBQyxVQUFVLEVBQUUsZUFBZSxDQUFXLENBQUMsTUFBTSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUNsRSxDQUFDLGVBQWUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksU0FBUyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLEtBQUssT0FBTyxDQUFDLENBQ3pHLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyJ9
@@ -0,0 +1,85 @@
1
+ import type { TBackupResourceType } from './backup.js';
2
+ /**
3
+ * Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
4
+ * it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
5
+ * the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
6
+ * whose contract carries that check. A refusal therefore always means nothing was written; the
7
+ * service keeps the data it had.
8
+ *
9
+ * - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
10
+ * restore names; `missing` names each of them.
11
+ * - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
12
+ * Corestore built before it answers the check route with HTTP 404), so it cannot prove the
13
+ * restore would be whole and nothing is restored.
14
+ * - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
15
+ * that carries the restore check, so Cloudly does not dispatch the restore; the relay states
16
+ * `relayInterfacesVersion` (`null` when it stated none) and the restore needs
17
+ * `minimumInterfacesVersion`.
18
+ *
19
+ * It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
20
+ * `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
21
+ * exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
22
+ * one without gating it on the reader's interfaces version.
23
+ */
24
+ export declare const backupRestoreRefusalNames: readonly ["restore-snapshot-unavailable", "restore-preflight-unsupported", "restore-relay-contract-behind"];
25
+ export type TBackupRestoreRefusalName = typeof backupRestoreRefusalNames[number];
26
+ export declare const backupRestoreRefusalContract: Readonly<{
27
+ /** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
28
+ readonly maximumMissingSnapshots: 256;
29
+ /** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
30
+ readonly maximumNameLength: 255;
31
+ }>;
32
+ /** One snapshot a restore names that the node's Corestore cannot read. */
33
+ export interface IBackupRestoreMissingSnapshot {
34
+ snapshotId: string;
35
+ /** The snapshot's resource type (`TBackupSnapshot['type']`). */
36
+ capability: TBackupResourceType;
37
+ /** The Corestore resource of a database or object-storage snapshot, when it is known. */
38
+ resourceName?: string;
39
+ }
40
+ export interface IBackupRestoreSnapshotUnavailableRefusal {
41
+ name: 'restore-snapshot-unavailable';
42
+ /** Non-empty; each snapshot once. */
43
+ missing: IBackupRestoreMissingSnapshot[];
44
+ }
45
+ export interface IBackupRestorePreflightUnsupportedRefusal {
46
+ name: 'restore-preflight-unsupported';
47
+ }
48
+ export interface IBackupRestoreRelayContractBehindRefusal {
49
+ name: 'restore-relay-contract-behind';
50
+ /** The interfaces release the relay speaks, or `null` when it stated none. */
51
+ relayInterfacesVersion: string | null;
52
+ /** The lowest interfaces release a relay must speak to carry a restore. */
53
+ minimumInterfacesVersion: string;
54
+ }
55
+ /** A refused restore, discriminated by `name` (`backupRestoreRefusalNames`). */
56
+ export type TBackupRestoreRefusal = IBackupRestoreSnapshotUnavailableRefusal | IBackupRestorePreflightUnsupportedRefusal | IBackupRestoreRelayContractBehindRefusal;
57
+ /** Validates one restore refusal against its exact schema and names every reason it is refused. */
58
+ export declare const validateBackupRestoreRefusal: (valueArg: unknown, pathArg?: string) => string[];
59
+ /** Whether a value is exactly one restore refusal. */
60
+ export declare const isBackupRestoreRefusal: (valueArg: unknown) => valueArg is TBackupRestoreRefusal;
61
+ /**
62
+ * The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
63
+ * or `null` when the value is not exactly one. It reads own data properties only, so a refusal
64
+ * read here carries nothing that runs.
65
+ */
66
+ export declare const readBackupRestoreRefusal: (valueArg: unknown) => TBackupRestoreRefusal | null;
67
+ /** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
68
+ export declare const buildBackupRestoreSnapshotUnavailableRefusal: (missingArg: IBackupRestoreMissingSnapshot[]) => IBackupRestoreSnapshotUnavailableRefusal;
69
+ /** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
70
+ export declare const buildBackupRestorePreflightUnsupportedRefusal: () => IBackupRestorePreflightUnsupportedRefusal;
71
+ /**
72
+ * `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
73
+ *
74
+ * Throws when a stated version is not a release version or is not below the minimum, so a caller
75
+ * passes `null` for a relay whose stated version it cannot read.
76
+ */
77
+ export declare const buildBackupRestoreRelayContractBehindRefusal: (relayInterfacesVersionArg: string | null, minimumInterfacesVersionArg: string) => IBackupRestoreRelayContractBehindRefusal;
78
+ /**
79
+ * Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
80
+ * every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
81
+ * present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
82
+ * it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
83
+ * `resourceTypes` they hold), so every stored event stays valid.
84
+ */
85
+ export declare const validateBackupRestoreEvent: (valueArg: unknown, pathArg?: string) => string[];
@@ -0,0 +1,228 @@
1
+ import { hasExactKeys, readDataElements, readDataProperties, readExactDataRecord, } from '../private/dataproperties.js';
2
+ import { compareReleaseVersions, parseReleaseVersion } from './releaseversion.js';
3
+ /**
4
+ * Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
5
+ * it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
6
+ * the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
7
+ * whose contract carries that check. A refusal therefore always means nothing was written; the
8
+ * service keeps the data it had.
9
+ *
10
+ * - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
11
+ * restore names; `missing` names each of them.
12
+ * - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
13
+ * Corestore built before it answers the check route with HTTP 404), so it cannot prove the
14
+ * restore would be whole and nothing is restored.
15
+ * - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
16
+ * that carries the restore check, so Cloudly does not dispatch the restore; the relay states
17
+ * `relayInterfacesVersion` (`null` when it stated none) and the restore needs
18
+ * `minimumInterfacesVersion`.
19
+ *
20
+ * It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
21
+ * `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
22
+ * exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
23
+ * one without gating it on the reader's interfaces version.
24
+ */
25
+ export const backupRestoreRefusalNames = Object.freeze([
26
+ 'restore-snapshot-unavailable',
27
+ 'restore-preflight-unsupported',
28
+ 'restore-relay-contract-behind',
29
+ ]);
30
+ export const backupRestoreRefusalContract = Object.freeze({
31
+ /** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
32
+ maximumMissingSnapshots: 256,
33
+ /** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
34
+ maximumNameLength: 255,
35
+ });
36
+ /** Every resource type, keyed so a new `TBackupResourceType` member fails to compile here. */
37
+ const backupResourceTypeKeys = Object.freeze({
38
+ volume: true,
39
+ database: true,
40
+ objectstorage: true,
41
+ });
42
+ const isBackupResourceType = (valueArg) => (typeof valueArg === 'string' && Object.hasOwn(backupResourceTypeKeys, valueArg));
43
+ const printableNamePattern = /^[\x21-\x7e]+$/;
44
+ const isRestoreName = (valueArg) => (typeof valueArg === 'string'
45
+ && valueArg.length <= backupRestoreRefusalContract.maximumNameLength
46
+ && printableNamePattern.test(valueArg));
47
+ const nameRule = 'must be printable ASCII without spaces, at most '
48
+ + `${backupRestoreRefusalContract.maximumNameLength} characters`;
49
+ const isRefusalName = (valueArg) => (typeof valueArg === 'string'
50
+ && backupRestoreRefusalNames.includes(valueArg));
51
+ const validateMissingSnapshot = (valueArg, pathArg) => {
52
+ const snapshot = readDataProperties(valueArg);
53
+ if (snapshot === null || !hasExactKeys(snapshot, ['snapshotId', 'capability'], ['resourceName'])) {
54
+ return [`${pathArg} must use its exact schema`];
55
+ }
56
+ const errors = [];
57
+ if (!isRestoreName(snapshot.snapshotId)) {
58
+ errors.push(`${pathArg}.snapshotId ${nameRule}`);
59
+ }
60
+ if (!isBackupResourceType(snapshot.capability)) {
61
+ errors.push(`${pathArg}.capability must be volume, database or objectstorage`);
62
+ }
63
+ if (Object.hasOwn(snapshot, 'resourceName') && !isRestoreName(snapshot.resourceName)) {
64
+ errors.push(`${pathArg}.resourceName ${nameRule}`);
65
+ }
66
+ return errors;
67
+ };
68
+ const validateMissingSnapshots = (valueArg, pathArg) => {
69
+ const elements = readDataElements(valueArg, backupRestoreRefusalContract.maximumMissingSnapshots);
70
+ if (elements === null) {
71
+ return [`${pathArg} must be a list of at most `
72
+ + `${backupRestoreRefusalContract.maximumMissingSnapshots} snapshots`];
73
+ }
74
+ if (elements.length === 0) {
75
+ return [`${pathArg} must name at least one snapshot`];
76
+ }
77
+ const errors = elements.flatMap((elementArg, indexArg) => (validateMissingSnapshot(elementArg, `${pathArg}[${indexArg}]`)));
78
+ if (errors.length > 0) {
79
+ return errors;
80
+ }
81
+ const snapshotIds = elements.map((elementArg) => elementArg.snapshotId);
82
+ if (new Set(snapshotIds).size !== snapshotIds.length) {
83
+ errors.push(`${pathArg} must name each snapshot once`);
84
+ }
85
+ return errors;
86
+ };
87
+ const validateRelayContractBehind = (refusalArg, pathArg) => {
88
+ const relay = refusalArg.relayInterfacesVersion === null
89
+ ? null
90
+ : parseReleaseVersion(refusalArg.relayInterfacesVersion);
91
+ const minimum = parseReleaseVersion(refusalArg.minimumInterfacesVersion);
92
+ const errors = [];
93
+ if (refusalArg.relayInterfacesVersion !== null && relay === null) {
94
+ errors.push(`${pathArg} relayInterfacesVersion must be a release version or null`);
95
+ }
96
+ if (minimum === null) {
97
+ errors.push(`${pathArg} minimumInterfacesVersion must be a release version`);
98
+ }
99
+ if (relay !== null && minimum !== null && compareReleaseVersions(relay, minimum) >= 0) {
100
+ errors.push(`${pathArg} relayInterfacesVersion must be below minimumInterfacesVersion`);
101
+ }
102
+ return errors;
103
+ };
104
+ /** The exact keys of each refusal. */
105
+ const refusalKeys = Object.freeze({
106
+ 'restore-snapshot-unavailable': Object.freeze(['name', 'missing']),
107
+ 'restore-preflight-unsupported': Object.freeze(['name']),
108
+ 'restore-relay-contract-behind': Object.freeze([
109
+ 'name', 'relayInterfacesVersion', 'minimumInterfacesVersion',
110
+ ]),
111
+ });
112
+ /** Validates one restore refusal against its exact schema and names every reason it is refused. */
113
+ export const validateBackupRestoreRefusal = (valueArg, pathArg = 'backup restore refusal') => {
114
+ const properties = readDataProperties(valueArg);
115
+ if (properties === null || !isRefusalName(properties.name)) {
116
+ return [`${pathArg} name must be one of ${backupRestoreRefusalNames.join(', ')}`];
117
+ }
118
+ const refusal = readExactDataRecord(valueArg, refusalKeys[properties.name]);
119
+ if (refusal === null) {
120
+ return [`${pathArg} must use the exact schema of ${properties.name}`];
121
+ }
122
+ switch (properties.name) {
123
+ case 'restore-snapshot-unavailable':
124
+ return validateMissingSnapshots(refusal.missing, `${pathArg} missing`);
125
+ case 'restore-preflight-unsupported':
126
+ return [];
127
+ case 'restore-relay-contract-behind':
128
+ return validateRelayContractBehind(refusal, pathArg);
129
+ }
130
+ };
131
+ /** Whether a value is exactly one restore refusal. */
132
+ export const isBackupRestoreRefusal = (valueArg) => (validateBackupRestoreRefusal(valueArg).length === 0);
133
+ const freezeMissingSnapshot = (snapshotArg) => Object.freeze({
134
+ snapshotId: snapshotArg.snapshotId,
135
+ capability: snapshotArg.capability,
136
+ ...(snapshotArg.resourceName === undefined ? {} : { resourceName: snapshotArg.resourceName }),
137
+ });
138
+ const copySnapshotUnavailableRefusal = (refusalArg) => {
139
+ const missing = refusalArg.missing.map(freezeMissingSnapshot);
140
+ Object.freeze(missing);
141
+ return Object.freeze({ name: refusalArg.name, missing });
142
+ };
143
+ const copyPreflightUnsupportedRefusal = (refusalArg) => Object.freeze({ name: refusalArg.name });
144
+ const copyRelayContractBehindRefusal = (refusalArg) => Object.freeze({
145
+ name: refusalArg.name,
146
+ relayInterfacesVersion: refusalArg.relayInterfacesVersion,
147
+ minimumInterfacesVersion: refusalArg.minimumInterfacesVersion,
148
+ });
149
+ /** An inert frozen copy of a refusal that passed `validateBackupRestoreRefusal`. */
150
+ const copyRefusal = (refusalArg) => {
151
+ switch (refusalArg.name) {
152
+ case 'restore-snapshot-unavailable':
153
+ return copySnapshotUnavailableRefusal(refusalArg);
154
+ case 'restore-preflight-unsupported':
155
+ return copyPreflightUnsupportedRefusal(refusalArg);
156
+ case 'restore-relay-contract-behind':
157
+ return copyRelayContractBehindRefusal(refusalArg);
158
+ }
159
+ };
160
+ /**
161
+ * The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
162
+ * or `null` when the value is not exactly one. It reads own data properties only, so a refusal
163
+ * read here carries nothing that runs.
164
+ */
165
+ export const readBackupRestoreRefusal = (valueArg) => (
166
+ // A value that passed holds own data properties of exactly the refusal's schema at every level,
167
+ // so the copy reads the same data the validation judged.
168
+ isBackupRestoreRefusal(valueArg) ? copyRefusal(valueArg) : null);
169
+ /** The refusal a builder composed, unchanged, or an error naming every reason it is refused. */
170
+ const requireRefusal = (refusalArg) => {
171
+ const errors = validateBackupRestoreRefusal(refusalArg);
172
+ if (errors.length > 0) {
173
+ throw new Error(errors.join('; '));
174
+ }
175
+ return refusalArg;
176
+ };
177
+ /** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
178
+ export const buildBackupRestoreSnapshotUnavailableRefusal = (missingArg) => copySnapshotUnavailableRefusal(requireRefusal({
179
+ name: 'restore-snapshot-unavailable',
180
+ missing: missingArg.map((snapshotArg) => ({ ...snapshotArg })),
181
+ }));
182
+ /** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
183
+ export const buildBackupRestorePreflightUnsupportedRefusal = () => (copyPreflightUnsupportedRefusal(requireRefusal({ name: 'restore-preflight-unsupported' })));
184
+ /**
185
+ * `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
186
+ *
187
+ * Throws when a stated version is not a release version or is not below the minimum, so a caller
188
+ * passes `null` for a relay whose stated version it cannot read.
189
+ */
190
+ export const buildBackupRestoreRelayContractBehindRefusal = (relayInterfacesVersionArg, minimumInterfacesVersionArg) => copyRelayContractBehindRefusal(requireRefusal({
191
+ name: 'restore-relay-contract-behind',
192
+ relayInterfacesVersion: relayInterfacesVersionArg,
193
+ minimumInterfacesVersion: minimumInterfacesVersionArg,
194
+ }));
195
+ const restoreEventStatuses = Object.freeze([
196
+ 'restored', 'failed', 'refused',
197
+ ]);
198
+ /**
199
+ * Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
200
+ * every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
201
+ * present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
202
+ * it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
203
+ * `resourceTypes` they hold), so every stored event stays valid.
204
+ */
205
+ export const validateBackupRestoreEvent = (valueArg, pathArg = 'backup restore event') => {
206
+ const event = readDataProperties(valueArg);
207
+ if (event === null) {
208
+ return [`${pathArg} must be a plain record of data properties`];
209
+ }
210
+ const errors = [];
211
+ if (!restoreEventStatuses.includes(event.status)) {
212
+ errors.push(`${pathArg} status must be one of ${restoreEventStatuses.join(', ')}`);
213
+ }
214
+ const hasRefusal = Object.hasOwn(event, 'refusal');
215
+ if (event.status === 'refused') {
216
+ if (!hasRefusal) {
217
+ errors.push(`${pathArg} refusal is required when status is refused`);
218
+ }
219
+ else {
220
+ errors.push(...validateBackupRestoreRefusal(event.refusal, `${pathArg} refusal`));
221
+ }
222
+ }
223
+ else if (hasRefusal) {
224
+ errors.push(`${pathArg} refusal is allowed only when status is refused`);
225
+ }
226
+ return errors;
227
+ };
228
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwcmVzdG9yZXJlZnVzYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2JhY2t1cHJlc3RvcmVyZWZ1c2FsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFDTCxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEdBQ3hFLE1BQU0sOEJBQThCLENBQUM7QUFDdEMsT0FBTyxFQUFFLHNCQUFzQixFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFbEY7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNILE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDckQsOEJBQThCO0lBQzlCLCtCQUErQjtJQUMvQiwrQkFBK0I7Q0FDdkIsQ0FBQyxDQUFDO0FBSVosTUFBTSxDQUFDLE1BQU0sNEJBQTRCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN4RCxtRkFBbUY7SUFDbkYsdUJBQXVCLEVBQUUsR0FBRztJQUM1QiwyRkFBMkY7SUFDM0YsaUJBQWlCLEVBQUUsR0FBRztDQUNkLENBQUMsQ0FBQztBQW1DWiw4RkFBOEY7QUFDOUYsTUFBTSxzQkFBc0IsR0FBZ0QsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN4RixNQUFNLEVBQUUsSUFBSTtJQUNaLFFBQVEsRUFBRSxJQUFJO0lBQ2QsYUFBYSxFQUFFLElBQUk7Q0FDcEIsQ0FBQyxDQUFDO0FBRUgsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLFFBQWlCLEVBQW1DLEVBQUUsQ0FBQyxDQUNuRixPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLE1BQU0sQ0FBQyxzQkFBc0IsRUFBRSxRQUFRLENBQUMsQ0FDaEYsQ0FBQztBQUVGLE1BQU0sb0JBQW9CLEdBQUcsZ0JBQWdCLENBQUM7QUFFOUMsTUFBTSxhQUFhLEdBQUcsQ0FBQyxRQUFpQixFQUFzQixFQUFFLENBQUMs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
@@ -75,6 +75,7 @@ export * from './secret.js';
75
75
  export * from './baremetal.js';
76
76
  export * from './baseos.js';
77
77
  export * from './backup.js';
78
+ export * from './backuprestorerefusal.js';
78
79
  export * from './clusternode.js';
79
80
  export * from './clusternodehostname.js';
80
81
  export * from './settings.js';
@@ -70,6 +70,7 @@ export * from './secret.js';
70
70
  export * from './baremetal.js';
71
71
  export * from './baseos.js';
72
72
  export * from './backup.js';
73
+ export * from './backuprestorerefusal.js';
73
74
  export * from './clusternode.js';
74
75
  export * from './clusternodehostname.js';
75
76
  export * from './settings.js';
@@ -91,4 +92,4 @@ export * from './user.js';
91
92
  export * from './version.js';
92
93
  export * from './webpush.js';
93
94
  export * from './workloadreplicaidentity.js';
94
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyx3QkFBd0IsQ0FBQztBQUl2QyxPQUFPLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUc5RCxPQUFPLEVBQUUscUNBQXFDLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUVwRixPQUFPLEVBQUUsMENBQTBDLEVBQUUsMENBQTBDLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUMxSSxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsZUFBZSxDQUFDO0FBQzlCLGNBQWMsNEJBQTRCLENBQUM7QUFDM0MsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMsdUJBQXVCLENBQUM7QUFDdEMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyx3QkFBd0IsQ0FBQztBQUN2QyxjQUFjLCtCQUErQixDQUFDO0FBQzlDLGNBQWMsbUJBQW1CLENBQUM7QUFDbEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsa0NBQWtDLENBQUM7QUFDakQsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLFdBQVcsQ0FBQztBQUMxQixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLDhCQUE4QixDQUFDIn0=
95
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxjQUFjLENBQUM7QUFDN0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDhCQUE4QixDQUFDO0FBQzdDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlCQUFpQixDQUFDO0FBQ2hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw2QkFBNkIsQ0FBQztBQUM1QyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYywwQkFBMEIsQ0FBQztBQUN6QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxVQUFVLENBQUM7QUFDekIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxZQUFZLENBQUM7QUFDM0IsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxXQUFXLENBQUM7QUFDMUIsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLHlCQUF5QixDQUFDO0FBQ3hDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsc0JBQXNCLENBQUM7QUFDckMsY0FBYyw4QkFBOEIsQ0FBQztBQUM3QyxjQUFjLG1CQUFtQixDQUFDO0FBQ2xDLGNBQWMsa0JBQWtCLENBQUM7QUFDakMsY0FBYyx3QkFBd0IsQ0FBQztBQUl2QyxPQUFPLEVBQUUsMEJBQTBCLEVBQUUsTUFBTSxrQkFBa0IsQ0FBQztBQUc5RCxPQUFPLEVBQUUscUNBQXFDLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUVwRixPQUFPLEVBQUUsMENBQTBDLEVBQUUsMENBQTBDLEVBQUUsTUFBTSxrQ0FBa0MsQ0FBQztBQUMxSSxjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMscUJBQXFCLENBQUM7QUFDcEMsY0FBYyxxQkFBcUIsQ0FBQztBQUNwQyxjQUFjLGVBQWUsQ0FBQztBQUM5QixjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsd0JBQXdCLENBQUM7QUFDdkMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDRCQUE0QixDQUFDO0FBQzNDLGNBQWMsNkJBQTZCLENBQUM7QUFDNUMsY0FBYyxvQkFBb0IsQ0FBQztBQUNuQyxjQUFjLDBCQUEwQixDQUFDO0FBQ3pDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxnQ0FBZ0MsQ0FBQztBQUMvQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMseUJBQXlCLENBQUM7QUFDeEMsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLDZCQUE2QixDQUFDO0FBQzVDLGNBQWMsYUFBYSxDQUFDO0FBQzVCLGNBQWMsZ0JBQWdCLENBQUM7QUFDL0IsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYyxhQUFhLENBQUM7QUFDNUIsY0FBYywyQkFBMkIsQ0FBQztBQUMxQyxjQUFjLGtCQUFrQixDQUFDO0FBQ2pDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxlQUFlLENBQUM7QUFDOUIsY0FBYyw0QkFBNEIsQ0FBQztBQUMzQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGNBQWMsQ0FBQztBQUM3QixjQUFjLHdCQUF3QixDQUFDO0FBQ3ZDLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLHFCQUFxQixDQUFDO0FBQ3BDLGNBQWMsMEJBQTBCLENBQUM7QUFDekMsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLGFBQWEsQ0FBQztBQUM1QixjQUFjLG9CQUFvQixDQUFDO0FBQ25DLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsV0FBVyxDQUFDO0FBQzFCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsY0FBYyxDQUFDO0FBQzdCLGNBQWMsOEJBQThCLENBQUMifQ==
@@ -45,6 +45,14 @@ export interface IReq_Any_Cloudly_RestoreServiceBackup extends plugins.typedrequ
45
45
  backupId: string;
46
46
  clear?: boolean;
47
47
  resourceTypes?: Array<TBackupSnapshot['type']>;
48
+ /**
49
+ * Must be `true` to restore a `failed` backup whose failure is `incomplete`, and is refused for
50
+ * any other backup. Such a restore restores what the backup captured: `resourceTypes` is
51
+ * required and names only types it captured, none of the resources it did not capture
52
+ * (`failure.uncaptured`) is restored, so the service then holds data from two points in time,
53
+ * and the backup stays `failed`.
54
+ */
55
+ confirmPartialRestore?: boolean;
48
56
  };
49
57
  response: {
50
58
  backup: IBackupRecord;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.69.0",
3
+ "version": "32.71.0",
4
4
  "private": false,
5
5
  "description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
6
6
  "exports": {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.69.0',
6
+ version: '32.71.0',
7
7
  description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
8
8
  }
package/ts/data/backup.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { TBackupRestoreRefusal } from './backuprestorerefusal.js';
1
2
  import type { ICorestoreDatabaseAllocationReference } from './corestore.js';
2
3
 
3
4
  export type TBackupStatus =
@@ -72,10 +73,56 @@ export interface IBackupReplicationResult {
72
73
  completedAt: number;
73
74
  }
74
75
 
76
+ /**
77
+ * One resource a service declares that a backup of it did not capture, with why; each kind of
78
+ * resource has its own reason.
79
+ *
80
+ * `volume-capture-unavailable`: a service volume is a node-local storage claim on its Pallet node,
81
+ * which no relay can capture yet; a `volume` snapshot is a Corestore Docker volume and captures no
82
+ * service volume. `binding-not-snapshotted`: the backup holds no snapshot of the Corestore resource
83
+ * the binding names.
84
+ */
85
+ export type TBackupUncapturedResource =
86
+ | { type: 'volume'; mountPath: string; reason: 'volume-capture-unavailable' }
87
+ | { type: 'database' | 'objectstorage'; bindingId: string; reason: 'binding-not-snapshotted' };
88
+
89
+ /** Why a resource a service declares is not in one of its backups (`TBackupUncapturedResource`). */
90
+ export type TBackupUncapturedReason = TBackupUncapturedResource['reason'];
91
+
92
+ /**
93
+ * The named kind of a `failed` backup's failure; a failure without one is named only in `errorText`.
94
+ *
95
+ * `incomplete`: the backup did not capture every resource its service declares, and `uncaptured`
96
+ * names each one it missed. What it did capture is restored only by a partial restore an
97
+ * administrator confirms (`IReq_Any_Cloudly_RestoreServiceBackup` `confirmPartialRestore`), which
98
+ * restores none of `uncaptured`, and the backup stays `failed`.
99
+ * `archive-unreadable`: its stored archive is missing or damaged, so it restores nothing.
100
+ */
101
+ export type TBackupFailure =
102
+ | { kind: 'incomplete'; uncaptured: TBackupUncapturedResource[] }
103
+ | { kind: 'archive-unreadable' };
104
+
105
+ /**
106
+ * One restore of a backup, appended to its `restoreHistory`; `validateBackupRestoreEvent` judges its
107
+ * `status` and `refusal`.
108
+ *
109
+ * `refused`: the restore was refused before it wrote anything (`refusal` says why), so the service
110
+ * keeps the data it had. `failed`: it failed after it began writing.
111
+ */
75
112
  export interface IBackupRestoreEvent {
113
+ /** When the restore ended, or when it was refused. */
76
114
  restoredAt: number;
77
- status: 'restored' | 'failed';
115
+ status: 'restored' | 'failed' | 'refused';
78
116
  errorText?: string;
117
+ /** The resource types the restore was limited to; absent when it restored every snapshot. */
118
+ resourceTypes?: TBackupResourceType[];
119
+ /**
120
+ * A confirmed partial restore of an `incomplete` backup: it restored only `resourceTypes`, and
121
+ * none of the backup's `failure.uncaptured`.
122
+ */
123
+ partial?: true;
124
+ /** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
125
+ refusal?: TBackupRestoreRefusal;
79
126
  }
80
127
 
81
128
  /**
@@ -114,6 +161,8 @@ export interface IBackupRecord {
114
161
  completedAt?: number;
115
162
  requestedBy?: string;
116
163
  errorText?: string;
164
+ /** The named kind of a `failed` backup's failure (`TBackupFailure`). */
165
+ failure?: TBackupFailure;
117
166
  restoreHistory?: IBackupRestoreEvent[];
118
167
  /**
119
168
  * Authoritative isolated-restore holds. Cloudly atomically appends the
@@ -124,6 +173,25 @@ export interface IBackupRecord {
124
173
  tags?: Record<string, string>;
125
174
  }
126
175
 
176
+ /**
177
+ * The resource types a partial restore of a backup may name (`confirmPartialRestore`).
178
+ *
179
+ * For a `failed` backup whose failure is `incomplete`: every type it holds a snapshot of, except
180
+ * `volume` (a `volume` snapshot is a Corestore Docker volume and captures no service volume) and the
181
+ * type of any resource it did not capture, so a partial restore never restores a snapshot standing
182
+ * for a resource the backup missed. For every other backup: none. Database before object storage.
183
+ */
184
+ export const readPartialRestoreResourceTypes = (
185
+ backupArg: Pick<IBackupRecord, 'status' | 'failure'> & { snapshots: ReadonlyArray<Pick<TBackupSnapshot, 'type'>> },
186
+ ): Array<Exclude<TBackupResourceType, 'volume'>> => {
187
+ const failure = backupArg.failure;
188
+ if (backupArg.status !== 'failed' || failure?.kind !== 'incomplete') return [];
189
+ const uncapturedTypes = new Set<TBackupResourceType>(failure.uncaptured.map((resourceArg) => resourceArg.type));
190
+ return (['database', 'objectstorage'] as const).filter((typeArg) => (
191
+ !uncapturedTypes.has(typeArg) && backupArg.snapshots.some((snapshotArg) => snapshotArg.type === typeArg)
192
+ ));
193
+ };
194
+
127
195
  /** Retention policy understood by containerarchive prune. */
128
196
  export interface IArchiveRetentionPolicy {
129
197
  keepLast?: number;
@@ -0,0 +1,324 @@
1
+ import type { IBackupRestoreEvent, TBackupResourceType } from './backup.js';
2
+ import {
3
+ hasExactKeys, readDataElements, readDataProperties, readExactDataRecord,
4
+ } from '../private/dataproperties.js';
5
+ import { compareReleaseVersions, parseReleaseVersion } from './releaseversion.js';
6
+
7
+ /**
8
+ * Why an in-place restore of a backup (`IReq_Any_Cloudly_RestoreServiceBackup`) was refused before
9
+ * it wrote anything. A restore is all-or-nothing: Corestore checks that it can read every snapshot
10
+ * the restore names before it replaces any data, and Cloudly dispatches the restore only to a relay
11
+ * whose contract carries that check. A refusal therefore always means nothing was written; the
12
+ * service keeps the data it had.
13
+ *
14
+ * - `restore-snapshot-unavailable`: the node's Corestore cannot read one or more snapshots the
15
+ * restore names; `missing` names each of them.
16
+ * - `restore-preflight-unsupported`: the node's Corestore does not offer the restore check (a
17
+ * Corestore built before it answers the check route with HTTP 404), so it cannot prove the
18
+ * restore would be whole and nothing is restored.
19
+ * - `restore-relay-contract-behind`: the cluster relay speaks an interfaces release below the one
20
+ * that carries the restore check, so Cloudly does not dispatch the restore; the relay states
21
+ * `relayInterfacesVersion` (`null` when it stated none) and the restore needs
22
+ * `minimumInterfacesVersion`.
23
+ *
24
+ * It travels as `TypedResponseError.errorData` of a refused restore and is recorded as the
25
+ * `refusal` of the backup's `refused` restore event (`IBackupRestoreEvent`). Each refusal is
26
+ * exactly its schema: `readBackupRestoreRefusal` refuses any other member, so a producer never adds
27
+ * one without gating it on the reader's interfaces version.
28
+ */
29
+ export const backupRestoreRefusalNames = Object.freeze([
30
+ 'restore-snapshot-unavailable',
31
+ 'restore-preflight-unsupported',
32
+ 'restore-relay-contract-behind',
33
+ ] as const);
34
+
35
+ export type TBackupRestoreRefusalName = typeof backupRestoreRefusalNames[number];
36
+
37
+ export const backupRestoreRefusalContract = Object.freeze({
38
+ /** At most this many snapshots are named in one `restore-snapshot-unavailable`. */
39
+ maximumMissingSnapshots: 256,
40
+ /** A snapshot id or resource name is printable ASCII without spaces, at most this long. */
41
+ maximumNameLength: 255,
42
+ } as const);
43
+
44
+ /** One snapshot a restore names that the node's Corestore cannot read. */
45
+ export interface IBackupRestoreMissingSnapshot {
46
+ snapshotId: string;
47
+ /** The snapshot's resource type (`TBackupSnapshot['type']`). */
48
+ capability: TBackupResourceType;
49
+ /** The Corestore resource of a database or object-storage snapshot, when it is known. */
50
+ resourceName?: string;
51
+ }
52
+
53
+ export interface IBackupRestoreSnapshotUnavailableRefusal {
54
+ name: 'restore-snapshot-unavailable';
55
+ /** Non-empty; each snapshot once. */
56
+ missing: IBackupRestoreMissingSnapshot[];
57
+ }
58
+
59
+ export interface IBackupRestorePreflightUnsupportedRefusal {
60
+ name: 'restore-preflight-unsupported';
61
+ }
62
+
63
+ export interface IBackupRestoreRelayContractBehindRefusal {
64
+ name: 'restore-relay-contract-behind';
65
+ /** The interfaces release the relay speaks, or `null` when it stated none. */
66
+ relayInterfacesVersion: string | null;
67
+ /** The lowest interfaces release a relay must speak to carry a restore. */
68
+ minimumInterfacesVersion: string;
69
+ }
70
+
71
+ /** A refused restore, discriminated by `name` (`backupRestoreRefusalNames`). */
72
+ export type TBackupRestoreRefusal =
73
+ | IBackupRestoreSnapshotUnavailableRefusal
74
+ | IBackupRestorePreflightUnsupportedRefusal
75
+ | IBackupRestoreRelayContractBehindRefusal;
76
+
77
+ /** Every resource type, keyed so a new `TBackupResourceType` member fails to compile here. */
78
+ const backupResourceTypeKeys: Readonly<Record<TBackupResourceType, true>> = Object.freeze({
79
+ volume: true,
80
+ database: true,
81
+ objectstorage: true,
82
+ });
83
+
84
+ const isBackupResourceType = (valueArg: unknown): valueArg is TBackupResourceType => (
85
+ typeof valueArg === 'string' && Object.hasOwn(backupResourceTypeKeys, valueArg)
86
+ );
87
+
88
+ const printableNamePattern = /^[\x21-\x7e]+$/;
89
+
90
+ const isRestoreName = (valueArg: unknown): valueArg is string => (
91
+ typeof valueArg === 'string'
92
+ && valueArg.length <= backupRestoreRefusalContract.maximumNameLength
93
+ && printableNamePattern.test(valueArg)
94
+ );
95
+
96
+ const nameRule = 'must be printable ASCII without spaces, at most '
97
+ + `${backupRestoreRefusalContract.maximumNameLength} characters`;
98
+
99
+ const isRefusalName = (valueArg: unknown): valueArg is TBackupRestoreRefusalName => (
100
+ typeof valueArg === 'string'
101
+ && (backupRestoreRefusalNames as readonly string[]).includes(valueArg)
102
+ );
103
+
104
+ const validateMissingSnapshot = (valueArg: unknown, pathArg: string): string[] => {
105
+ const snapshot = readDataProperties(valueArg);
106
+ if (snapshot === null || !hasExactKeys(snapshot, ['snapshotId', 'capability'], ['resourceName'])) {
107
+ return [`${pathArg} must use its exact schema`];
108
+ }
109
+ const errors: string[] = [];
110
+ if (!isRestoreName(snapshot.snapshotId)) {
111
+ errors.push(`${pathArg}.snapshotId ${nameRule}`);
112
+ }
113
+ if (!isBackupResourceType(snapshot.capability)) {
114
+ errors.push(`${pathArg}.capability must be volume, database or objectstorage`);
115
+ }
116
+ if (Object.hasOwn(snapshot, 'resourceName') && !isRestoreName(snapshot.resourceName)) {
117
+ errors.push(`${pathArg}.resourceName ${nameRule}`);
118
+ }
119
+ return errors;
120
+ };
121
+
122
+ const validateMissingSnapshots = (valueArg: unknown, pathArg: string): string[] => {
123
+ const elements = readDataElements(valueArg, backupRestoreRefusalContract.maximumMissingSnapshots);
124
+ if (elements === null) {
125
+ return [`${pathArg} must be a list of at most `
126
+ + `${backupRestoreRefusalContract.maximumMissingSnapshots} snapshots`];
127
+ }
128
+ if (elements.length === 0) {
129
+ return [`${pathArg} must name at least one snapshot`];
130
+ }
131
+ const errors = elements.flatMap((elementArg, indexArg) => (
132
+ validateMissingSnapshot(elementArg, `${pathArg}[${indexArg}]`)
133
+ ));
134
+ if (errors.length > 0) {
135
+ return errors;
136
+ }
137
+ const snapshotIds = elements.map((elementArg) => (elementArg as IBackupRestoreMissingSnapshot).snapshotId);
138
+ if (new Set(snapshotIds).size !== snapshotIds.length) {
139
+ errors.push(`${pathArg} must name each snapshot once`);
140
+ }
141
+ return errors;
142
+ };
143
+
144
+ const validateRelayContractBehind = (
145
+ refusalArg: Record<string, unknown>,
146
+ pathArg: string,
147
+ ): string[] => {
148
+ const relay = refusalArg.relayInterfacesVersion === null
149
+ ? null
150
+ : parseReleaseVersion(refusalArg.relayInterfacesVersion);
151
+ const minimum = parseReleaseVersion(refusalArg.minimumInterfacesVersion);
152
+ const errors: string[] = [];
153
+ if (refusalArg.relayInterfacesVersion !== null && relay === null) {
154
+ errors.push(`${pathArg} relayInterfacesVersion must be a release version or null`);
155
+ }
156
+ if (minimum === null) {
157
+ errors.push(`${pathArg} minimumInterfacesVersion must be a release version`);
158
+ }
159
+ if (relay !== null && minimum !== null && compareReleaseVersions(relay, minimum) >= 0) {
160
+ errors.push(`${pathArg} relayInterfacesVersion must be below minimumInterfacesVersion`);
161
+ }
162
+ return errors;
163
+ };
164
+
165
+ /** The exact keys of each refusal. */
166
+ const refusalKeys: Readonly<Record<TBackupRestoreRefusalName, readonly string[]>> = Object.freeze({
167
+ 'restore-snapshot-unavailable': Object.freeze(['name', 'missing']),
168
+ 'restore-preflight-unsupported': Object.freeze(['name']),
169
+ 'restore-relay-contract-behind': Object.freeze([
170
+ 'name', 'relayInterfacesVersion', 'minimumInterfacesVersion',
171
+ ]),
172
+ });
173
+
174
+ /** Validates one restore refusal against its exact schema and names every reason it is refused. */
175
+ export const validateBackupRestoreRefusal = (
176
+ valueArg: unknown,
177
+ pathArg = 'backup restore refusal',
178
+ ): string[] => {
179
+ const properties = readDataProperties(valueArg);
180
+ if (properties === null || !isRefusalName(properties.name)) {
181
+ return [`${pathArg} name must be one of ${backupRestoreRefusalNames.join(', ')}`];
182
+ }
183
+ const refusal = readExactDataRecord(valueArg, refusalKeys[properties.name]);
184
+ if (refusal === null) {
185
+ return [`${pathArg} must use the exact schema of ${properties.name}`];
186
+ }
187
+ switch (properties.name) {
188
+ case 'restore-snapshot-unavailable':
189
+ return validateMissingSnapshots(refusal.missing, `${pathArg} missing`);
190
+ case 'restore-preflight-unsupported':
191
+ return [];
192
+ case 'restore-relay-contract-behind':
193
+ return validateRelayContractBehind(refusal, pathArg);
194
+ }
195
+ };
196
+
197
+ /** Whether a value is exactly one restore refusal. */
198
+ export const isBackupRestoreRefusal = (valueArg: unknown): valueArg is TBackupRestoreRefusal => (
199
+ validateBackupRestoreRefusal(valueArg).length === 0
200
+ );
201
+
202
+ const freezeMissingSnapshot = (
203
+ snapshotArg: IBackupRestoreMissingSnapshot,
204
+ ): IBackupRestoreMissingSnapshot => Object.freeze({
205
+ snapshotId: snapshotArg.snapshotId,
206
+ capability: snapshotArg.capability,
207
+ ...(snapshotArg.resourceName === undefined ? {} : { resourceName: snapshotArg.resourceName }),
208
+ });
209
+
210
+ const copySnapshotUnavailableRefusal = (
211
+ refusalArg: IBackupRestoreSnapshotUnavailableRefusal,
212
+ ): IBackupRestoreSnapshotUnavailableRefusal => {
213
+ const missing = refusalArg.missing.map(freezeMissingSnapshot);
214
+ Object.freeze(missing);
215
+ return Object.freeze({ name: refusalArg.name, missing });
216
+ };
217
+
218
+ const copyPreflightUnsupportedRefusal = (
219
+ refusalArg: IBackupRestorePreflightUnsupportedRefusal,
220
+ ): IBackupRestorePreflightUnsupportedRefusal => Object.freeze({ name: refusalArg.name });
221
+
222
+ const copyRelayContractBehindRefusal = (
223
+ refusalArg: IBackupRestoreRelayContractBehindRefusal,
224
+ ): IBackupRestoreRelayContractBehindRefusal => Object.freeze({
225
+ name: refusalArg.name,
226
+ relayInterfacesVersion: refusalArg.relayInterfacesVersion,
227
+ minimumInterfacesVersion: refusalArg.minimumInterfacesVersion,
228
+ });
229
+
230
+ /** An inert frozen copy of a refusal that passed `validateBackupRestoreRefusal`. */
231
+ const copyRefusal = (refusalArg: TBackupRestoreRefusal): TBackupRestoreRefusal => {
232
+ switch (refusalArg.name) {
233
+ case 'restore-snapshot-unavailable':
234
+ return copySnapshotUnavailableRefusal(refusalArg);
235
+ case 'restore-preflight-unsupported':
236
+ return copyPreflightUnsupportedRefusal(refusalArg);
237
+ case 'restore-relay-contract-behind':
238
+ return copyRelayContractBehindRefusal(refusalArg);
239
+ }
240
+ };
241
+
242
+ /**
243
+ * The restore refusal inside an error payload or a stored restore event, as an inert frozen copy,
244
+ * or `null` when the value is not exactly one. It reads own data properties only, so a refusal
245
+ * read here carries nothing that runs.
246
+ */
247
+ export const readBackupRestoreRefusal = (valueArg: unknown): TBackupRestoreRefusal | null => (
248
+ // A value that passed holds own data properties of exactly the refusal's schema at every level,
249
+ // so the copy reads the same data the validation judged.
250
+ isBackupRestoreRefusal(valueArg) ? copyRefusal(valueArg) : null
251
+ );
252
+
253
+ /** The refusal a builder composed, unchanged, or an error naming every reason it is refused. */
254
+ const requireRefusal = <T extends TBackupRestoreRefusal>(refusalArg: T): T => {
255
+ const errors = validateBackupRestoreRefusal(refusalArg);
256
+ if (errors.length > 0) {
257
+ throw new Error(errors.join('; '));
258
+ }
259
+ return refusalArg;
260
+ };
261
+
262
+ /** `restore-snapshot-unavailable` naming each snapshot the node's Corestore cannot read. */
263
+ export const buildBackupRestoreSnapshotUnavailableRefusal = (
264
+ missingArg: IBackupRestoreMissingSnapshot[],
265
+ ): IBackupRestoreSnapshotUnavailableRefusal => copySnapshotUnavailableRefusal(requireRefusal({
266
+ name: 'restore-snapshot-unavailable',
267
+ missing: missingArg.map((snapshotArg) => ({ ...snapshotArg })),
268
+ }));
269
+
270
+ /** `restore-preflight-unsupported`: the node's Corestore offers no restore check. */
271
+ export const buildBackupRestorePreflightUnsupportedRefusal = (): IBackupRestorePreflightUnsupportedRefusal => (
272
+ copyPreflightUnsupportedRefusal(requireRefusal({ name: 'restore-preflight-unsupported' }))
273
+ );
274
+
275
+ /**
276
+ * `restore-relay-contract-behind`: the relay speaks an interfaces release below the minimum.
277
+ *
278
+ * Throws when a stated version is not a release version or is not below the minimum, so a caller
279
+ * passes `null` for a relay whose stated version it cannot read.
280
+ */
281
+ export const buildBackupRestoreRelayContractBehindRefusal = (
282
+ relayInterfacesVersionArg: string | null,
283
+ minimumInterfacesVersionArg: string,
284
+ ): IBackupRestoreRelayContractBehindRefusal => copyRelayContractBehindRefusal(requireRefusal({
285
+ name: 'restore-relay-contract-behind',
286
+ relayInterfacesVersion: relayInterfacesVersionArg,
287
+ minimumInterfacesVersion: minimumInterfacesVersionArg,
288
+ }));
289
+
290
+ const restoreEventStatuses: readonly IBackupRestoreEvent['status'][] = Object.freeze([
291
+ 'restored', 'failed', 'refused',
292
+ ]);
293
+
294
+ /**
295
+ * Validates what this contract adds to a restore event of a backup's `restoreHistory` and names
296
+ * every reason it is refused: `status` is `restored`, `failed` or `refused`, and `refusal` is
297
+ * present exactly when `status` is `refused`, as one restore refusal. Every other member is left as
298
+ * it was written (records Cloudly already stores keep any `restoredAt`, `errorText`, `partial` and
299
+ * `resourceTypes` they hold), so every stored event stays valid.
300
+ */
301
+ export const validateBackupRestoreEvent = (
302
+ valueArg: unknown,
303
+ pathArg = 'backup restore event',
304
+ ): string[] => {
305
+ const event = readDataProperties(valueArg);
306
+ if (event === null) {
307
+ return [`${pathArg} must be a plain record of data properties`];
308
+ }
309
+ const errors: string[] = [];
310
+ if (!(restoreEventStatuses as readonly unknown[]).includes(event.status)) {
311
+ errors.push(`${pathArg} status must be one of ${restoreEventStatuses.join(', ')}`);
312
+ }
313
+ const hasRefusal = Object.hasOwn(event, 'refusal');
314
+ if (event.status === 'refused') {
315
+ if (!hasRefusal) {
316
+ errors.push(`${pathArg} refusal is required when status is refused`);
317
+ } else {
318
+ errors.push(...validateBackupRestoreRefusal(event.refusal, `${pathArg} refusal`));
319
+ }
320
+ } else if (hasRefusal) {
321
+ errors.push(`${pathArg} refusal is allowed only when status is refused`);
322
+ }
323
+ return errors;
324
+ };
package/ts/data/index.ts CHANGED
@@ -76,6 +76,7 @@ export * from './secret.js';
76
76
  export * from './baremetal.js';
77
77
  export * from './baseos.js';
78
78
  export * from './backup.js';
79
+ export * from './backuprestorerefusal.js';
79
80
  export * from './clusternode.js';
80
81
  export * from './clusternodehostname.js';
81
82
  export * from './settings.js';
@@ -81,6 +81,14 @@ extends plugins.typedrequestInterfaces.implementsTR<
81
81
  backupId: string;
82
82
  clear?: boolean;
83
83
  resourceTypes?: Array<TBackupSnapshot['type']>;
84
+ /**
85
+ * Must be `true` to restore a `failed` backup whose failure is `incomplete`, and is refused for
86
+ * any other backup. Such a restore restores what the backup captured: `resourceTypes` is
87
+ * required and names only types it captured, none of the resources it did not capture
88
+ * (`failure.uncaptured`) is restored, so the service then holds data from two points in time,
89
+ * and the backup stays `failed`.
90
+ */
91
+ confirmPartialRestore?: boolean;
84
92
  };
85
93
  response: {
86
94
  backup: IBackupRecord;