@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
package/ts/platform/storage.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { TSecretManagementScope } from '../data/secret.js';
|
|
2
|
+
import {
|
|
3
|
+
canonicalizeStrictJson,
|
|
4
|
+
createCanonicalJsonSha256Hex,
|
|
5
|
+
} from '../private/canonicaljson.js';
|
|
6
|
+
import { createStorageContractReaders } from '../private/storagecontract.js';
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Backend-neutral storage vocabulary shared by App Store manifests, control
|
|
@@ -186,8 +191,12 @@ export interface IResolvedStorageBindingBase {
|
|
|
186
191
|
/** Stable App Store storageRequests[].id. */
|
|
187
192
|
requestId: string;
|
|
188
193
|
/**
|
|
189
|
-
* Canonical digest of the normalized request
|
|
190
|
-
*
|
|
194
|
+
* Canonical digest of the normalized request, exactly as
|
|
195
|
+
* `createStorageRequestSha256` states it: bare lowercase hex over the request
|
|
196
|
+
* alone. Adapters use it to distinguish an idempotent reconciliation from a
|
|
197
|
+
* migration, and a consumer re-derives it to recognise the request it asked
|
|
198
|
+
* for, so the control plane and the consumer call that one function rather
|
|
199
|
+
* than each deriving a value the other cannot reproduce.
|
|
191
200
|
*/
|
|
192
201
|
requestDigest: string;
|
|
193
202
|
kind: TStorageResourceKind;
|
|
@@ -232,3 +241,51 @@ extends IResolvedStorageBindingBase {
|
|
|
232
241
|
export type TResolvedStorageBinding =
|
|
233
242
|
| IResolvedFilesystemStorageBinding
|
|
234
243
|
| IResolvedObjectStorageBinding;
|
|
244
|
+
|
|
245
|
+
export class StorageContractError extends Error {
|
|
246
|
+
public constructor(reasonArg: string) {
|
|
247
|
+
super(`storage contract error: ${reasonArg}`);
|
|
248
|
+
this.name = 'StorageContractError';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const fail = (reasonArg: string): never => {
|
|
253
|
+
throw new StorageContractError(reasonArg);
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const { normalizeStorageRequest } = createStorageContractReaders(fail);
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The exact bytes a storage request's digest is taken over: the request normalized to its contract
|
|
260
|
+
* shape, written as strict canonical JSON — keys in UTF-16 code-unit order, no whitespace, no
|
|
261
|
+
* absent member carried as `null`.
|
|
262
|
+
*
|
|
263
|
+
* Anything that is not a storage request is refused rather than hashed, because a digest of an
|
|
264
|
+
* unjudged body would name a request no adapter could fulfil.
|
|
265
|
+
*/
|
|
266
|
+
export const canonicalizeStorageRequest = (valueArg: unknown): string => {
|
|
267
|
+
return canonicalizeStrictJson(
|
|
268
|
+
normalizeStorageRequest(valueArg, 'storage request'),
|
|
269
|
+
fail,
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The storage request digest: bare lowercase hex SHA-256 over the canonical request, with no
|
|
275
|
+
* prefix and nothing but the request in the hashed value.
|
|
276
|
+
*
|
|
277
|
+
* A control plane issues this value on a binding and a consumer re-derives it from the request it
|
|
278
|
+
* declared; the two only agree because they call this one function. A second derivation elsewhere
|
|
279
|
+
* — a prefix, or a wrapper object around the request — is a different value for the same binding,
|
|
280
|
+
* and the consumer would keep rejecting a binding that is in fact its own.
|
|
281
|
+
*/
|
|
282
|
+
export const createStorageRequestSha256 = async (
|
|
283
|
+
valueArg: unknown,
|
|
284
|
+
): Promise<string> => {
|
|
285
|
+
return createCanonicalJsonSha256Hex(canonicalizeStorageRequest(valueArg), fail);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export {
|
|
289
|
+
storageRequestCanonicalDigestGoldenVectors,
|
|
290
|
+
type IStorageRequestCanonicalDigestGoldenVector,
|
|
291
|
+
} from './storage.golden.js';
|
|
@@ -8,6 +8,10 @@ import {
|
|
|
8
8
|
deepFreezeValue,
|
|
9
9
|
strictCanonicalJsonRules,
|
|
10
10
|
} from '../private/canonicaljson.js';
|
|
11
|
+
import {
|
|
12
|
+
createStorageContractReaders,
|
|
13
|
+
storageContractStringLimits,
|
|
14
|
+
} from '../private/storagecontract.js';
|
|
11
15
|
import type {
|
|
12
16
|
IResolvedObjectStorageBinding,
|
|
13
17
|
IResolvedStoragePolicyRef,
|
|
@@ -16,11 +20,13 @@ import type {
|
|
|
16
20
|
|
|
17
21
|
export const storageMigrationContractLimits = Object.freeze({
|
|
18
22
|
maximumControlJsonBytes: 256 * 1024,
|
|
19
|
-
maximumConfiguredStringBytes:
|
|
23
|
+
maximumConfiguredStringBytes:
|
|
24
|
+
storageContractStringLimits.maximumConfiguredStringBytes,
|
|
20
25
|
maximumIssueMessageBytes: 4096,
|
|
21
26
|
maximumAbortReasonBytes: 4096,
|
|
22
|
-
maximumEnvironmentKeyBytes:
|
|
23
|
-
|
|
27
|
+
maximumEnvironmentKeyBytes:
|
|
28
|
+
storageContractStringLimits.maximumEnvironmentKeyBytes,
|
|
29
|
+
maximumSecretPathBytes: storageContractStringLimits.maximumSecretPathBytes,
|
|
24
30
|
});
|
|
25
31
|
|
|
26
32
|
export const storageMigrationCanonicalJsonRules = strictCanonicalJsonRules;
|
|
@@ -408,9 +414,28 @@ const fail = (reasonArg: string): never => {
|
|
|
408
414
|
throw new StorageMigrationContractError(reasonArg);
|
|
409
415
|
};
|
|
410
416
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
417
|
+
/**
|
|
418
|
+
* The storage contract's readers, reporting every refusal as a migration contract error. The
|
|
419
|
+
* request a migration targets is the same request a control plane reconciles, so it is judged by
|
|
420
|
+
* the same rules and, through them, digests to the same bytes.
|
|
421
|
+
*/
|
|
422
|
+
const {
|
|
423
|
+
utf8ByteLength,
|
|
424
|
+
readRecord,
|
|
425
|
+
readDenseArray,
|
|
426
|
+
assertKeys,
|
|
427
|
+
assertExactKeys,
|
|
428
|
+
readString,
|
|
429
|
+
readIdentifier,
|
|
430
|
+
readSha256,
|
|
431
|
+
readSafeInteger,
|
|
432
|
+
readBoolean,
|
|
433
|
+
readLiteral,
|
|
434
|
+
readCapacityQuantity,
|
|
435
|
+
capacityQuantityBytes,
|
|
436
|
+
normalizeDelivery,
|
|
437
|
+
normalizeObjectStorageRequest,
|
|
438
|
+
} = createStorageContractReaders(fail);
|
|
414
439
|
|
|
415
440
|
/**
|
|
416
441
|
* Enforce this against Content-Length and the running request byte count
|
|
@@ -427,200 +452,6 @@ export const assertStorageMigrationControlIngressByteLength = (
|
|
|
427
452
|
);
|
|
428
453
|
};
|
|
429
454
|
|
|
430
|
-
const readRecord = (
|
|
431
|
-
valueArg: unknown,
|
|
432
|
-
fieldNameArg: string,
|
|
433
|
-
): Record<string, unknown> => {
|
|
434
|
-
if (!valueArg || typeof valueArg !== 'object' || Array.isArray(valueArg)) {
|
|
435
|
-
return fail(`${fieldNameArg} must be a plain object`);
|
|
436
|
-
}
|
|
437
|
-
const prototype = Object.getPrototypeOf(valueArg);
|
|
438
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
439
|
-
return fail(`${fieldNameArg} must be a plain object`);
|
|
440
|
-
}
|
|
441
|
-
const record = valueArg as Record<string, unknown>;
|
|
442
|
-
const ownKeys = Reflect.ownKeys(record);
|
|
443
|
-
if (ownKeys.some((keyArg) => typeof keyArg !== 'string')) {
|
|
444
|
-
return fail(`${fieldNameArg} must not contain symbol keys`);
|
|
445
|
-
}
|
|
446
|
-
for (const key of ownKeys as string[]) {
|
|
447
|
-
const descriptor = Object.getOwnPropertyDescriptor(record, key);
|
|
448
|
-
if (
|
|
449
|
-
!descriptor ||
|
|
450
|
-
!descriptor.enumerable ||
|
|
451
|
-
!Object.hasOwn(descriptor, 'value') ||
|
|
452
|
-
descriptor.value === undefined
|
|
453
|
-
) {
|
|
454
|
-
return fail(
|
|
455
|
-
`${fieldNameArg} must contain enumerable JSON data properties only`,
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return record;
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
const readDenseArray = (
|
|
463
|
-
valueArg: unknown,
|
|
464
|
-
fieldNameArg: string,
|
|
465
|
-
minimumLengthArg: number,
|
|
466
|
-
maximumLengthArg: number,
|
|
467
|
-
): unknown[] => {
|
|
468
|
-
if (
|
|
469
|
-
!Array.isArray(valueArg) ||
|
|
470
|
-
Object.getPrototypeOf(valueArg) !== Array.prototype
|
|
471
|
-
) {
|
|
472
|
-
return fail(`${fieldNameArg} must be a dense plain array`);
|
|
473
|
-
}
|
|
474
|
-
if (
|
|
475
|
-
valueArg.length < minimumLengthArg ||
|
|
476
|
-
valueArg.length > maximumLengthArg
|
|
477
|
-
) {
|
|
478
|
-
return fail(
|
|
479
|
-
`${fieldNameArg} must contain between ${minimumLengthArg} and ${maximumLengthArg} entries`,
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
for (let index = 0; index < valueArg.length; index++) {
|
|
483
|
-
if (!Object.hasOwn(valueArg, index)) {
|
|
484
|
-
return fail(`${fieldNameArg} must not contain sparse entries`);
|
|
485
|
-
}
|
|
486
|
-
const descriptor = Object.getOwnPropertyDescriptor(valueArg, String(index));
|
|
487
|
-
if (
|
|
488
|
-
!descriptor ||
|
|
489
|
-
!descriptor.enumerable ||
|
|
490
|
-
!Object.hasOwn(descriptor, 'value') ||
|
|
491
|
-
descriptor.value === undefined
|
|
492
|
-
) {
|
|
493
|
-
return fail(
|
|
494
|
-
`${fieldNameArg} must contain enumerable JSON data entries only`,
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
for (const key of Reflect.ownKeys(valueArg)) {
|
|
499
|
-
if (key === 'length') continue;
|
|
500
|
-
if (
|
|
501
|
-
typeof key !== 'string' ||
|
|
502
|
-
!/^(?:0|[1-9][0-9]*)$/.test(key) ||
|
|
503
|
-
Number(key) >= valueArg.length
|
|
504
|
-
) {
|
|
505
|
-
return fail(`${fieldNameArg} must not contain extra properties`);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
return valueArg;
|
|
509
|
-
};
|
|
510
|
-
|
|
511
|
-
const assertKeys = (
|
|
512
|
-
recordArg: Record<string, unknown>,
|
|
513
|
-
requiredKeysArg: readonly string[],
|
|
514
|
-
optionalKeysArg: readonly string[],
|
|
515
|
-
fieldNameArg: string,
|
|
516
|
-
): void => {
|
|
517
|
-
const actualKeys = Object.keys(recordArg).sort();
|
|
518
|
-
const allowedKeys = new Set([...requiredKeysArg, ...optionalKeysArg]);
|
|
519
|
-
if (
|
|
520
|
-
requiredKeysArg.some((keyArg) => !Object.hasOwn(recordArg, keyArg)) ||
|
|
521
|
-
actualKeys.some((keyArg) => !allowedKeys.has(keyArg))
|
|
522
|
-
) {
|
|
523
|
-
fail(`${fieldNameArg} does not match its exact schema`);
|
|
524
|
-
}
|
|
525
|
-
};
|
|
526
|
-
|
|
527
|
-
const assertExactKeys = (
|
|
528
|
-
recordArg: Record<string, unknown>,
|
|
529
|
-
expectedKeysArg: readonly string[],
|
|
530
|
-
fieldNameArg: string,
|
|
531
|
-
): void => {
|
|
532
|
-
assertKeys(recordArg, expectedKeysArg, [], fieldNameArg);
|
|
533
|
-
};
|
|
534
|
-
|
|
535
|
-
const readString = (
|
|
536
|
-
valueArg: unknown,
|
|
537
|
-
fieldNameArg: string,
|
|
538
|
-
maximumBytesArg: number =
|
|
539
|
-
storageMigrationContractLimits.maximumConfiguredStringBytes,
|
|
540
|
-
): string => {
|
|
541
|
-
if (
|
|
542
|
-
typeof valueArg !== 'string' ||
|
|
543
|
-
valueArg.length === 0 ||
|
|
544
|
-
utf8ByteLength(valueArg) > maximumBytesArg ||
|
|
545
|
-
/[\u0000-\u001f\u007f]/u.test(valueArg)
|
|
546
|
-
) {
|
|
547
|
-
return fail(`${fieldNameArg} must be a bounded non-empty JSON string`);
|
|
548
|
-
}
|
|
549
|
-
for (let index = 0; index < valueArg.length; index++) {
|
|
550
|
-
const codeUnit = valueArg.charCodeAt(index);
|
|
551
|
-
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
552
|
-
const following = valueArg.charCodeAt(index + 1);
|
|
553
|
-
if (following < 0xdc00 || following > 0xdfff) {
|
|
554
|
-
return fail(`${fieldNameArg} contains an unpaired UTF-16 surrogate`);
|
|
555
|
-
}
|
|
556
|
-
index++;
|
|
557
|
-
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
558
|
-
return fail(`${fieldNameArg} contains an unpaired UTF-16 surrogate`);
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
return valueArg;
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
const canonicalIdPattern = /^[A-Za-z0-9][A-Za-z0-9._:@-]{0,191}$/;
|
|
565
|
-
const environmentKeyPattern = /^[A-Z_][A-Z0-9_]{0,255}$/;
|
|
566
|
-
const sha256Pattern = /^[a-f0-9]{64}$/;
|
|
567
|
-
const capacityPattern = /^[1-9][0-9]*(?:KiB|MiB|GiB|TiB)$/;
|
|
568
|
-
|
|
569
|
-
const readIdentifier = (valueArg: unknown, fieldNameArg: string): string => {
|
|
570
|
-
if (typeof valueArg !== 'string' || !canonicalIdPattern.test(valueArg)) {
|
|
571
|
-
return fail(`${fieldNameArg} must be a canonical identifier`);
|
|
572
|
-
}
|
|
573
|
-
return valueArg;
|
|
574
|
-
};
|
|
575
|
-
|
|
576
|
-
const readSha256 = (valueArg: unknown, fieldNameArg: string): string => {
|
|
577
|
-
if (typeof valueArg !== 'string' || !sha256Pattern.test(valueArg)) {
|
|
578
|
-
return fail(`${fieldNameArg} must be a bare lowercase 64-hex SHA-256 digest`);
|
|
579
|
-
}
|
|
580
|
-
return valueArg;
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
const readSafeInteger = (
|
|
584
|
-
valueArg: unknown,
|
|
585
|
-
fieldNameArg: string,
|
|
586
|
-
minimumArg: number,
|
|
587
|
-
maximumArg = Number.MAX_SAFE_INTEGER,
|
|
588
|
-
): number => {
|
|
589
|
-
if (
|
|
590
|
-
typeof valueArg !== 'number' ||
|
|
591
|
-
!Number.isSafeInteger(valueArg) ||
|
|
592
|
-
Object.is(valueArg, -0) ||
|
|
593
|
-
valueArg < minimumArg ||
|
|
594
|
-
valueArg > maximumArg
|
|
595
|
-
) {
|
|
596
|
-
return fail(
|
|
597
|
-
`${fieldNameArg} must be a safe integer between ${minimumArg} and ${maximumArg}`,
|
|
598
|
-
);
|
|
599
|
-
}
|
|
600
|
-
return valueArg;
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
const readBoolean = (valueArg: unknown, fieldNameArg: string): boolean => {
|
|
604
|
-
if (typeof valueArg !== 'boolean') {
|
|
605
|
-
return fail(`${fieldNameArg} must be a boolean`);
|
|
606
|
-
}
|
|
607
|
-
return valueArg;
|
|
608
|
-
};
|
|
609
|
-
|
|
610
|
-
const readLiteral = <TLiteral extends string>(
|
|
611
|
-
valueArg: unknown,
|
|
612
|
-
allowedArg: readonly TLiteral[],
|
|
613
|
-
fieldNameArg: string,
|
|
614
|
-
): TLiteral => {
|
|
615
|
-
if (
|
|
616
|
-
typeof valueArg !== 'string' ||
|
|
617
|
-
!allowedArg.includes(valueArg as TLiteral)
|
|
618
|
-
) {
|
|
619
|
-
return fail(`${fieldNameArg} is outside the supported values`);
|
|
620
|
-
}
|
|
621
|
-
return valueArg as TLiteral;
|
|
622
|
-
};
|
|
623
|
-
|
|
624
455
|
const canonicalize = (valueArg: unknown): string => {
|
|
625
456
|
return canonicalizeStrictJson(valueArg, fail);
|
|
626
457
|
};
|
|
@@ -777,50 +608,6 @@ const normalizeStorageClass = (
|
|
|
777
608
|
};
|
|
778
609
|
};
|
|
779
610
|
|
|
780
|
-
const readCapacityQuantity = (
|
|
781
|
-
valueArg: unknown,
|
|
782
|
-
fieldNameArg: string,
|
|
783
|
-
): string => {
|
|
784
|
-
if (typeof valueArg !== 'string' || !capacityPattern.test(valueArg)) {
|
|
785
|
-
return fail(`${fieldNameArg} must be a canonical base-2 capacity quantity`);
|
|
786
|
-
}
|
|
787
|
-
const numericPart = valueArg.replace(/(?:KiB|MiB|GiB|TiB)$/, '');
|
|
788
|
-
if (!Number.isSafeInteger(Number(numericPart))) {
|
|
789
|
-
return fail(`${fieldNameArg} exceeds the safe integer quantity range`);
|
|
790
|
-
}
|
|
791
|
-
return valueArg;
|
|
792
|
-
};
|
|
793
|
-
|
|
794
|
-
const capacityQuantityBytes = (valueArg: string): bigint => {
|
|
795
|
-
const units: Record<string, bigint> = {
|
|
796
|
-
KiB: 1024n,
|
|
797
|
-
MiB: 1024n ** 2n,
|
|
798
|
-
GiB: 1024n ** 3n,
|
|
799
|
-
TiB: 1024n ** 4n,
|
|
800
|
-
};
|
|
801
|
-
const unit = valueArg.slice(-3);
|
|
802
|
-
return BigInt(valueArg.slice(0, -3)) * units[unit];
|
|
803
|
-
};
|
|
804
|
-
|
|
805
|
-
const normalizeCapacityRequest = (
|
|
806
|
-
valueArg: unknown,
|
|
807
|
-
fieldNameArg: string,
|
|
808
|
-
): NonNullable<IAppStoreObjectStorageRequest['capacity']> => {
|
|
809
|
-
const value = readRecord(valueArg, fieldNameArg);
|
|
810
|
-
assertKeys(value, ['request'], ['limit'], fieldNameArg);
|
|
811
|
-
const request = readCapacityQuantity(value.request, `${fieldNameArg}.request`);
|
|
812
|
-
const limit = Object.hasOwn(value, 'limit')
|
|
813
|
-
? readCapacityQuantity(value.limit, `${fieldNameArg}.limit`)
|
|
814
|
-
: undefined;
|
|
815
|
-
if (limit && capacityQuantityBytes(limit) < capacityQuantityBytes(request)) {
|
|
816
|
-
return fail(`${fieldNameArg}.limit must be at least request`);
|
|
817
|
-
}
|
|
818
|
-
return {
|
|
819
|
-
request,
|
|
820
|
-
...(limit ? { limit } : {}),
|
|
821
|
-
};
|
|
822
|
-
};
|
|
823
|
-
|
|
824
611
|
const normalizeResolvedCapacity = (
|
|
825
612
|
valueArg: unknown,
|
|
826
613
|
fieldNameArg: string,
|
|
@@ -867,102 +654,6 @@ const normalizeResolvedCapacity = (
|
|
|
867
654
|
};
|
|
868
655
|
};
|
|
869
656
|
|
|
870
|
-
const readEnvironmentKey = (
|
|
871
|
-
valueArg: unknown,
|
|
872
|
-
fieldNameArg: string,
|
|
873
|
-
): string => {
|
|
874
|
-
if (
|
|
875
|
-
typeof valueArg !== 'string' ||
|
|
876
|
-
utf8ByteLength(valueArg) >
|
|
877
|
-
storageMigrationContractLimits.maximumEnvironmentKeyBytes ||
|
|
878
|
-
!environmentKeyPattern.test(valueArg)
|
|
879
|
-
) {
|
|
880
|
-
return fail(`${fieldNameArg} must be a canonical environment key`);
|
|
881
|
-
}
|
|
882
|
-
return valueArg;
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
const normalizeDelivery = (
|
|
886
|
-
valueArg: unknown,
|
|
887
|
-
fieldNameArg: string,
|
|
888
|
-
): IResolvedObjectStorageBinding['delivery'] => {
|
|
889
|
-
const value = readRecord(valueArg, fieldNameArg);
|
|
890
|
-
if (value.type === 'launcher-environment') {
|
|
891
|
-
assertExactKeys(value, ['type', 'keys'], fieldNameArg);
|
|
892
|
-
const keys = readRecord(value.keys, `${fieldNameArg}.keys`);
|
|
893
|
-
assertKeys(
|
|
894
|
-
keys,
|
|
895
|
-
['endpoint', 'bucket', 'region', 'accessKeyId', 'secretAccessKey'],
|
|
896
|
-
['sessionToken'],
|
|
897
|
-
`${fieldNameArg}.keys`,
|
|
898
|
-
);
|
|
899
|
-
const normalizedKeys = {
|
|
900
|
-
endpoint: readEnvironmentKey(
|
|
901
|
-
keys.endpoint,
|
|
902
|
-
`${fieldNameArg}.keys.endpoint`,
|
|
903
|
-
),
|
|
904
|
-
bucket: readEnvironmentKey(keys.bucket, `${fieldNameArg}.keys.bucket`),
|
|
905
|
-
region: readEnvironmentKey(keys.region, `${fieldNameArg}.keys.region`),
|
|
906
|
-
accessKeyId: readEnvironmentKey(
|
|
907
|
-
keys.accessKeyId,
|
|
908
|
-
`${fieldNameArg}.keys.accessKeyId`,
|
|
909
|
-
),
|
|
910
|
-
secretAccessKey: readEnvironmentKey(
|
|
911
|
-
keys.secretAccessKey,
|
|
912
|
-
`${fieldNameArg}.keys.secretAccessKey`,
|
|
913
|
-
),
|
|
914
|
-
...(Object.hasOwn(keys, 'sessionToken')
|
|
915
|
-
? {
|
|
916
|
-
sessionToken: readEnvironmentKey(
|
|
917
|
-
keys.sessionToken,
|
|
918
|
-
`${fieldNameArg}.keys.sessionToken`,
|
|
919
|
-
),
|
|
920
|
-
}
|
|
921
|
-
: {}),
|
|
922
|
-
};
|
|
923
|
-
if (
|
|
924
|
-
new Set(Object.values(normalizedKeys)).size !==
|
|
925
|
-
Object.keys(normalizedKeys).length
|
|
926
|
-
) {
|
|
927
|
-
return fail(`${fieldNameArg}.keys values must be distinct`);
|
|
928
|
-
}
|
|
929
|
-
return { type: 'launcher-environment', keys: normalizedKeys };
|
|
930
|
-
}
|
|
931
|
-
if (value.type === 'file') {
|
|
932
|
-
assertExactKeys(value, ['type', 'targetPath', 'format', 'uid', 'gid', 'mode'], fieldNameArg);
|
|
933
|
-
const targetPath = readString(
|
|
934
|
-
value.targetPath,
|
|
935
|
-
`${fieldNameArg}.targetPath`,
|
|
936
|
-
storageMigrationContractLimits.maximumSecretPathBytes,
|
|
937
|
-
);
|
|
938
|
-
if (
|
|
939
|
-
!targetPath.startsWith('/run/secrets/') ||
|
|
940
|
-
targetPath.includes('\\') ||
|
|
941
|
-
targetPath.split('/').some((segmentArg) =>
|
|
942
|
-
segmentArg === '.' || segmentArg === '..'
|
|
943
|
-
)
|
|
944
|
-
) {
|
|
945
|
-
return fail(
|
|
946
|
-
`${fieldNameArg}.targetPath must be a canonical path below /run/secrets`,
|
|
947
|
-
);
|
|
948
|
-
}
|
|
949
|
-
if (value.format !== 'servezone-object-storage') {
|
|
950
|
-
return fail(`${fieldNameArg}.format is unsupported`);
|
|
951
|
-
}
|
|
952
|
-
return {
|
|
953
|
-
type: 'file',
|
|
954
|
-
targetPath,
|
|
955
|
-
format: 'servezone-object-storage',
|
|
956
|
-
uid: readSafeInteger(value.uid, `${fieldNameArg}.uid`, 0, 2_147_483_647),
|
|
957
|
-
gid: readSafeInteger(value.gid, `${fieldNameArg}.gid`, 0, 2_147_483_647),
|
|
958
|
-
mode: value.mode === 0o400 || value.mode === 0o440
|
|
959
|
-
? value.mode
|
|
960
|
-
: fail(`${fieldNameArg}.mode must be 0400 or 0440`),
|
|
961
|
-
};
|
|
962
|
-
}
|
|
963
|
-
return fail(`${fieldNameArg}.type is unsupported`);
|
|
964
|
-
};
|
|
965
|
-
|
|
966
657
|
const normalizeCredentialManagementScope = (
|
|
967
658
|
valueArg: unknown,
|
|
968
659
|
fieldNameArg: string,
|
|
@@ -974,106 +665,6 @@ const normalizeCredentialManagementScope = (
|
|
|
974
665
|
return value as IResolvedObjectStorageBinding['credentialManagementScope'];
|
|
975
666
|
};
|
|
976
667
|
|
|
977
|
-
const normalizeObjectStorageProtection = (
|
|
978
|
-
valueArg: unknown,
|
|
979
|
-
fieldNameArg: string,
|
|
980
|
-
): NonNullable<IAppStoreObjectStorageRequest['protection']> => {
|
|
981
|
-
const value = readRecord(valueArg, fieldNameArg);
|
|
982
|
-
assertKeys(
|
|
983
|
-
value,
|
|
984
|
-
[],
|
|
985
|
-
['backup', 'versioning', 'retentionDays'],
|
|
986
|
-
fieldNameArg,
|
|
987
|
-
);
|
|
988
|
-
if (Object.keys(value).length === 0) {
|
|
989
|
-
return fail(`${fieldNameArg} must not be empty`);
|
|
990
|
-
}
|
|
991
|
-
return {
|
|
992
|
-
...(Object.hasOwn(value, 'backup')
|
|
993
|
-
? {
|
|
994
|
-
backup: value.backup === 'required'
|
|
995
|
-
? 'required' as const
|
|
996
|
-
: fail(`${fieldNameArg}.backup must be required`),
|
|
997
|
-
}
|
|
998
|
-
: {}),
|
|
999
|
-
...(Object.hasOwn(value, 'versioning')
|
|
1000
|
-
? {
|
|
1001
|
-
versioning: value.versioning === 'required'
|
|
1002
|
-
? 'required' as const
|
|
1003
|
-
: fail(`${fieldNameArg}.versioning must be required`),
|
|
1004
|
-
}
|
|
1005
|
-
: {}),
|
|
1006
|
-
...(Object.hasOwn(value, 'retentionDays')
|
|
1007
|
-
? {
|
|
1008
|
-
retentionDays: readSafeInteger(
|
|
1009
|
-
value.retentionDays,
|
|
1010
|
-
`${fieldNameArg}.retentionDays`,
|
|
1011
|
-
1,
|
|
1012
|
-
365_000,
|
|
1013
|
-
),
|
|
1014
|
-
}
|
|
1015
|
-
: {}),
|
|
1016
|
-
};
|
|
1017
|
-
};
|
|
1018
|
-
|
|
1019
|
-
const normalizeObjectStorageRequest = (
|
|
1020
|
-
valueArg: unknown,
|
|
1021
|
-
fieldNameArg: string,
|
|
1022
|
-
): IAppStoreObjectStorageRequest => {
|
|
1023
|
-
const value = readRecord(valueArg, fieldNameArg);
|
|
1024
|
-
assertKeys(
|
|
1025
|
-
value,
|
|
1026
|
-
[
|
|
1027
|
-
'id',
|
|
1028
|
-
'kind',
|
|
1029
|
-
'storageClass',
|
|
1030
|
-
'accessMode',
|
|
1031
|
-
'reclaimPolicy',
|
|
1032
|
-
'delivery',
|
|
1033
|
-
],
|
|
1034
|
-
['capacity', 'protection'],
|
|
1035
|
-
fieldNameArg,
|
|
1036
|
-
);
|
|
1037
|
-
if (value.kind !== 'objectStorage') {
|
|
1038
|
-
return fail(`${fieldNameArg}.kind must be objectStorage`);
|
|
1039
|
-
}
|
|
1040
|
-
return {
|
|
1041
|
-
id: readIdentifier(value.id, `${fieldNameArg}.id`),
|
|
1042
|
-
kind: 'objectStorage',
|
|
1043
|
-
storageClass: readIdentifier(
|
|
1044
|
-
value.storageClass,
|
|
1045
|
-
`${fieldNameArg}.storageClass`,
|
|
1046
|
-
),
|
|
1047
|
-
accessMode: readLiteral(
|
|
1048
|
-
value.accessMode,
|
|
1049
|
-
['readOnly', 'readWrite'] as const,
|
|
1050
|
-
`${fieldNameArg}.accessMode`,
|
|
1051
|
-
),
|
|
1052
|
-
reclaimPolicy: readLiteral(
|
|
1053
|
-
value.reclaimPolicy,
|
|
1054
|
-
['retain', 'delete'] as const,
|
|
1055
|
-
`${fieldNameArg}.reclaimPolicy`,
|
|
1056
|
-
),
|
|
1057
|
-
delivery: normalizeDelivery(value.delivery, `${fieldNameArg}.delivery`),
|
|
1058
|
-
...(Object.hasOwn(value, 'capacity')
|
|
1059
|
-
? {
|
|
1060
|
-
capacity: normalizeCapacityRequest(
|
|
1061
|
-
value.capacity,
|
|
1062
|
-
`${fieldNameArg}.capacity`,
|
|
1063
|
-
),
|
|
1064
|
-
}
|
|
1065
|
-
: {}),
|
|
1066
|
-
...(Object.hasOwn(value, 'protection')
|
|
1067
|
-
? {
|
|
1068
|
-
protection: normalizeObjectStorageProtection(
|
|
1069
|
-
value.protection,
|
|
1070
|
-
`${fieldNameArg}.protection`,
|
|
1071
|
-
),
|
|
1072
|
-
}
|
|
1073
|
-
: {}),
|
|
1074
|
-
};
|
|
1075
|
-
};
|
|
1076
|
-
|
|
1077
668
|
const normalizeCapabilities = (
|
|
1078
669
|
valueArg: unknown,
|
|
1079
670
|
fieldNameArg: string,
|