@serve.zone/interfaces 23.2.0 → 24.0.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 +9 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/appstore/types.d.ts +2 -2
- package/dist_ts/appstore/types.js +1 -1
- package/dist_ts/data/clusternode.d.ts +85 -0
- package/dist_ts/data/clusternode.js +355 -1
- package/dist_ts/data/secret.d.ts +51 -4
- package/dist_ts/data/secret.js +326 -10
- package/dist_ts/requests/secret.d.ts +33 -6
- package/dist_ts/requests/secret.js +198 -1
- package/dist_ts/runtime.d.ts +52 -29
- package/dist_ts/runtime.js +239 -94
- package/dist_ts/runtime.workloadinit.d.ts +129 -0
- package/dist_ts/runtime.workloadinit.js +405 -0
- package/package.json +1 -1
- package/readme.md +45 -17
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/appstore/types.ts +2 -2
- package/ts/data/clusternode.ts +513 -0
- package/ts/data/secret.ts +402 -16
- package/ts/requests/secret.ts +248 -6
- package/ts/runtime.ts +366 -141
- package/ts/runtime.workloadinit.ts +582 -0
package/ts/data/secret.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type TSecretVersionLifecycleState =
|
|
|
14
14
|
| 'active'
|
|
15
15
|
| 'retired'
|
|
16
16
|
| 'revoked'
|
|
17
|
+
| 'purge-pending'
|
|
17
18
|
| 'purged';
|
|
18
19
|
|
|
19
20
|
/** Retired versions remain usable only through an already-accepted manifest. */
|
|
@@ -89,6 +90,10 @@ export interface ISecretVersionRetentionReference {
|
|
|
89
90
|
expiresAt?: number;
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
export const secretVersionRetentionReferenceLimits = Object.freeze({
|
|
94
|
+
pageSizeMaximum: 256,
|
|
95
|
+
});
|
|
96
|
+
|
|
92
97
|
/** Browser-safe logical secret metadata. It never contains value material. */
|
|
93
98
|
export interface ISecretMetadata {
|
|
94
99
|
id: string;
|
|
@@ -116,15 +121,61 @@ export interface ISecretMetadata {
|
|
|
116
121
|
export interface ISecretVersionMetadata {
|
|
117
122
|
id: string;
|
|
118
123
|
secretId: string;
|
|
124
|
+
revision: number;
|
|
119
125
|
lifecycleState: TSecretVersionLifecycleState;
|
|
120
126
|
createdAt: number;
|
|
121
127
|
createdBy: string;
|
|
122
128
|
source: TSecretManagementSource;
|
|
123
129
|
retiredAt?: number;
|
|
124
130
|
revokedAt?: number;
|
|
131
|
+
purgeOperationId?: string;
|
|
125
132
|
purgedAt?: number;
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
/** Irreversibly erases every platform-managed copy after all references are gone. */
|
|
136
|
+
export interface ISecretVersionPurgeIntentV1 {
|
|
137
|
+
schemaVersion: 1;
|
|
138
|
+
mutationId: string;
|
|
139
|
+
secretId: string;
|
|
140
|
+
secretVersionId: string;
|
|
141
|
+
expectedSecretRevision: number;
|
|
142
|
+
expectedSecretVersionRevision: number;
|
|
143
|
+
expectedTargetSecretsRevision: number;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface ISecretVersionPurgeFailureV1 {
|
|
147
|
+
code: string;
|
|
148
|
+
retryable: boolean;
|
|
149
|
+
failedAt: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface ISecretVersionPurgeOperationBaseV1 {
|
|
153
|
+
schemaVersion: 1;
|
|
154
|
+
id: string;
|
|
155
|
+
revision: number;
|
|
156
|
+
intent: ISecretVersionPurgeIntentV1;
|
|
157
|
+
intentDigest: TSha256Digest;
|
|
158
|
+
createdAt: number;
|
|
159
|
+
updatedAt: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export type TSecretVersionPurgeOperationV1 =
|
|
163
|
+
| (ISecretVersionPurgeOperationBaseV1 & {
|
|
164
|
+
state: 'pending' | 'erasing';
|
|
165
|
+
failure?: never;
|
|
166
|
+
purgedAt?: never;
|
|
167
|
+
})
|
|
168
|
+
| (ISecretVersionPurgeOperationBaseV1 & {
|
|
169
|
+
state: 'failed';
|
|
170
|
+
failure: ISecretVersionPurgeFailureV1;
|
|
171
|
+
purgedAt?: never;
|
|
172
|
+
})
|
|
173
|
+
| (ISecretVersionPurgeOperationBaseV1 & {
|
|
174
|
+
state: 'succeeded';
|
|
175
|
+
failure?: never;
|
|
176
|
+
purgedAt: number;
|
|
177
|
+
});
|
|
178
|
+
|
|
128
179
|
export interface ISecretSetMetadata {
|
|
129
180
|
id: string;
|
|
130
181
|
organizationId: string;
|
|
@@ -370,12 +421,264 @@ const isSecretEnvironment = (valueArg: unknown): valueArg is string => (
|
|
|
370
421
|
typeof valueArg === 'string' && secretEnvironmentRegex.test(valueArg)
|
|
371
422
|
);
|
|
372
423
|
|
|
424
|
+
const isNonNegativeSafeInteger = (valueArg: unknown): valueArg is number => (
|
|
425
|
+
Number.isSafeInteger(valueArg) && (valueArg as number) >= 0
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const isPositiveSafeInteger = (valueArg: unknown): valueArg is number => (
|
|
429
|
+
Number.isSafeInteger(valueArg) && (valueArg as number) > 0
|
|
430
|
+
);
|
|
431
|
+
|
|
373
432
|
export const validateSecretMutationId = (mutationIdArg: unknown): string[] => (
|
|
374
433
|
isSecretIdentifier(mutationIdArg)
|
|
375
434
|
? []
|
|
376
435
|
: ['mutationId must be a canonical identifier']
|
|
377
436
|
);
|
|
378
437
|
|
|
438
|
+
export const validateSecretVersionRetentionReferences = (
|
|
439
|
+
referencesArg: unknown,
|
|
440
|
+
): string[] => {
|
|
441
|
+
if (!Array.isArray(referencesArg)
|
|
442
|
+
|| referencesArg.length > secretVersionRetentionReferenceLimits.pageSizeMaximum) {
|
|
443
|
+
return ['secret version retention references must be a bounded array'];
|
|
444
|
+
}
|
|
445
|
+
const errors: string[] = [];
|
|
446
|
+
const kinds = new Set<TSecretVersionRetentionReferenceKind>([
|
|
447
|
+
'desired-manifest',
|
|
448
|
+
'applied-manifest',
|
|
449
|
+
'previous-accepted-manifest',
|
|
450
|
+
'rollback-manifest',
|
|
451
|
+
'backup',
|
|
452
|
+
'retention-window',
|
|
453
|
+
]);
|
|
454
|
+
let previousSortKey: string | undefined;
|
|
455
|
+
for (const [index, referenceArg] of referencesArg.entries()) {
|
|
456
|
+
if (!isRecord(referenceArg)
|
|
457
|
+
|| !hasOnlyKeys(referenceArg, ['kind', 'resourceId', 'expiresAt'])
|
|
458
|
+
|| !kinds.has(referenceArg.kind as TSecretVersionRetentionReferenceKind)
|
|
459
|
+
|| !isSecretIdentifier(referenceArg.resourceId)
|
|
460
|
+
|| (referenceArg.expiresAt !== undefined
|
|
461
|
+
&& !isNonNegativeSafeInteger(referenceArg.expiresAt))) {
|
|
462
|
+
errors.push(`secret version retention references[${index}] must be canonical`);
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
const sortKey = `${referenceArg.kind}\0${referenceArg.resourceId}\0${
|
|
466
|
+
referenceArg.expiresAt === undefined
|
|
467
|
+
? ''
|
|
468
|
+
: String(referenceArg.expiresAt).padStart(16, '0')
|
|
469
|
+
}`;
|
|
470
|
+
if (previousSortKey !== undefined && sortKey <= previousSortKey) {
|
|
471
|
+
errors.push('secret version retention references must be uniquely sorted');
|
|
472
|
+
}
|
|
473
|
+
previousSortKey = sortKey;
|
|
474
|
+
}
|
|
475
|
+
return errors;
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
export const validateSecretVersionMetadata = (
|
|
479
|
+
versionArg: unknown,
|
|
480
|
+
): string[] => {
|
|
481
|
+
if (!isRecord(versionArg)) return ['secret version metadata must be an object'];
|
|
482
|
+
const errors: string[] = [];
|
|
483
|
+
if (!hasOnlyKeys(versionArg, [
|
|
484
|
+
'id',
|
|
485
|
+
'secretId',
|
|
486
|
+
'revision',
|
|
487
|
+
'lifecycleState',
|
|
488
|
+
'createdAt',
|
|
489
|
+
'createdBy',
|
|
490
|
+
'source',
|
|
491
|
+
'retiredAt',
|
|
492
|
+
'revokedAt',
|
|
493
|
+
'purgeOperationId',
|
|
494
|
+
'purgedAt',
|
|
495
|
+
])) {
|
|
496
|
+
errors.push('secret version metadata contains unsupported fields');
|
|
497
|
+
}
|
|
498
|
+
if (!isSecretIdentifier(versionArg.id) || !isSecretIdentifier(versionArg.secretId)) {
|
|
499
|
+
errors.push('secret version metadata identity must be canonical');
|
|
500
|
+
}
|
|
501
|
+
if (!isPositiveSafeInteger(versionArg.revision)) {
|
|
502
|
+
errors.push('secret version metadata revision must be positive');
|
|
503
|
+
}
|
|
504
|
+
if (!['active', 'retired', 'revoked', 'purge-pending', 'purged']
|
|
505
|
+
.includes(versionArg.lifecycleState as string)) {
|
|
506
|
+
errors.push('secret version metadata lifecycleState must be canonical');
|
|
507
|
+
}
|
|
508
|
+
if (!isNonNegativeSafeInteger(versionArg.createdAt)
|
|
509
|
+
|| !isSecretIdentifier(versionArg.createdBy)
|
|
510
|
+
|| !['operator', 'appstore', 'mail', 'oidc', 'webpush', 'platform', 'system']
|
|
511
|
+
.includes(versionArg.source as string)) {
|
|
512
|
+
errors.push('secret version metadata creation fields must be canonical');
|
|
513
|
+
}
|
|
514
|
+
for (const field of ['retiredAt', 'revokedAt', 'purgedAt'] as const) {
|
|
515
|
+
if (versionArg[field] !== undefined && !isNonNegativeSafeInteger(versionArg[field])) {
|
|
516
|
+
errors.push(`secret version metadata ${field} must be a non-negative integer`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const hasPurgeOperationId = isSecretIdentifier(versionArg.purgeOperationId);
|
|
520
|
+
if (versionArg.lifecycleState === 'purge-pending') {
|
|
521
|
+
if (!hasPurgeOperationId || versionArg.purgedAt !== undefined) {
|
|
522
|
+
errors.push('purge-pending secret version metadata requires only a purge operation');
|
|
523
|
+
}
|
|
524
|
+
} else if (versionArg.lifecycleState === 'purged') {
|
|
525
|
+
if (!hasPurgeOperationId || !isNonNegativeSafeInteger(versionArg.purgedAt)) {
|
|
526
|
+
errors.push('purged secret version metadata requires a purge operation and purgedAt');
|
|
527
|
+
}
|
|
528
|
+
} else if (versionArg.purgeOperationId !== undefined || versionArg.purgedAt !== undefined) {
|
|
529
|
+
errors.push('non-purging secret version metadata forbids purge fields');
|
|
530
|
+
}
|
|
531
|
+
return errors;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
export const validateSecretVersionPurgeIntent = (
|
|
535
|
+
intentArg: unknown,
|
|
536
|
+
): string[] => {
|
|
537
|
+
if (!isRecord(intentArg)
|
|
538
|
+
|| !hasExactKeys(intentArg, [
|
|
539
|
+
'schemaVersion',
|
|
540
|
+
'mutationId',
|
|
541
|
+
'secretId',
|
|
542
|
+
'secretVersionId',
|
|
543
|
+
'expectedSecretRevision',
|
|
544
|
+
'expectedSecretVersionRevision',
|
|
545
|
+
'expectedTargetSecretsRevision',
|
|
546
|
+
])) {
|
|
547
|
+
return ['secret version purge intent must use its exact schema'];
|
|
548
|
+
}
|
|
549
|
+
const errors = validateSecretMutationId(intentArg.mutationId);
|
|
550
|
+
if (intentArg.schemaVersion !== 1) {
|
|
551
|
+
errors.push('secret version purge intent schemaVersion must be 1');
|
|
552
|
+
}
|
|
553
|
+
for (const field of ['secretId', 'secretVersionId'] as const) {
|
|
554
|
+
if (!isSecretIdentifier(intentArg[field])) {
|
|
555
|
+
errors.push(`secret version purge intent ${field} must be canonical`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
for (const field of ['expectedSecretRevision', 'expectedSecretVersionRevision'] as const) {
|
|
559
|
+
if (!isPositiveSafeInteger(intentArg[field])) {
|
|
560
|
+
errors.push(`secret version purge intent ${field} must be positive`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
if (!isNonNegativeSafeInteger(intentArg.expectedTargetSecretsRevision)) {
|
|
564
|
+
errors.push('secret version purge intent expectedTargetSecretsRevision must be non-negative');
|
|
565
|
+
}
|
|
566
|
+
return errors;
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
export const createSecretVersionPurgeIntentDigestInput = (
|
|
570
|
+
intentArg: ISecretVersionPurgeIntentV1,
|
|
571
|
+
): string => JSON.stringify({
|
|
572
|
+
schemaVersion: intentArg.schemaVersion,
|
|
573
|
+
purpose: 'serve.zone/secret-version-purge',
|
|
574
|
+
mutationId: intentArg.mutationId,
|
|
575
|
+
secretId: intentArg.secretId,
|
|
576
|
+
secretVersionId: intentArg.secretVersionId,
|
|
577
|
+
expectedSecretRevision: intentArg.expectedSecretRevision,
|
|
578
|
+
expectedSecretVersionRevision: intentArg.expectedSecretVersionRevision,
|
|
579
|
+
expectedTargetSecretsRevision: intentArg.expectedTargetSecretsRevision,
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
export const computeSecretVersionPurgeIntentDigest = async (
|
|
583
|
+
intentArg: ISecretVersionPurgeIntentV1,
|
|
584
|
+
): Promise<TSha256Digest> => {
|
|
585
|
+
if (validateSecretVersionPurgeIntent(intentArg).length > 0) {
|
|
586
|
+
throw new Error('secret version purge intent must be canonical');
|
|
587
|
+
}
|
|
588
|
+
const digest = new Uint8Array(await globalThis.crypto.subtle.digest(
|
|
589
|
+
'SHA-256',
|
|
590
|
+
new TextEncoder().encode(createSecretVersionPurgeIntentDigestInput(intentArg)),
|
|
591
|
+
));
|
|
592
|
+
return `sha256:${[...digest]
|
|
593
|
+
.map((byteArg) => byteArg.toString(16).padStart(2, '0'))
|
|
594
|
+
.join('')}` as TSha256Digest;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
export const verifySecretVersionPurgeIntentDigest = async (
|
|
598
|
+
intentArg: ISecretVersionPurgeIntentV1,
|
|
599
|
+
digestArg: unknown,
|
|
600
|
+
): Promise<boolean> => {
|
|
601
|
+
try {
|
|
602
|
+
return typeof digestArg === 'string'
|
|
603
|
+
&& secretDigestRegex.test(digestArg)
|
|
604
|
+
&& digestArg === await computeSecretVersionPurgeIntentDigest(intentArg);
|
|
605
|
+
} catch {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
export const validateSecretVersionPurgeOperation = async (
|
|
611
|
+
operationArg: unknown,
|
|
612
|
+
): Promise<string[]> => {
|
|
613
|
+
if (!isRecord(operationArg)) return ['secret version purge operation must be an object'];
|
|
614
|
+
const state = operationArg.state;
|
|
615
|
+
const expectedKeys = state === 'failed'
|
|
616
|
+
? [
|
|
617
|
+
'schemaVersion', 'id', 'revision', 'intent', 'intentDigest', 'state',
|
|
618
|
+
'failure', 'createdAt', 'updatedAt',
|
|
619
|
+
]
|
|
620
|
+
: state === 'succeeded'
|
|
621
|
+
? [
|
|
622
|
+
'schemaVersion', 'id', 'revision', 'intent', 'intentDigest', 'state',
|
|
623
|
+
'purgedAt', 'createdAt', 'updatedAt',
|
|
624
|
+
]
|
|
625
|
+
: [
|
|
626
|
+
'schemaVersion', 'id', 'revision', 'intent', 'intentDigest', 'state',
|
|
627
|
+
'createdAt', 'updatedAt',
|
|
628
|
+
];
|
|
629
|
+
if (!['pending', 'erasing', 'failed', 'succeeded'].includes(state as string)
|
|
630
|
+
|| !hasExactKeys(operationArg, expectedKeys)) {
|
|
631
|
+
return ['secret version purge operation must use its exact state schema'];
|
|
632
|
+
}
|
|
633
|
+
const errors = validateSecretVersionPurgeIntent(operationArg.intent);
|
|
634
|
+
if (operationArg.schemaVersion !== 1) {
|
|
635
|
+
errors.push('secret version purge operation schemaVersion must be 1');
|
|
636
|
+
}
|
|
637
|
+
if (!isSecretIdentifier(operationArg.id) || !isPositiveSafeInteger(operationArg.revision)) {
|
|
638
|
+
errors.push('secret version purge operation identity and revision must be canonical');
|
|
639
|
+
}
|
|
640
|
+
if (!isNonNegativeSafeInteger(operationArg.createdAt)
|
|
641
|
+
|| !isNonNegativeSafeInteger(operationArg.updatedAt)
|
|
642
|
+
|| (operationArg.updatedAt as number) < (operationArg.createdAt as number)) {
|
|
643
|
+
errors.push('secret version purge operation timestamps must be ordered');
|
|
644
|
+
}
|
|
645
|
+
if (isRecord(operationArg.intent)
|
|
646
|
+
&& typeof operationArg.intentDigest === 'string'
|
|
647
|
+
&& secretDigestRegex.test(operationArg.intentDigest)) {
|
|
648
|
+
try {
|
|
649
|
+
if (!await verifySecretVersionPurgeIntentDigest(
|
|
650
|
+
operationArg.intent as unknown as ISecretVersionPurgeIntentV1,
|
|
651
|
+
operationArg.intentDigest,
|
|
652
|
+
)) {
|
|
653
|
+
errors.push('secret version purge operation intentDigest does not match');
|
|
654
|
+
}
|
|
655
|
+
} catch {
|
|
656
|
+
errors.push('secret version purge operation intentDigest cannot be verified');
|
|
657
|
+
}
|
|
658
|
+
} else {
|
|
659
|
+
errors.push('secret version purge operation intentDigest must be canonical');
|
|
660
|
+
}
|
|
661
|
+
if (state === 'failed') {
|
|
662
|
+
if (!isRecord(operationArg.failure)
|
|
663
|
+
|| !hasExactKeys(operationArg.failure, ['code', 'retryable', 'failedAt'])
|
|
664
|
+
|| typeof operationArg.failure.code !== 'string'
|
|
665
|
+
|| !/^[A-Z][A-Z0-9_]{0,127}$/.test(operationArg.failure.code)
|
|
666
|
+
|| typeof operationArg.failure.retryable !== 'boolean'
|
|
667
|
+
|| !isNonNegativeSafeInteger(operationArg.failure.failedAt)
|
|
668
|
+
|| (operationArg.failure.failedAt as number) < (operationArg.createdAt as number)
|
|
669
|
+
|| (operationArg.failure.failedAt as number) > (operationArg.updatedAt as number)) {
|
|
670
|
+
errors.push('secret version purge operation failure must be canonical');
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (state === 'succeeded'
|
|
674
|
+
&& (!isNonNegativeSafeInteger(operationArg.purgedAt)
|
|
675
|
+
|| (operationArg.purgedAt as number) < (operationArg.createdAt as number)
|
|
676
|
+
|| (operationArg.purgedAt as number) > (operationArg.updatedAt as number))) {
|
|
677
|
+
errors.push('succeeded secret version purge operation requires a canonical purgedAt');
|
|
678
|
+
}
|
|
679
|
+
return errors;
|
|
680
|
+
};
|
|
681
|
+
|
|
379
682
|
export const validateSecretOwner = (ownerArg: unknown): string[] => {
|
|
380
683
|
if (!isRecord(ownerArg)) return ['secret owner must be an object'];
|
|
381
684
|
const shapeByKind = {
|
|
@@ -446,6 +749,105 @@ export const validateSecretDelivery = (deliveryArg: unknown): string[] => {
|
|
|
446
749
|
return errors;
|
|
447
750
|
};
|
|
448
751
|
|
|
752
|
+
export const validateSecretMetadata = (secretArg: unknown): string[] => {
|
|
753
|
+
if (!isRecord(secretArg)) return ['secret metadata must be an object'];
|
|
754
|
+
const expectedKeys = [
|
|
755
|
+
'id',
|
|
756
|
+
'organizationId',
|
|
757
|
+
'owner',
|
|
758
|
+
'key',
|
|
759
|
+
'environment',
|
|
760
|
+
'name',
|
|
761
|
+
...(secretArg.description === undefined ? [] : ['description']),
|
|
762
|
+
'tags',
|
|
763
|
+
'managementSource',
|
|
764
|
+
'managementScope',
|
|
765
|
+
'defaultDelivery',
|
|
766
|
+
'lifecycleState',
|
|
767
|
+
...(secretArg.activeVersion === undefined ? [] : ['activeVersion']),
|
|
768
|
+
'versionCount',
|
|
769
|
+
'revision',
|
|
770
|
+
'createdAt',
|
|
771
|
+
'updatedAt',
|
|
772
|
+
...(secretArg.lastRotatedAt === undefined ? [] : ['lastRotatedAt']),
|
|
773
|
+
];
|
|
774
|
+
const errors: string[] = [];
|
|
775
|
+
if (!hasExactKeys(secretArg, expectedKeys)) {
|
|
776
|
+
errors.push('secret metadata must use its exact schema');
|
|
777
|
+
}
|
|
778
|
+
if (!isSecretIdentifier(secretArg.id) || !isSecretIdentifier(secretArg.organizationId)) {
|
|
779
|
+
errors.push('secret metadata identity must be canonical');
|
|
780
|
+
}
|
|
781
|
+
errors.push(...validateSecretOwner(secretArg.owner));
|
|
782
|
+
if (!isSecretKey(secretArg.key) || !isSecretEnvironment(secretArg.environment)) {
|
|
783
|
+
errors.push('secret metadata key and environment must be canonical');
|
|
784
|
+
}
|
|
785
|
+
if (typeof secretArg.name !== 'string'
|
|
786
|
+
|| secretArg.name.trim() !== secretArg.name
|
|
787
|
+
|| secretArg.name.length < 1
|
|
788
|
+
|| secretArg.name.length > 200
|
|
789
|
+
|| (secretArg.description !== undefined
|
|
790
|
+
&& (typeof secretArg.description !== 'string' || secretArg.description.length > 2000))) {
|
|
791
|
+
errors.push('secret metadata name and description must be bounded');
|
|
792
|
+
}
|
|
793
|
+
if (!Array.isArray(secretArg.tags) || secretArg.tags.length > 64) {
|
|
794
|
+
errors.push('secret metadata tags must be a bounded array');
|
|
795
|
+
} else {
|
|
796
|
+
const tagKeys: string[] = [];
|
|
797
|
+
for (const [index, tagArg] of secretArg.tags.entries()) {
|
|
798
|
+
if (!isRecord(tagArg)
|
|
799
|
+
|| !hasExactKeys(tagArg, ['key', 'value'])
|
|
800
|
+
|| typeof tagArg.key !== 'string'
|
|
801
|
+
|| tagArg.key.length < 1
|
|
802
|
+
|| tagArg.key.length > 100
|
|
803
|
+
|| typeof tagArg.value !== 'string'
|
|
804
|
+
|| tagArg.value.length > 500) {
|
|
805
|
+
errors.push(`secret metadata tags[${index}] must use the bounded key/value schema`);
|
|
806
|
+
} else {
|
|
807
|
+
tagKeys.push(tagArg.key);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (new Set(tagKeys).size !== tagKeys.length) {
|
|
811
|
+
errors.push('secret metadata tag keys must be unique');
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
if (!['operator', 'appstore', 'mail', 'oidc', 'webpush', 'platform', 'system']
|
|
815
|
+
.includes(secretArg.managementSource as string)) {
|
|
816
|
+
errors.push('secret metadata managementSource must be canonical');
|
|
817
|
+
} else {
|
|
818
|
+
errors.push(...validateSecretManagementScope(
|
|
819
|
+
secretArg.managementSource as TSecretManagementSource,
|
|
820
|
+
secretArg.managementScope,
|
|
821
|
+
));
|
|
822
|
+
}
|
|
823
|
+
errors.push(...validateSecretDelivery(secretArg.defaultDelivery));
|
|
824
|
+
if (!['active', 'retired', 'revoked', 'deleted'].includes(secretArg.lifecycleState as string)) {
|
|
825
|
+
errors.push('secret metadata lifecycleState must be canonical');
|
|
826
|
+
}
|
|
827
|
+
if (secretArg.activeVersion !== undefined) {
|
|
828
|
+
if (!isRecord(secretArg.activeVersion)
|
|
829
|
+
|| !hasExactKeys(secretArg.activeVersion, ['secretId', 'secretVersionId'])
|
|
830
|
+
|| secretArg.activeVersion.secretId !== secretArg.id
|
|
831
|
+
|| !isSecretIdentifier(secretArg.activeVersion.secretVersionId)) {
|
|
832
|
+
errors.push('secret metadata activeVersion must be a canonical self-reference');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (!isNonNegativeSafeInteger(secretArg.versionCount)
|
|
836
|
+
|| !isPositiveSafeInteger(secretArg.revision)
|
|
837
|
+
|| !isNonNegativeSafeInteger(secretArg.createdAt)
|
|
838
|
+
|| !isNonNegativeSafeInteger(secretArg.updatedAt)
|
|
839
|
+
|| (secretArg.updatedAt as number) < (secretArg.createdAt as number)) {
|
|
840
|
+
errors.push('secret metadata counts, revision, and timestamps must be canonical');
|
|
841
|
+
}
|
|
842
|
+
if (secretArg.lastRotatedAt !== undefined
|
|
843
|
+
&& (!isNonNegativeSafeInteger(secretArg.lastRotatedAt)
|
|
844
|
+
|| (secretArg.lastRotatedAt as number) < (secretArg.createdAt as number)
|
|
845
|
+
|| (secretArg.lastRotatedAt as number) > (secretArg.updatedAt as number))) {
|
|
846
|
+
errors.push('secret metadata lastRotatedAt must be within its lifetime');
|
|
847
|
+
}
|
|
848
|
+
return errors;
|
|
849
|
+
};
|
|
850
|
+
|
|
449
851
|
export const validateSecretValueInput = (valueInputArg: unknown): string[] => {
|
|
450
852
|
if (!isRecord(valueInputArg)) return ['valueInput must be an object'];
|
|
451
853
|
if (valueInputArg.mode === 'sealed') {
|
|
@@ -1306,8 +1708,6 @@ export type TSecretRecipientMetadata =
|
|
|
1306
1708
|
| IRetiringSecretRecipientMetadata
|
|
1307
1709
|
| IRevokedSecretRecipientMetadata;
|
|
1308
1710
|
|
|
1309
|
-
export type TSecretIngressRecipient = TSecretRecipientMetadata;
|
|
1310
|
-
|
|
1311
1711
|
export const secretRecipientLimits = Object.freeze({
|
|
1312
1712
|
maximumRecipients: 64,
|
|
1313
1713
|
});
|
|
@@ -1424,20 +1824,6 @@ export const validateSecretValueInputForRecipient = async (
|
|
|
1424
1824
|
return errors;
|
|
1425
1825
|
};
|
|
1426
1826
|
|
|
1427
|
-
/** Retiring recipients open only already-issued envelopes before their deadline. */
|
|
1428
|
-
export const canOpenIssuedSecretEnvelope = (
|
|
1429
|
-
recipientArg: TSecretRecipientMetadata,
|
|
1430
|
-
issuedAtArg: number,
|
|
1431
|
-
nowArg: number,
|
|
1432
|
-
): boolean => Number.isSafeInteger(issuedAtArg)
|
|
1433
|
-
&& Number.isSafeInteger(nowArg)
|
|
1434
|
-
&& issuedAtArg >= recipientArg.activatedAt
|
|
1435
|
-
&& issuedAtArg <= nowArg
|
|
1436
|
-
&& (recipientArg.lifecycleState === 'active'
|
|
1437
|
-
|| (recipientArg.lifecycleState === 'retiring'
|
|
1438
|
-
&& issuedAtArg < recipientArg.acceptUntil
|
|
1439
|
-
&& nowArg < recipientArg.acceptUntil));
|
|
1440
|
-
|
|
1441
1827
|
const canonicalSecretOwner = (ownerArg: TSecretOwner): Record<string, string> => {
|
|
1442
1828
|
switch (ownerArg.kind) {
|
|
1443
1829
|
case 'service': return { kind: ownerArg.kind, serviceId: ownerArg.serviceId };
|