@serve.zone/interfaces 32.70.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,11 @@
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
+
3
9
  ## 2026-11-05 - 32.70.0
4
10
 
5
11
  ### Features
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/interfaces',
6
- version: '32.70.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';
@@ -83,9 +84,17 @@ export type TBackupFailure = {
83
84
  } | {
84
85
  kind: 'archive-unreadable';
85
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
+ */
86
94
  export interface IBackupRestoreEvent {
95
+ /** When the restore ended, or when it was refused. */
87
96
  restoredAt: number;
88
- status: 'restored' | 'failed';
97
+ status: 'restored' | 'failed' | 'refused';
89
98
  errorText?: string;
90
99
  /** The resource types the restore was limited to; absent when it restored every snapshot. */
91
100
  resourceTypes?: TBackupResourceType[];
@@ -94,6 +103,8 @@ export interface IBackupRestoreEvent {
94
103
  * none of the backup's `failure.uncaptured`.
95
104
  */
96
105
  partial?: true;
106
+ /** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
107
+ refusal?: TBackupRestoreRefusal;
97
108
  }
98
109
  /**
99
110
  * Durable retention hold owned by one isolated restore. An entry is active
@@ -13,4 +13,4 @@ export const readPartialRestoreResourceTypes = (backupArg) => {
13
13
  const uncapturedTypes = new Set(failure.uncaptured.map((resourceArg) => resourceArg.type));
14
14
  return ['database', 'objectstorage'].filter((typeArg) => (!uncapturedTypes.has(typeArg) && backupArg.snapshots.some((snapshotArg) => snapshotArg.type === typeArg)));
15
15
  };
16
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvZGF0YS9iYWNrdXAudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBb0tBOzs7Ozs7O0dBT0c7QUFDSCxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRyxDQUM3QyxTQUFrSCxFQUNuRSxFQUFFO0lBQ2pELE1BQU0sT0FBTyxHQUFHLFNBQVMsQ0FBQyxPQUFPLENBQUM7SUFDbEMsSUFBSSxTQUFTLENBQUMsTUFBTSxLQUFLLFFBQVEsSUFBSSxPQUFPLEVBQUUsSUFBSSxLQUFLLFlBQVk7UUFBRSxPQUFPLEVBQUUsQ0FBQztJQUMvRSxNQUFNLGVBQWUsR0FBRyxJQUFJLEdBQUcsQ0FBc0IsT0FBTyxDQUFDLFVBQVUsQ0FBQyxHQUFHLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQ2hILE9BQVEsQ0FBQyxVQUFVLEVBQUUsZUFBZSxDQUFXLENBQUMsTUFBTSxDQUFDLENBQUMsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUNsRSxDQUFDLGVBQWUsQ0FBQyxHQUFHLENBQUMsT0FBTyxDQUFDLElBQUksU0FBUyxDQUFDLFNBQVMsQ0FBQyxJQUFJLENBQUMsQ0FBQyxXQUFXLEVBQUUsRUFBRSxDQUFDLFdBQVcsQ0FBQyxJQUFJLEtBQUssT0FBTyxDQUFDLENBQ3pHLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyJ9
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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYmFja3VwcmVzdG9yZXJlZnVzYWwuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi90cy9kYXRhL2JhY2t1cHJlc3RvcmVyZWZ1c2FsLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUNBLE9BQU8sRUFDTCxZQUFZLEVBQUUsZ0JBQWdCLEVBQUUsa0JBQWtCLEVBQUUsbUJBQW1CLEdBQ3hFLE1BQU0sOEJBQThCLENBQUM7QUFDdEMsT0FBTyxFQUFFLHNCQUFzQixFQUFFLG1CQUFtQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFbEY7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQXFCRztBQUNILE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDckQsOEJBQThCO0lBQzlCLCtCQUErQjtJQUMvQiwrQkFBK0I7Q0FDdkIsQ0FBQyxDQUFDO0FBSVosTUFBTSxDQUFDLE1BQU0sNEJBQTRCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN4RCxtRkFBbUY7SUFDbkYsdUJBQXVCLEVBQUUsR0FBRztJQUM1QiwyRkFBMkY7SUFDM0YsaUJBQWlCLEVBQUUsR0FBRztDQUNkLENBQUMsQ0FBQztBQW1DWiw4RkFBOEY7QUFDOUYsTUFBTSxzQkFBc0IsR0FBZ0QsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN4RixNQUFNLEVBQUUsSUFBSTtJQUNaLFFBQVEsRUFBRSxJQUFJO0lBQ2QsYUFBYSxFQUFFLElBQUk7Q0FDcEIsQ0FBQyxDQUFDO0FBRUgsTUFBTSxvQkFBb0IsR0FBRyxDQUFDLFFBQWlCLEVBQW1DLEVBQUUsQ0FBQyxDQUNuRixPQUFPLFFBQVEsS0FBSyxRQUFRLElBQUksTUFBTSxDQUFDLE1BQU0sQ0FBQyxzQkFBc0IsRUFBRSxRQUFRLENBQUMsQ0FDaEYsQ0FBQztBQUVGLE1BQU0sb0JBQW9CLEdBQUcsZ0JBQWdCLENBQUM7QUFFOUMsTUFBTSxhQUFhLEdBQUcsQ0FBQyxRQUFpQixFQUFzQixFQUFFLENBQUMsQ0FDL0QsT0FBTyxRQUFRLEtBQUssUUFBUTtPQUN6QixRQUFRLENBQUMsTUFBTSxJQUFJLDRCQUE0QixDQUFDLGlCQUFpQjtPQUNqRSxvQkFBb0IsQ0FBQyxJQUFJLENBQUMsUUFBUSxDQUFDLENBQ3ZDLENBQUM7QUFFRixNQUFNLFFBQVEsR0FBRyxrREFBa0Q7TUFDL0QsR0FBRyw0QkFBNEIsQ0FBQyxpQkFBaUIsYUFBYSxDQUFDO0FBRW5FLE1BQU0sYUFBYSxHQUFHLENBQUMsUUFBaUIsRUFBeUMsRUFBRSxDQUFDLENBQ2xGLE9BQU8sUUFBUSxLQUFLLFFBQVE7T0FDeEIseUJBQStDLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxDQUN2RSxDQUFDO0FBRUYsTUFBTSx1QkFBdUIsR0FBRyxDQUFDLFFBQWlCLEVBQUUsT0FBZSxFQUFZLEVBQUU7SUFDL0UsTUFBTSxRQUFRLEdBQUcsa0JBQWtCLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDOUMsSUFBSSxRQUFRLEtBQUssSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLFFBQVEsRUFBRSxDQUFDLFlBQVksRUFBRSxZQUFZLENBQUMsRUFBRSxDQUFDLGNBQWMsQ0FBQyxDQUFDLEVBQUUsQ0FBQztRQUNqRyxPQUFPLENBQUMsR0FBRyxPQUFPLDRCQUE0QixDQUFDLENBQUM7SUFDbEQsQ0FBQztJQUNELE1BQU0sTUFBTSxHQUFhLEVBQUUsQ0FBQztJQUM1QixJQUFJLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDO1FBQ3hDLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLGVBQWUsUUFBUSxFQUFFLENBQUMsQ0FBQztJQUNuRCxDQUFDO0lBQ0QsSUFBSSxDQUFDLG9CQUFvQixDQUFDLFFBQVEsQ0FBQyxVQUFVLENBQUMsRUFBRSxDQUFDO1FBQy9DLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLHVEQUF1RCxDQUFDLENBQUM7SUFDakYsQ0FBQztJQUNELElBQUksTUFBTSxDQUFDLE1BQU0sQ0FBQyxRQUFRLEVBQUUsY0FBYyxDQUFDLElBQUksQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxFQUFFLENBQUM7UUFDckYsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8saUJBQWlCLFFBQVEsRUFBRSxDQUFDLENBQUM7SUFDckQsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMsQ0FBQztBQUVGLE1BQU0sd0JBQXdCLEdBQUcsQ0FBQyxRQUFpQixFQUFFLE9BQWUsRUFBWSxFQUFFO0lBQ2hGLE1BQU0sUUFBUSxHQUFHLGdCQUFnQixDQUFDLFFBQVEsRUFBRSw0QkFBNEIsQ0FBQyx1QkFBdUIsQ0FBQyxDQUFDO0lBQ2xHLElBQUksUUFBUSxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ3RCLE9BQU8sQ0FBQyxHQUFHLE9BQU8sNkJBQTZCO2tCQUMzQyxHQUFHLDRCQUE0QixDQUFDLHVCQUF1QixZQUFZLENBQUMsQ0FBQztJQUMzRSxDQUFDO0lBQ0QsSUFBSSxRQUFRLENBQUMsTUFBTSxLQUFLLENBQUMsRUFBRSxDQUFDO1FBQzFCLE9BQU8sQ0FBQyxHQUFHLE9BQU8sa0NBQWtDLENBQUMsQ0FBQztJQUN4RCxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLFVBQVUsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQ3hELHVCQUF1QixDQUFDLFVBQVUsRUFBRSxHQUFHLE9BQU8sSUFBSSxRQUFRLEdBQUcsQ0FBQyxDQUMvRCxDQUFDLENBQUM7SUFDSCxJQUFJLE1BQU0sQ0FBQyxNQUFNLEdBQUcsQ0FBQyxFQUFFLENBQUM7UUFDdEIsT0FBTyxNQUFNLENBQUM7SUFDaEIsQ0FBQztJQUNELE1BQU0sV0FBVyxHQUFHLFFBQVEsQ0FBQyxHQUFHLENBQUMsQ0FBQyxVQUFVLEVBQUUsRUFBRSxDQUFFLFVBQTRDLENBQUMsVUFBVSxDQUFDLENBQUM7SUFDM0csSUFBSSxJQUFJLEdBQUcsQ0FBQyxXQUFXLENBQUMsQ0FBQyxJQUFJLEtBQUssV0FBVyxDQUFDLE1BQU0sRUFBRSxDQUFDO1FBQ3JELE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLCtCQUErQixDQUFDLENBQUM7SUFDekQsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMsQ0FBQztBQUVGLE1BQU0sMkJBQTJCLEdBQUcsQ0FDbEMsVUFBbUMsRUFDbkMsT0FBZSxFQUNMLEVBQUU7SUFDWixNQUFNLEtBQUssR0FBRyxVQUFVLENBQUMsc0JBQXNCLEtBQUssSUFBSTtRQUN0RCxDQUFDLENBQUMsSUFBSTtRQUNOLENBQUMsQ0FBQyxtQkFBbUIsQ0FBQyxVQUFVLENBQUMsc0JBQXNCLENBQUMsQ0FBQztJQUMzRCxNQUFNLE9BQU8sR0FBRyxtQkFBbUIsQ0FBQyxVQUFVLENBQUMsd0JBQXdCLENBQUMsQ0FBQztJQUN6RSxNQUFNLE1BQU0sR0FBYSxFQUFFLENBQUM7SUFDNUIsSUFBSSxVQUFVLENBQUMsc0JBQXNCLEtBQUssSUFBSSxJQUFJLEtBQUssS0FBSyxJQUFJLEVBQUUsQ0FBQztRQUNqRSxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTywyREFBMkQsQ0FBQyxDQUFDO0lBQ3JGLENBQUM7SUFDRCxJQUFJLE9BQU8sS0FBSyxJQUFJLEVBQUUsQ0FBQztRQUNyQixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxxREFBcUQsQ0FBQyxDQUFDO0lBQy9FLENBQUM7SUFDRCxJQUFJLEtBQUssS0FBSyxJQUFJLElBQUksT0FBTyxLQUFLLElBQUksSUFBSSxzQkFBc0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7UUFDdEYsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sZ0VBQWdFLENBQUMsQ0FBQztJQUMxRixDQUFDO0lBQ0QsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQyxDQUFDO0FBRUYsc0NBQXNDO0FBQ3RDLE1BQU0sV0FBVyxHQUFtRSxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2hHLDhCQUE4QixFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxNQUFNLEVBQUUsU0FBUyxDQUFDLENBQUM7SUFDbEUsK0JBQStCLEVBQUUsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDO0lBQ3hELCtCQUErQixFQUFFLE1BQU0sQ0FBQyxNQUFNLENBQUM7UUFDN0MsTUFBTSxFQUFFLHdCQUF3QixFQUFFLDBCQUEwQjtLQUM3RCxDQUFDO0NBQ0gsQ0FBQyxDQUFDO0FBRUgsbUdBQW1HO0FBQ25HLE1BQU0sQ0FBQyxNQUFNLDRCQUE0QixHQUFHLENBQzFDLFFBQWlCLEVBQ2pCLE9BQU8sR0FBRyx3QkFBd0IsRUFDeEIsRUFBRTtJQUNaLE1BQU0sVUFBVSxHQUFHLGtCQUFrQixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ2hELElBQUksVUFBVSxLQUFLLElBQUksSUFBSSxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQztRQUMzRCxPQUFPLENBQUMsR0FBRyxPQUFPLHdCQUF3Qix5QkFBeUIsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ3BGLENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxtQkFBbUIsQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLFVBQVUsQ0FBQyxJQUFJLENBQUMsQ0FBQyxDQUFDO0lBQzVFLElBQUksT0FBTyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ3JCLE9BQU8sQ0FBQyxHQUFHLE9BQU8saUNBQWlDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBQ3hFLENBQUM7SUFDRCxRQUFRLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN4QixLQUFLLDhCQUE4QjtZQUNqQyxPQUFPLHdCQUF3QixDQUFDLE9BQU8sQ0FBQyxPQUFPLEVBQUUsR0FBRyxPQUFPLFVBQVUsQ0FBQyxDQUFDO1FBQ3pFLEtBQUssK0JBQStCO1lBQ2xDLE9BQU8sRUFBRSxDQUFDO1FBQ1osS0FBSywrQkFBK0I7WUFDbEMsT0FBTywyQkFBMkIsQ0FBQyxPQUFPLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDekQsQ0FBQztBQUNILENBQUMsQ0FBQztBQUVGLHNEQUFzRDtBQUN0RCxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLFFBQWlCLEVBQXFDLEVBQUUsQ0FBQyxDQUM5Riw0QkFBNEIsQ0FBQyxRQUFRLENBQUMsQ0FBQyxNQUFNLEtBQUssQ0FBQyxDQUNwRCxDQUFDO0FBRUYsTUFBTSxxQkFBcUIsR0FBRyxDQUM1QixXQUEwQyxFQUNYLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQ2hELFVBQVUsRUFBRSxXQUFXLENBQUMsVUFBVTtJQUNsQyxVQUFVLEVBQUUsV0FBVyxDQUFDLFVBQVU7SUFDbEMsR0FBRyxDQUFDLFdBQVcsQ0FBQyxZQUFZLEtBQUssU0FBUyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLEVBQUUsWUFBWSxFQUFFLFdBQVcsQ0FBQyxZQUFZLEVBQUUsQ0FBQztDQUM5RixDQUFDLENBQUM7QUFFSCxNQUFNLDhCQUE4QixHQUFHLENBQ3JDLFVBQW9ELEVBQ1YsRUFBRTtJQUM1QyxNQUFNLE9BQU8sR0FBRyxVQUFVLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxxQkFBcUIsQ0FBQyxDQUFDO0lBQzlELE1BQU0sQ0FBQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUM7SUFDdkIsT0FBTyxNQUFNLENBQUMsTUFBTSxDQUFDLEVBQUUsSUFBSSxFQUFFLFVBQVUsQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFLENBQUMsQ0FBQztBQUMzRCxDQUFDLENBQUM7QUFFRixNQUFNLCtCQUErQixHQUFHLENBQ3RDLFVBQXFELEVBQ1YsRUFBRSxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBRSxJQUFJLEVBQUUsVUFBVSxDQUFDLElBQUksRUFBRSxDQUFDLENBQUM7QUFFekYsTUFBTSw4QkFBOEIsR0FBRyxDQUNyQyxVQUFvRCxFQUNWLEVBQUUsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDO0lBQzNELElBQUksRUFBRSxVQUFVLENBQUMsSUFBSTtJQUNyQixzQkFBc0IsRUFBRSxVQUFVLENBQUMsc0JBQXNCO0lBQ3pELHdCQUF3QixFQUFFLFVBQVUsQ0FBQyx3QkFBd0I7Q0FDOUQsQ0FBQyxDQUFDO0FBRUgsb0ZBQW9GO0FBQ3BGLE1BQU0sV0FBVyxHQUFHLENBQUMsVUFBaUMsRUFBeUIsRUFBRTtJQUMvRSxRQUFRLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQztRQUN4QixLQUFLLDhCQUE4QjtZQUNqQyxPQUFPLDhCQUE4QixDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQ3BELEtBQUssK0JBQStCO1lBQ2xDLE9BQU8sK0JBQStCLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDckQsS0FBSywrQkFBK0I7WUFDbEMsT0FBTyw4QkFBOEIsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUN0RCxDQUFDO0FBQ0gsQ0FBQyxDQUFDO0FBRUY7Ozs7R0FJRztBQUNILE1BQU0sQ0FBQyxNQUFNLHdCQUF3QixHQUFHLENBQUMsUUFBaUIsRUFBZ0MsRUFBRSxDQUFDO0FBQzNGLGdHQUFnRztBQUNoRyx5REFBeUQ7QUFDekQsc0JBQXNCLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUNoRSxDQUFDO0FBRUYsZ0dBQWdHO0FBQ2hHLE1BQU0sY0FBYyxHQUFHLENBQWtDLFVBQWEsRUFBSyxFQUFFO0lBQzNFLE1BQU0sTUFBTSxHQUFHLDRCQUE0QixDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ3hELElBQUksTUFBTSxDQUFDLE1BQU0sR0FBRyxDQUFDLEVBQUUsQ0FBQztRQUN0QixNQUFNLElBQUksS0FBSyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUNyQyxDQUFDO0lBQ0QsT0FBTyxVQUFVLENBQUM7QUFDcEIsQ0FBQyxDQUFDO0FBRUYsNEZBQTRGO0FBQzVGLE1BQU0sQ0FBQyxNQUFNLDRDQUE0QyxHQUFHLENBQzFELFVBQTJDLEVBQ0QsRUFBRSxDQUFDLDhCQUE4QixDQUFDLGNBQWMsQ0FBQztJQUMzRixJQUFJLEVBQUUsOEJBQThCO0lBQ3BDLE9BQU8sRUFBRSxVQUFVLENBQUMsR0FBRyxDQUFDLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLEVBQUUsR0FBRyxXQUFXLEVBQUUsQ0FBQyxDQUFDO0NBQy9ELENBQUMsQ0FBQyxDQUFDO0FBRUoscUZBQXFGO0FBQ3JGLE1BQU0sQ0FBQyxNQUFNLDZDQUE2QyxHQUFHLEdBQThDLEVBQUUsQ0FBQyxDQUM1RywrQkFBK0IsQ0FBQyxjQUFjLENBQUMsRUFBRSxJQUFJLEVBQUUsK0JBQStCLEVBQUUsQ0FBQyxDQUFDLENBQzNGLENBQUM7QUFFRjs7Ozs7R0FLRztBQUNILE1BQU0sQ0FBQyxNQUFNLDRDQUE0QyxHQUFHLENBQzFELHlCQUF3QyxFQUN4QywyQkFBbUMsRUFDTyxFQUFFLENBQUMsOEJBQThCLENBQUMsY0FBYyxDQUFDO0lBQzNGLElBQUksRUFBRSwrQkFBK0I7SUFDckMsc0JBQXNCLEVBQUUseUJBQXlCO0lBQ2pELHdCQUF3QixFQUFFLDJCQUEyQjtDQUN0RCxDQUFDLENBQUMsQ0FBQztBQUVKLE1BQU0sb0JBQW9CLEdBQTZDLE1BQU0sQ0FBQyxNQUFNLENBQUM7SUFDbkYsVUFBVSxFQUFFLFFBQVEsRUFBRSxTQUFTO0NBQ2hDLENBQUMsQ0FBQztBQUVIOzs7Ozs7R0FNRztBQUNILE1BQU0sQ0FBQyxNQUFNLDBCQUEwQixHQUFHLENBQ3hDLFFBQWlCLEVBQ2pCLE9BQU8sR0FBRyxzQkFBc0IsRUFDdEIsRUFBRTtJQUNaLE1BQU0sS0FBSyxHQUFHLGtCQUFrQixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQzNDLElBQUksS0FBSyxLQUFLLElBQUksRUFBRSxDQUFDO1FBQ25CLE9BQU8sQ0FBQyxHQUFHLE9BQU8sNENBQTRDLENBQUMsQ0FBQztJQUNsRSxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQWEsRUFBRSxDQUFDO0lBQzVCLElBQUksQ0FBRSxvQkFBMkMsQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLE1BQU0sQ0FBQyxFQUFFLENBQUM7UUFDekUsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sMEJBQTBCLG9CQUFvQixDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDckYsQ0FBQztJQUNELE1BQU0sVUFBVSxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsS0FBSyxFQUFFLFNBQVMsQ0FBQyxDQUFDO0lBQ25ELElBQUksS0FBSyxDQUFDLE1BQU0sS0FBSyxTQUFTLEVBQUUsQ0FBQztRQUMvQixJQUFJLENBQUMsVUFBVSxFQUFFLENBQUM7WUFDaEIsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sNkNBQTZDLENBQUMsQ0FBQztRQUN2RSxDQUFDO2FBQU0sQ0FBQztZQUNOLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyw0QkFBNEIsQ0FBQyxLQUFLLENBQUMsT0FBTyxFQUFFLEdBQUcsT0FBTyxVQUFVLENBQUMsQ0FBQyxDQUFDO1FBQ3BGLENBQUM7SUFDSCxDQUFDO1NBQU0sSUFBSSxVQUFVLEVBQUUsQ0FBQztRQUN0QixNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxpREFBaUQsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFDRCxPQUFPLE1BQU0sQ0FBQztBQUNoQixDQUFDLENBQUMifQ==
@@ -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==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serve.zone/interfaces",
3
- "version": "32.70.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.70.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 =
@@ -101,9 +102,17 @@ export type TBackupFailure =
101
102
  | { kind: 'incomplete'; uncaptured: TBackupUncapturedResource[] }
102
103
  | { kind: 'archive-unreadable' };
103
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
+ */
104
112
  export interface IBackupRestoreEvent {
113
+ /** When the restore ended, or when it was refused. */
105
114
  restoredAt: number;
106
- status: 'restored' | 'failed';
115
+ status: 'restored' | 'failed' | 'refused';
107
116
  errorText?: string;
108
117
  /** The resource types the restore was limited to; absent when it restored every snapshot. */
109
118
  resourceTypes?: TBackupResourceType[];
@@ -112,6 +121,8 @@ export interface IBackupRestoreEvent {
112
121
  * none of the backup's `failure.uncaptured`.
113
122
  */
114
123
  partial?: true;
124
+ /** Why a `refused` restore wrote nothing; present exactly when `status` is `refused`. */
125
+ refusal?: TBackupRestoreRefusal;
115
126
  }
116
127
 
117
128
  /**
@@ -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';