@serve.zone/interfaces 32.1.0 → 32.2.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 +18 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/platform/storage.d.ts +29 -2
- package/dist_ts/platform/storage.golden.d.ts +31 -0
- package/dist_ts/platform/storage.golden.js +84 -0
- package/dist_ts/platform/storage.js +37 -1
- package/dist_ts/platform/storagemigration.js +11 -287
- package/dist_ts/private/storagecontract.d.ts +40 -0
- package/dist_ts/private/storagecontract.js +399 -0
- package/package.json +1 -1
- package/readme.md +15 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/platform/storage.golden.ts +97 -0
- package/ts/platform/storage.ts +59 -2
- package/ts/platform/storagemigration.ts +31 -440
- package/ts/private/storagecontract.ts +623 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The readers a storage control body is judged by, and the one normalizer that turns a storage
|
|
3
|
+
* request into the exact value its digest is taken over.
|
|
4
|
+
*
|
|
5
|
+
* Two modules judge the same bytes in their own error vocabulary — `platform/storage.ts` for a
|
|
6
|
+
* storage request on its own and `platform/storagemigration.ts` for the migration control plane
|
|
7
|
+
* that carries one — so the rules take the failure they report through. A second copy of them
|
|
8
|
+
* would be a second answer to what a request is, and a request digest that a control plane issues
|
|
9
|
+
* and a consumer re-derives would stop being one value.
|
|
10
|
+
*/
|
|
11
|
+
import type {
|
|
12
|
+
IAppStoreFilesystemStorageRequest,
|
|
13
|
+
IAppStoreObjectStorageRequest,
|
|
14
|
+
TAppStoreStorageRequest,
|
|
15
|
+
} from '../appstore/types.js';
|
|
16
|
+
import { normalizeServiceAbsolutePath } from '../data/service.js';
|
|
17
|
+
import type { TObjectStorageDelivery } from '../platform/storage.js';
|
|
18
|
+
import type { TCanonicalJsonFailure } from './canonicaljson.js';
|
|
19
|
+
|
|
20
|
+
/** Byte bounds every reader of a storage control string applies. */
|
|
21
|
+
export const storageContractStringLimits = Object.freeze({
|
|
22
|
+
maximumConfiguredStringBytes: 2048,
|
|
23
|
+
maximumEnvironmentKeyBytes: 256,
|
|
24
|
+
maximumSecretPathBytes: 2048,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const canonicalIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,191}$/;
|
|
28
|
+
const environmentKeyPattern = /^[A-Z_][A-Z0-9_]{0,255}$/;
|
|
29
|
+
const sha256Pattern = /^[a-f0-9]{64}$/;
|
|
30
|
+
const capacityPattern = /^[1-9][0-9]*(?:KiB|MiB|GiB|TiB)$/;
|
|
31
|
+
/**
|
|
32
|
+
* The mount a Secret file is delivered under. A filesystem request that mounts it, or anything
|
|
33
|
+
* containing it, would shadow every credential the platform writes for the same service.
|
|
34
|
+
*/
|
|
35
|
+
const secretDeliveryRoot = '/run/secrets';
|
|
36
|
+
|
|
37
|
+
export const createStorageContractReaders = (failArg: TCanonicalJsonFailure) => {
|
|
38
|
+
const utf8ByteLength = (valueArg: string): number => {
|
|
39
|
+
return new TextEncoder().encode(valueArg).byteLength;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const readRecord = (
|
|
43
|
+
valueArg: unknown,
|
|
44
|
+
fieldNameArg: string,
|
|
45
|
+
): Record<string, unknown> => {
|
|
46
|
+
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
47
|
+
return failArg(`${fieldNameArg} must be a plain object`);
|
|
48
|
+
}
|
|
49
|
+
const prototype = Object.getPrototypeOf(valueArg);
|
|
50
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
51
|
+
return failArg(`${fieldNameArg} must be a plain object`);
|
|
52
|
+
}
|
|
53
|
+
const record = valueArg as Record<string, unknown>;
|
|
54
|
+
const ownKeys = Reflect.ownKeys(record);
|
|
55
|
+
if (ownKeys.some((keyArg) => typeof keyArg !== 'string')) {
|
|
56
|
+
return failArg(`${fieldNameArg} must not contain symbol keys`);
|
|
57
|
+
}
|
|
58
|
+
for (const key of ownKeys as string[]) {
|
|
59
|
+
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
60
|
+
if (
|
|
61
|
+
!descriptor ||
|
|
62
|
+
!descriptor.enumerable ||
|
|
63
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
64
|
+
descriptor.value === undefined
|
|
65
|
+
) {
|
|
66
|
+
return failArg(
|
|
67
|
+
`${fieldNameArg} must contain enumerable JSON data properties only`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return record;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const readDenseArray = (
|
|
75
|
+
valueArg: unknown,
|
|
76
|
+
fieldNameArg: string,
|
|
77
|
+
minimumLengthArg: number,
|
|
78
|
+
maximumLengthArg: number,
|
|
79
|
+
): unknown[] => {
|
|
80
|
+
if (
|
|
81
|
+
!Array.isArray(valueArg) ||
|
|
82
|
+
Object.getPrototypeOf(valueArg) !== Array.prototype
|
|
83
|
+
) {
|
|
84
|
+
return failArg(`${fieldNameArg} must be a dense plain array`);
|
|
85
|
+
}
|
|
86
|
+
if (
|
|
87
|
+
valueArg.length < minimumLengthArg ||
|
|
88
|
+
valueArg.length > maximumLengthArg
|
|
89
|
+
) {
|
|
90
|
+
return failArg(
|
|
91
|
+
`${fieldNameArg} must contain between ${minimumLengthArg} and ${maximumLengthArg} entries`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
95
|
+
if (!Object.hasOwn(valueArg, index)) {
|
|
96
|
+
return failArg(`${fieldNameArg} must not contain sparse entries`);
|
|
97
|
+
}
|
|
98
|
+
const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index));
|
|
99
|
+
if (
|
|
100
|
+
!descriptor ||
|
|
101
|
+
!descriptor.enumerable ||
|
|
102
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
103
|
+
descriptor.value === undefined
|
|
104
|
+
) {
|
|
105
|
+
return failArg(
|
|
106
|
+
`${fieldNameArg} must contain enumerable JSON data entries only`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
for (const key of Reflect.ownKeys(valueArg)) {
|
|
111
|
+
if (key === 'length') continue;
|
|
112
|
+
if (
|
|
113
|
+
typeof key !== 'string' ||
|
|
114
|
+
!/^(?:0|[1-9][0-9]*)$/.test(key) ||
|
|
115
|
+
Number(key) >= valueArg.length
|
|
116
|
+
) {
|
|
117
|
+
return failArg(`${fieldNameArg} must not contain extra properties`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return valueArg;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const assertKeys = (
|
|
124
|
+
recordArg: Record<string, unknown>,
|
|
125
|
+
requiredKeysArg: readonly string[],
|
|
126
|
+
optionalKeysArg: readonly string[],
|
|
127
|
+
fieldNameArg: string,
|
|
128
|
+
): void => {
|
|
129
|
+
const actualKeys = Object.keys(recordArg).sort();
|
|
130
|
+
const allowedKeys = new Set([...requiredKeysArg, ...optionalKeysArg]);
|
|
131
|
+
if (
|
|
132
|
+
requiredKeysArg.some((keyArg) => !Object.hasOwn(recordArg, keyArg)) ||
|
|
133
|
+
actualKeys.some((keyArg) => !allowedKeys.has(keyArg))
|
|
134
|
+
) {
|
|
135
|
+
failArg(`${fieldNameArg} does not match its exact schema`);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const assertExactKeys = (
|
|
140
|
+
recordArg: Record<string, unknown>,
|
|
141
|
+
expectedKeysArg: readonly string[],
|
|
142
|
+
fieldNameArg: string,
|
|
143
|
+
): void => {
|
|
144
|
+
assertKeys(recordArg, expectedKeysArg, [], fieldNameArg);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const readString = (
|
|
148
|
+
valueArg: unknown,
|
|
149
|
+
fieldNameArg: string,
|
|
150
|
+
maximumBytesArg: number =
|
|
151
|
+
storageContractStringLimits.maximumConfiguredStringBytes,
|
|
152
|
+
): string => {
|
|
153
|
+
if (
|
|
154
|
+
typeof valueArg !== 'string' ||
|
|
155
|
+
valueArg.length === 0 ||
|
|
156
|
+
utf8ByteLength(valueArg) > maximumBytesArg ||
|
|
157
|
+
/[\u0000-\u001f\u007f]/u.test(valueArg)
|
|
158
|
+
) {
|
|
159
|
+
return failArg(`${fieldNameArg} must be a bounded non-empty JSON string`);
|
|
160
|
+
}
|
|
161
|
+
for (let index = 0; index < valueArg.length; index++) {
|
|
162
|
+
const codeUnit = valueArg.charCodeAt(index);
|
|
163
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
164
|
+
const following = valueArg.charCodeAt(index + 1);
|
|
165
|
+
if (following < 0xdc00 || following > 0xdfff) {
|
|
166
|
+
return failArg(`${fieldNameArg} contains an unpaired UTF-16 surrogate`);
|
|
167
|
+
}
|
|
168
|
+
index++;
|
|
169
|
+
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
170
|
+
return failArg(`${fieldNameArg} contains an unpaired UTF-16 surrogate`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return valueArg;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const readIdentifier = (valueArg: unknown, fieldNameArg: string): string => {
|
|
177
|
+
if (typeof valueArg !== 'string' || !canonicalIdPattern.test(valueArg)) {
|
|
178
|
+
return failArg(`${fieldNameArg} must be a canonical identifier`);
|
|
179
|
+
}
|
|
180
|
+
return valueArg;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const readSha256 = (valueArg: unknown, fieldNameArg: string): string => {
|
|
184
|
+
if (typeof valueArg !== 'string' || !sha256Pattern.test(valueArg)) {
|
|
185
|
+
return failArg(`${fieldNameArg} must be a bare lowercase 64-hex SHA-256 digest`);
|
|
186
|
+
}
|
|
187
|
+
return valueArg;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const readSafeInteger = (
|
|
191
|
+
valueArg: unknown,
|
|
192
|
+
fieldNameArg: string,
|
|
193
|
+
minimumArg: number,
|
|
194
|
+
maximumArg = Number.MAX_SAFE_INTEGER,
|
|
195
|
+
): number => {
|
|
196
|
+
if (
|
|
197
|
+
typeof valueArg !== 'number' ||
|
|
198
|
+
!Number.isSafeInteger(valueArg) ||
|
|
199
|
+
Object.is(valueArg, -0) ||
|
|
200
|
+
valueArg < minimumArg ||
|
|
201
|
+
valueArg > maximumArg
|
|
202
|
+
) {
|
|
203
|
+
return failArg(
|
|
204
|
+
`${fieldNameArg} must be a safe integer between ${minimumArg} and ${maximumArg}`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return valueArg;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const readBoolean = (valueArg: unknown, fieldNameArg: string): boolean => {
|
|
211
|
+
if (typeof valueArg !== 'boolean') {
|
|
212
|
+
return failArg(`${fieldNameArg} must be a boolean`);
|
|
213
|
+
}
|
|
214
|
+
return valueArg;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const readLiteral = <TLiteral extends string>(
|
|
218
|
+
valueArg: unknown,
|
|
219
|
+
allowedArg: readonly TLiteral[],
|
|
220
|
+
fieldNameArg: string,
|
|
221
|
+
): TLiteral => {
|
|
222
|
+
if (
|
|
223
|
+
typeof valueArg !== 'string' ||
|
|
224
|
+
!allowedArg.includes(valueArg as TLiteral)
|
|
225
|
+
) {
|
|
226
|
+
return failArg(`${fieldNameArg} is outside the supported values`);
|
|
227
|
+
}
|
|
228
|
+
return valueArg as TLiteral;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const readCapacityQuantity = (
|
|
232
|
+
valueArg: unknown,
|
|
233
|
+
fieldNameArg: string,
|
|
234
|
+
): string => {
|
|
235
|
+
if (typeof valueArg !== 'string' || !capacityPattern.test(valueArg)) {
|
|
236
|
+
return failArg(`${fieldNameArg} must be a canonical base-2 capacity quantity`);
|
|
237
|
+
}
|
|
238
|
+
const numericPart = valueArg.replace(/(?:KiB|MiB|GiB|TiB)$/, '');
|
|
239
|
+
if (!Number.isSafeInteger(Number(numericPart))) {
|
|
240
|
+
return failArg(`${fieldNameArg} exceeds the safe integer quantity range`);
|
|
241
|
+
}
|
|
242
|
+
return valueArg;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const capacityQuantityBytes = (valueArg: string): bigint => {
|
|
246
|
+
const units: Record<string, bigint> = {
|
|
247
|
+
KiB: 1024n,
|
|
248
|
+
MiB: 1024n ** 2n,
|
|
249
|
+
GiB: 1024n ** 3n,
|
|
250
|
+
TiB: 1024n ** 4n,
|
|
251
|
+
};
|
|
252
|
+
const unit = valueArg.slice(-3);
|
|
253
|
+
return BigInt(valueArg.slice(0, -3)) * units[unit];
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const normalizeCapacityRequest = (
|
|
257
|
+
valueArg: unknown,
|
|
258
|
+
fieldNameArg: string,
|
|
259
|
+
): NonNullable<TAppStoreStorageRequest['capacity']> => {
|
|
260
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
261
|
+
assertKeys(value, ['request'], ['limit'], fieldNameArg);
|
|
262
|
+
const request = readCapacityQuantity(value.request, `${fieldNameArg}.request`);
|
|
263
|
+
const limit = Object.hasOwn(value, 'limit')
|
|
264
|
+
? readCapacityQuantity(value.limit, `${fieldNameArg}.limit`)
|
|
265
|
+
: undefined;
|
|
266
|
+
if (limit && capacityQuantityBytes(limit) < capacityQuantityBytes(request)) {
|
|
267
|
+
return failArg(`${fieldNameArg}.limit must be at least request`);
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
request,
|
|
271
|
+
...(limit ? { limit } : {}),
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const readEnvironmentKey = (
|
|
276
|
+
valueArg: unknown,
|
|
277
|
+
fieldNameArg: string,
|
|
278
|
+
): string => {
|
|
279
|
+
if (
|
|
280
|
+
typeof valueArg !== 'string' ||
|
|
281
|
+
utf8ByteLength(valueArg) >
|
|
282
|
+
storageContractStringLimits.maximumEnvironmentKeyBytes ||
|
|
283
|
+
!environmentKeyPattern.test(valueArg)
|
|
284
|
+
) {
|
|
285
|
+
return failArg(`${fieldNameArg} must be a canonical environment key`);
|
|
286
|
+
}
|
|
287
|
+
return valueArg;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const normalizeDelivery = (
|
|
291
|
+
valueArg: unknown,
|
|
292
|
+
fieldNameArg: string,
|
|
293
|
+
): TObjectStorageDelivery => {
|
|
294
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
295
|
+
if (value.type === 'launcher-environment') {
|
|
296
|
+
assertExactKeys(value, ['type', 'keys'], fieldNameArg);
|
|
297
|
+
const keys = readRecord(value.keys, `${fieldNameArg}.keys`);
|
|
298
|
+
assertKeys(
|
|
299
|
+
keys,
|
|
300
|
+
['endpoint', 'bucket', 'region', 'accessKeyId', 'secretAccessKey'],
|
|
301
|
+
['sessionToken'],
|
|
302
|
+
`${fieldNameArg}.keys`,
|
|
303
|
+
);
|
|
304
|
+
const normalizedKeys = {
|
|
305
|
+
endpoint: readEnvironmentKey(
|
|
306
|
+
keys.endpoint,
|
|
307
|
+
`${fieldNameArg}.keys.endpoint`,
|
|
308
|
+
),
|
|
309
|
+
bucket: readEnvironmentKey(keys.bucket, `${fieldNameArg}.keys.bucket`),
|
|
310
|
+
region: readEnvironmentKey(keys.region, `${fieldNameArg}.keys.region`),
|
|
311
|
+
accessKeyId: readEnvironmentKey(
|
|
312
|
+
keys.accessKeyId,
|
|
313
|
+
`${fieldNameArg}.keys.accessKeyId`,
|
|
314
|
+
),
|
|
315
|
+
secretAccessKey: readEnvironmentKey(
|
|
316
|
+
keys.secretAccessKey,
|
|
317
|
+
`${fieldNameArg}.keys.secretAccessKey`,
|
|
318
|
+
),
|
|
319
|
+
...(Object.hasOwn(keys, 'sessionToken')
|
|
320
|
+
? {
|
|
321
|
+
sessionToken: readEnvironmentKey(
|
|
322
|
+
keys.sessionToken,
|
|
323
|
+
`${fieldNameArg}.keys.sessionToken`,
|
|
324
|
+
),
|
|
325
|
+
}
|
|
326
|
+
: {}),
|
|
327
|
+
};
|
|
328
|
+
if (
|
|
329
|
+
new Set(Object.values(normalizedKeys)).size !==
|
|
330
|
+
Object.keys(normalizedKeys).length
|
|
331
|
+
) {
|
|
332
|
+
return failArg(`${fieldNameArg}.keys values must be distinct`);
|
|
333
|
+
}
|
|
334
|
+
return { type: 'launcher-environment', keys: normalizedKeys };
|
|
335
|
+
}
|
|
336
|
+
if (value.type === 'file') {
|
|
337
|
+
assertExactKeys(value, ['type', 'targetPath', 'format', 'uid', 'gid', 'mode'], fieldNameArg);
|
|
338
|
+
const targetPath = readString(
|
|
339
|
+
value.targetPath,
|
|
340
|
+
`${fieldNameArg}.targetPath`,
|
|
341
|
+
storageContractStringLimits.maximumSecretPathBytes,
|
|
342
|
+
);
|
|
343
|
+
if (
|
|
344
|
+
!targetPath.startsWith(`${secretDeliveryRoot}/`) ||
|
|
345
|
+
targetPath.includes('\\') ||
|
|
346
|
+
targetPath.split('/').some((segmentArg) =>
|
|
347
|
+
segmentArg === '.' || segmentArg === '..'
|
|
348
|
+
)
|
|
349
|
+
) {
|
|
350
|
+
return failArg(
|
|
351
|
+
`${fieldNameArg}.targetPath must be a canonical path below ${secretDeliveryRoot}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (value.format !== 'servezone-object-storage') {
|
|
355
|
+
return failArg(`${fieldNameArg}.format is unsupported`);
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
type: 'file',
|
|
359
|
+
targetPath,
|
|
360
|
+
format: 'servezone-object-storage',
|
|
361
|
+
uid: readSafeInteger(value.uid, `${fieldNameArg}.uid`, 0, 2_147_483_647),
|
|
362
|
+
gid: readSafeInteger(value.gid, `${fieldNameArg}.gid`, 0, 2_147_483_647),
|
|
363
|
+
mode: value.mode === 0o400 || value.mode === 0o440
|
|
364
|
+
? value.mode
|
|
365
|
+
: failArg(`${fieldNameArg}.mode must be 0400 or 0440`),
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return failArg(`${fieldNameArg}.type is unsupported`);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const normalizeObjectStorageProtection = (
|
|
372
|
+
valueArg: unknown,
|
|
373
|
+
fieldNameArg: string,
|
|
374
|
+
): NonNullable<IAppStoreObjectStorageRequest['protection']> => {
|
|
375
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
376
|
+
assertKeys(
|
|
377
|
+
value,
|
|
378
|
+
[],
|
|
379
|
+
['backup', 'versioning', 'retentionDays'],
|
|
380
|
+
fieldNameArg,
|
|
381
|
+
);
|
|
382
|
+
if (Object.keys(value).length === 0) {
|
|
383
|
+
return failArg(`${fieldNameArg} must not be empty`);
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
...(Object.hasOwn(value, 'backup')
|
|
387
|
+
? {
|
|
388
|
+
backup: value.backup === 'required'
|
|
389
|
+
? 'required' as const
|
|
390
|
+
: failArg(`${fieldNameArg}.backup must be required`),
|
|
391
|
+
}
|
|
392
|
+
: {}),
|
|
393
|
+
...(Object.hasOwn(value, 'versioning')
|
|
394
|
+
? {
|
|
395
|
+
versioning: value.versioning === 'required'
|
|
396
|
+
? 'required' as const
|
|
397
|
+
: failArg(`${fieldNameArg}.versioning must be required`),
|
|
398
|
+
}
|
|
399
|
+
: {}),
|
|
400
|
+
...(Object.hasOwn(value, 'retentionDays')
|
|
401
|
+
? {
|
|
402
|
+
retentionDays: readSafeInteger(
|
|
403
|
+
value.retentionDays,
|
|
404
|
+
`${fieldNameArg}.retentionDays`,
|
|
405
|
+
1,
|
|
406
|
+
365_000,
|
|
407
|
+
),
|
|
408
|
+
}
|
|
409
|
+
: {}),
|
|
410
|
+
};
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
const normalizeFilesystemProtection = (
|
|
414
|
+
valueArg: unknown,
|
|
415
|
+
fieldNameArg: string,
|
|
416
|
+
): NonNullable<IAppStoreFilesystemStorageRequest['protection']> => {
|
|
417
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
418
|
+
assertKeys(value, [], ['backup', 'snapshots', 'consistency'], fieldNameArg);
|
|
419
|
+
if (Object.keys(value).length === 0) {
|
|
420
|
+
return failArg(`${fieldNameArg} must not be empty`);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
...(Object.hasOwn(value, 'backup')
|
|
424
|
+
? {
|
|
425
|
+
backup: value.backup === 'required'
|
|
426
|
+
? 'required' as const
|
|
427
|
+
: failArg(`${fieldNameArg}.backup must be required`),
|
|
428
|
+
}
|
|
429
|
+
: {}),
|
|
430
|
+
...(Object.hasOwn(value, 'snapshots')
|
|
431
|
+
? {
|
|
432
|
+
snapshots: readLiteral(
|
|
433
|
+
value.snapshots,
|
|
434
|
+
['portable', 'native'] as const,
|
|
435
|
+
`${fieldNameArg}.snapshots`,
|
|
436
|
+
),
|
|
437
|
+
}
|
|
438
|
+
: {}),
|
|
439
|
+
...(Object.hasOwn(value, 'consistency')
|
|
440
|
+
? {
|
|
441
|
+
consistency: readLiteral(
|
|
442
|
+
value.consistency,
|
|
443
|
+
['crashConsistent', 'applicationConsistent'] as const,
|
|
444
|
+
`${fieldNameArg}.consistency`,
|
|
445
|
+
),
|
|
446
|
+
}
|
|
447
|
+
: {}),
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const readMountPath = (valueArg: unknown, fieldNameArg: string): string => {
|
|
452
|
+
const value = readString(
|
|
453
|
+
valueArg,
|
|
454
|
+
fieldNameArg,
|
|
455
|
+
storageContractStringLimits.maximumSecretPathBytes,
|
|
456
|
+
);
|
|
457
|
+
if (normalizeServiceAbsolutePath(value) !== value || value === '/') {
|
|
458
|
+
return failArg(
|
|
459
|
+
`${fieldNameArg} must be a canonical non-root absolute path`,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (
|
|
463
|
+
value === secretDeliveryRoot ||
|
|
464
|
+
value.startsWith(`${secretDeliveryRoot}/`) ||
|
|
465
|
+
secretDeliveryRoot.startsWith(`${value}/`)
|
|
466
|
+
) {
|
|
467
|
+
return failArg(`${fieldNameArg} must not overlap ${secretDeliveryRoot}`);
|
|
468
|
+
}
|
|
469
|
+
return value;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const normalizeObjectStorageRequest = (
|
|
473
|
+
valueArg: unknown,
|
|
474
|
+
fieldNameArg: string,
|
|
475
|
+
): IAppStoreObjectStorageRequest => {
|
|
476
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
477
|
+
assertKeys(
|
|
478
|
+
value,
|
|
479
|
+
[
|
|
480
|
+
'id',
|
|
481
|
+
'kind',
|
|
482
|
+
'storageClass',
|
|
483
|
+
'accessMode',
|
|
484
|
+
'reclaimPolicy',
|
|
485
|
+
'delivery',
|
|
486
|
+
],
|
|
487
|
+
['capacity', 'protection'],
|
|
488
|
+
fieldNameArg,
|
|
489
|
+
);
|
|
490
|
+
if (value.kind !== 'objectStorage') {
|
|
491
|
+
return failArg(`${fieldNameArg}.kind must be objectStorage`);
|
|
492
|
+
}
|
|
493
|
+
return {
|
|
494
|
+
id: readIdentifier(value.id, `${fieldNameArg}.id`),
|
|
495
|
+
kind: 'objectStorage',
|
|
496
|
+
storageClass: readIdentifier(
|
|
497
|
+
value.storageClass,
|
|
498
|
+
`${fieldNameArg}.storageClass`,
|
|
499
|
+
),
|
|
500
|
+
accessMode: readLiteral(
|
|
501
|
+
value.accessMode,
|
|
502
|
+
['readOnly', 'readWrite'] as const,
|
|
503
|
+
`${fieldNameArg}.accessMode`,
|
|
504
|
+
),
|
|
505
|
+
reclaimPolicy: readLiteral(
|
|
506
|
+
value.reclaimPolicy,
|
|
507
|
+
['retain', 'delete'] as const,
|
|
508
|
+
`${fieldNameArg}.reclaimPolicy`,
|
|
509
|
+
),
|
|
510
|
+
delivery: normalizeDelivery(value.delivery, `${fieldNameArg}.delivery`),
|
|
511
|
+
...(Object.hasOwn(value, 'capacity')
|
|
512
|
+
? {
|
|
513
|
+
capacity: normalizeCapacityRequest(
|
|
514
|
+
value.capacity,
|
|
515
|
+
`${fieldNameArg}.capacity`,
|
|
516
|
+
),
|
|
517
|
+
}
|
|
518
|
+
: {}),
|
|
519
|
+
...(Object.hasOwn(value, 'protection')
|
|
520
|
+
? {
|
|
521
|
+
protection: normalizeObjectStorageProtection(
|
|
522
|
+
value.protection,
|
|
523
|
+
`${fieldNameArg}.protection`,
|
|
524
|
+
),
|
|
525
|
+
}
|
|
526
|
+
: {}),
|
|
527
|
+
};
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
const normalizeFilesystemStorageRequest = (
|
|
531
|
+
valueArg: unknown,
|
|
532
|
+
fieldNameArg: string,
|
|
533
|
+
): IAppStoreFilesystemStorageRequest => {
|
|
534
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
535
|
+
assertKeys(
|
|
536
|
+
value,
|
|
537
|
+
[
|
|
538
|
+
'id',
|
|
539
|
+
'kind',
|
|
540
|
+
'storageClass',
|
|
541
|
+
'accessMode',
|
|
542
|
+
'reclaimPolicy',
|
|
543
|
+
'mountPath',
|
|
544
|
+
],
|
|
545
|
+
['capacity', 'protection'],
|
|
546
|
+
fieldNameArg,
|
|
547
|
+
);
|
|
548
|
+
if (value.kind !== 'filesystem') {
|
|
549
|
+
return failArg(`${fieldNameArg}.kind must be filesystem`);
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
id: readIdentifier(value.id, `${fieldNameArg}.id`),
|
|
553
|
+
kind: 'filesystem',
|
|
554
|
+
storageClass: readIdentifier(
|
|
555
|
+
value.storageClass,
|
|
556
|
+
`${fieldNameArg}.storageClass`,
|
|
557
|
+
),
|
|
558
|
+
accessMode: readLiteral(
|
|
559
|
+
value.accessMode,
|
|
560
|
+
['ReadWriteOnce', 'ReadOnlyMany', 'ReadWriteMany'] as const,
|
|
561
|
+
`${fieldNameArg}.accessMode`,
|
|
562
|
+
),
|
|
563
|
+
reclaimPolicy: readLiteral(
|
|
564
|
+
value.reclaimPolicy,
|
|
565
|
+
['retain', 'delete'] as const,
|
|
566
|
+
`${fieldNameArg}.reclaimPolicy`,
|
|
567
|
+
),
|
|
568
|
+
mountPath: readMountPath(value.mountPath, `${fieldNameArg}.mountPath`),
|
|
569
|
+
...(Object.hasOwn(value, 'capacity')
|
|
570
|
+
? {
|
|
571
|
+
capacity: normalizeCapacityRequest(
|
|
572
|
+
value.capacity,
|
|
573
|
+
`${fieldNameArg}.capacity`,
|
|
574
|
+
),
|
|
575
|
+
}
|
|
576
|
+
: {}),
|
|
577
|
+
...(Object.hasOwn(value, 'protection')
|
|
578
|
+
? {
|
|
579
|
+
protection: normalizeFilesystemProtection(
|
|
580
|
+
value.protection,
|
|
581
|
+
`${fieldNameArg}.protection`,
|
|
582
|
+
),
|
|
583
|
+
}
|
|
584
|
+
: {}),
|
|
585
|
+
};
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const normalizeStorageRequest = (
|
|
589
|
+
valueArg: unknown,
|
|
590
|
+
fieldNameArg: string,
|
|
591
|
+
): TAppStoreStorageRequest => {
|
|
592
|
+
const value = readRecord(valueArg, fieldNameArg);
|
|
593
|
+
if (value.kind === 'filesystem') {
|
|
594
|
+
return normalizeFilesystemStorageRequest(value, fieldNameArg);
|
|
595
|
+
}
|
|
596
|
+
if (value.kind === 'objectStorage') {
|
|
597
|
+
return normalizeObjectStorageRequest(value, fieldNameArg);
|
|
598
|
+
}
|
|
599
|
+
return failArg(`${fieldNameArg}.kind must be filesystem or objectStorage`);
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
utf8ByteLength,
|
|
604
|
+
readRecord,
|
|
605
|
+
readDenseArray,
|
|
606
|
+
assertKeys,
|
|
607
|
+
assertExactKeys,
|
|
608
|
+
readString,
|
|
609
|
+
readIdentifier,
|
|
610
|
+
readSha256,
|
|
611
|
+
readSafeInteger,
|
|
612
|
+
readBoolean,
|
|
613
|
+
readLiteral,
|
|
614
|
+
readCapacityQuantity,
|
|
615
|
+
capacityQuantityBytes,
|
|
616
|
+
readEnvironmentKey,
|
|
617
|
+
normalizeCapacityRequest,
|
|
618
|
+
normalizeDelivery,
|
|
619
|
+
normalizeObjectStorageRequest,
|
|
620
|
+
normalizeFilesystemStorageRequest,
|
|
621
|
+
normalizeStorageRequest,
|
|
622
|
+
};
|
|
623
|
+
};
|