@serve.zone/interfaces 23.1.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.
@@ -2,13 +2,15 @@ import type * as plugins from '../plugins.js';
2
2
  import * as secretData from '../data/secret.js';
3
3
  import type {
4
4
  IResolvedSecretManifestEntry,
5
+ IActiveSecretRecipientMetadata,
5
6
  ISecretMetadata,
6
7
  ISecretSetAttachment,
7
8
  ISecretSetConsumerRolloutStatus,
8
9
  ISecretSetMetadata,
9
10
  ISecretVersionMetadata,
11
+ ISecretVersionPurgeIntentV1,
10
12
  ISecretVersionRetentionReference,
11
- TSecretIngressRecipient,
13
+ TSecretVersionPurgeOperationV1,
12
14
  TSecretMutationTarget,
13
15
  TSecretValueInput,
14
16
  TSecretDelivery,
@@ -26,7 +28,7 @@ extends plugins.typedrequestInterfaces.implementsTR<
26
28
  identity: IIdentityCredential;
27
29
  };
28
30
  response: {
29
- recipient: TSecretIngressRecipient;
31
+ recipient: IActiveSecretRecipientMetadata;
30
32
  };
31
33
  }
32
34
 
@@ -120,7 +122,7 @@ export interface IReq_ChangeSecretLifecycle extends plugins.typedrequestInterfac
120
122
  request: {
121
123
  identity: IIdentityCredential;
122
124
  secretId: string;
123
- action: 'retire' | 'revoke' | 'delete' | 'request-purge';
125
+ action: 'retire' | 'revoke' | 'delete';
124
126
  expectedSecretRevision: number;
125
127
  expectedTargetSecretsRevision: number;
126
128
  };
@@ -140,10 +142,44 @@ extends plugins.typedrequestInterfaces.implementsTR<
140
142
  identity: IIdentityCredential;
141
143
  secretId: string;
142
144
  secretVersionId: string;
145
+ cursor?: string;
146
+ limit?: number;
143
147
  };
144
148
  response: {
145
- purgeAllowed: boolean;
149
+ schemaVersion: 1;
150
+ secret: ISecretMetadata;
151
+ version: ISecretVersionMetadata;
152
+ targetSecretsRevision: number;
153
+ eligible: boolean;
154
+ blockers: TSecretVersionPurgeBlocker[];
155
+ blockingReferenceCount: number;
146
156
  references: ISecretVersionRetentionReference[];
157
+ nextCursor?: string;
158
+ referencesTruncated: boolean;
159
+ operation?: TSecretVersionPurgeOperationV1;
160
+ };
161
+ }
162
+
163
+ export type TSecretVersionPurgeBlocker =
164
+ | 'version-active'
165
+ | 'retention-reference'
166
+ | 'purge-pending'
167
+ | 'already-purged';
168
+
169
+ export interface IReq_PurgeSecretVersion
170
+ extends plugins.typedrequestInterfaces.implementsTR<
171
+ plugins.typedrequestInterfaces.ITypedRequest,
172
+ IReq_PurgeSecretVersion
173
+ > {
174
+ method: 'purgeSecretVersion';
175
+ request: ISecretVersionPurgeIntentV1 & {
176
+ identity: IIdentityCredential;
177
+ };
178
+ response: {
179
+ secret: ISecretMetadata;
180
+ version: ISecretVersionMetadata;
181
+ operation: TSecretVersionPurgeOperationV1;
182
+ targetSecretsRevision: number;
147
183
  };
148
184
  }
149
185
 
@@ -281,7 +317,7 @@ const isRecord = (valueArg: unknown): valueArg is Record<string, unknown> => (
281
317
 
282
318
  export const validateCreateSecretRequest = async (
283
319
  requestArg: unknown,
284
- activeIngressRecipientArg: TSecretIngressRecipient,
320
+ activeIngressRecipientArg: IActiveSecretRecipientMetadata,
285
321
  ): Promise<string[]> => {
286
322
  if (!requestArg || typeof requestArg !== 'object' || Array.isArray(requestArg)) {
287
323
  return ['create secret request must be an object'];
@@ -388,7 +424,7 @@ export const validateCreateSecretRequest = async (
388
424
 
389
425
  export const validateRotateSecretRequest = async (
390
426
  requestArg: unknown,
391
- activeIngressRecipientArg: TSecretIngressRecipient,
427
+ activeIngressRecipientArg: IActiveSecretRecipientMetadata,
392
428
  ): Promise<string[]> => {
393
429
  if (!requestArg || typeof requestArg !== 'object' || Array.isArray(requestArg)) {
394
430
  return ['rotate secret request must be an object'];
@@ -449,3 +485,209 @@ export const validateRotateSecretRequest = async (
449
485
  ));
450
486
  return errors;
451
487
  };
488
+
489
+ export const validatePurgeSecretVersionRequest = (
490
+ requestArg: unknown,
491
+ ): string[] => {
492
+ try {
493
+ if (!isRecord(requestArg)) return ['purge secret version request must be an object'];
494
+ const request = requestArg as Record<string, unknown>;
495
+ const intent = Object.fromEntries(
496
+ Object.entries(request).filter(([keyArg]) => keyArg !== 'identity'),
497
+ );
498
+ const errors = secretData.validateSecretVersionPurgeIntent(intent);
499
+ if (!request.identity || typeof request.identity !== 'object' || Array.isArray(request.identity)
500
+ || JSON.stringify(Object.keys(request.identity).sort()) !== JSON.stringify(['jwt'])
501
+ || typeof (request.identity as Record<string, unknown>).jwt !== 'string'
502
+ || !(request.identity as Record<string, unknown>).jwt) {
503
+ errors.push('purge secret version request requires only a JWT identity');
504
+ }
505
+ return errors;
506
+ } catch {
507
+ return ['purge secret version request must be safely inspectable'];
508
+ }
509
+ };
510
+
511
+ export const validateGetSecretVersionPurgePreflightRequest = (
512
+ requestArg: unknown,
513
+ ): string[] => {
514
+ try {
515
+ if (!isRecord(requestArg)) {
516
+ return ['secret version purge preflight request must be an object'];
517
+ }
518
+ const expectedKeys = [
519
+ 'identity',
520
+ 'secretId',
521
+ 'secretVersionId',
522
+ ...(requestArg.cursor === undefined ? [] : ['cursor']),
523
+ ...(requestArg.limit === undefined ? [] : ['limit']),
524
+ ];
525
+ const errors: string[] = [];
526
+ if (JSON.stringify(Object.keys(requestArg).sort())
527
+ !== JSON.stringify(expectedKeys.sort())) {
528
+ errors.push('secret version purge preflight request must use its exact schema');
529
+ }
530
+ if (!isRecord(requestArg.identity)
531
+ || JSON.stringify(Object.keys(requestArg.identity).sort()) !== JSON.stringify(['jwt'])
532
+ || typeof requestArg.identity.jwt !== 'string'
533
+ || !requestArg.identity.jwt) {
534
+ errors.push('secret version purge preflight request requires only a JWT identity');
535
+ }
536
+ for (const field of ['secretId', 'secretVersionId'] as const) {
537
+ if (typeof requestArg[field] !== 'string'
538
+ || !/^[A-Za-z0-9][A-Za-z0-9:._-]{0,199}$/.test(requestArg[field])) {
539
+ errors.push(`secret version purge preflight ${field} must be canonical`);
540
+ }
541
+ }
542
+ if (requestArg.cursor !== undefined
543
+ && (typeof requestArg.cursor !== 'string' || requestArg.cursor.length === 0)) {
544
+ errors.push('secret version purge preflight cursor must be opaque and non-empty');
545
+ }
546
+ if (requestArg.limit !== undefined
547
+ && (!Number.isSafeInteger(requestArg.limit)
548
+ || (requestArg.limit as number) < 1
549
+ || (requestArg.limit as number)
550
+ > secretData.secretVersionRetentionReferenceLimits.pageSizeMaximum)) {
551
+ errors.push('secret version purge preflight limit must be bounded');
552
+ }
553
+ return errors;
554
+ } catch {
555
+ return ['secret version purge preflight request must be safely inspectable'];
556
+ }
557
+ };
558
+
559
+ export const validateSecretVersionPurgePreflightResponse = async (
560
+ responseArg: unknown,
561
+ ): Promise<string[]> => {
562
+ try {
563
+ if (!isRecord(responseArg)) {
564
+ return ['secret version purge preflight response must be an object'];
565
+ }
566
+ const response = responseArg as Record<string, unknown>;
567
+ const expectedKeys = [
568
+ 'schemaVersion',
569
+ 'secret',
570
+ 'version',
571
+ 'targetSecretsRevision',
572
+ 'eligible',
573
+ 'blockers',
574
+ 'blockingReferenceCount',
575
+ 'references',
576
+ 'referencesTruncated',
577
+ ...(response.nextCursor === undefined ? [] : ['nextCursor']),
578
+ ...(response.operation === undefined ? [] : ['operation']),
579
+ ];
580
+ const errors: string[] = [];
581
+ if (JSON.stringify(Object.keys(response).sort())
582
+ !== JSON.stringify([...expectedKeys].sort())) {
583
+ errors.push('secret version purge preflight response must use its exact schema');
584
+ }
585
+ if (response.schemaVersion !== 1) {
586
+ errors.push('secret version purge preflight response schemaVersion must be 1');
587
+ }
588
+ if (secretData.validateSecretVersionMetadata(response.version).length > 0) {
589
+ errors.push('secret version purge preflight version must be canonical');
590
+ }
591
+ if (secretData.validateSecretMetadata(response.secret).length > 0) {
592
+ errors.push('secret version purge preflight secret must be canonical');
593
+ }
594
+ if (isRecord(response.secret)
595
+ && isRecord(response.version)
596
+ && response.version.secretId !== response.secret.id) {
597
+ errors.push('secret version purge preflight version must belong to its secret');
598
+ }
599
+ if (!Number.isSafeInteger(response.targetSecretsRevision)
600
+ || (response.targetSecretsRevision as number) < 0) {
601
+ errors.push('secret version purge preflight target revision must be non-negative');
602
+ }
603
+ const allowedBlockers = new Set<TSecretVersionPurgeBlocker>([
604
+ 'version-active',
605
+ 'retention-reference',
606
+ 'purge-pending',
607
+ 'already-purged',
608
+ ]);
609
+ if (!Array.isArray(response.blockers)
610
+ || response.blockers.some((blockerArg) => !allowedBlockers.has(
611
+ blockerArg as TSecretVersionPurgeBlocker,
612
+ ))
613
+ || new Set(response.blockers).size !== response.blockers.length
614
+ || JSON.stringify(response.blockers) !== JSON.stringify([...response.blockers].sort())) {
615
+ errors.push('secret version purge preflight blockers must be uniquely sorted');
616
+ }
617
+ if (typeof response.eligible !== 'boolean'
618
+ || response.eligible !== (Array.isArray(response.blockers) && response.blockers.length === 0)) {
619
+ errors.push('secret version purge preflight eligibility must be derived from blockers');
620
+ }
621
+ if (!Number.isSafeInteger(response.blockingReferenceCount)
622
+ || (response.blockingReferenceCount as number) < 0) {
623
+ errors.push('secret version purge preflight reference count must be non-negative');
624
+ }
625
+ errors.push(...secretData.validateSecretVersionRetentionReferences(response.references));
626
+ if (typeof response.referencesTruncated !== 'boolean') {
627
+ errors.push('secret version purge preflight referencesTruncated must be boolean');
628
+ }
629
+ if (response.nextCursor !== undefined
630
+ && (typeof response.nextCursor !== 'string' || response.nextCursor.length === 0)) {
631
+ errors.push('secret version purge preflight nextCursor must be opaque and non-empty');
632
+ }
633
+ if (response.referencesTruncated !== (response.nextCursor !== undefined)) {
634
+ errors.push('secret version purge preflight truncation must match nextCursor presence');
635
+ }
636
+ if (Array.isArray(response.references)
637
+ && Number.isSafeInteger(response.blockingReferenceCount)
638
+ && (response.blockingReferenceCount as number) < response.references.length) {
639
+ errors.push('secret version purge preflight reference count cannot be smaller than its page');
640
+ }
641
+ if (Array.isArray(response.blockers)
642
+ && isRecord(response.version)
643
+ && Number.isSafeInteger(response.blockingReferenceCount)) {
644
+ const expectedBlockers: TSecretVersionPurgeBlocker[] = [];
645
+ if (response.version.lifecycleState === 'active') expectedBlockers.push('version-active');
646
+ if ((response.blockingReferenceCount as number) > 0) {
647
+ expectedBlockers.push('retention-reference');
648
+ }
649
+ if (response.version.lifecycleState === 'purge-pending') {
650
+ expectedBlockers.push('purge-pending');
651
+ }
652
+ if (response.version.lifecycleState === 'purged') expectedBlockers.push('already-purged');
653
+ expectedBlockers.sort();
654
+ if (JSON.stringify(response.blockers) !== JSON.stringify(expectedBlockers)) {
655
+ errors.push('secret version purge preflight blockers do not match current state');
656
+ }
657
+ }
658
+ if (response.operation !== undefined) {
659
+ errors.push(...await secretData.validateSecretVersionPurgeOperation(response.operation));
660
+ if (isRecord(response.operation)
661
+ && isRecord(response.operation.intent)
662
+ && isRecord(response.secret)
663
+ && isRecord(response.version)
664
+ && (
665
+ response.operation.id !== response.version.purgeOperationId
666
+ || response.operation.intent.secretId !== response.secret.id
667
+ || response.operation.intent.secretVersionId !== response.version.id
668
+ )) {
669
+ errors.push('secret version purge preflight operation does not match its version');
670
+ }
671
+ if (isRecord(response.operation) && isRecord(response.version)) {
672
+ if (response.version.lifecycleState === 'purge-pending'
673
+ && response.operation.state === 'succeeded') {
674
+ errors.push('purge-pending version cannot have a succeeded purge operation');
675
+ }
676
+ if (response.version.lifecycleState === 'purged'
677
+ && (response.operation.state !== 'succeeded'
678
+ || response.operation.purgedAt !== response.version.purgedAt)) {
679
+ errors.push('purged version requires the matching succeeded purge operation');
680
+ }
681
+ }
682
+ }
683
+ if (isRecord(response.version)
684
+ && (response.version.lifecycleState === 'purge-pending'
685
+ || response.version.lifecycleState === 'purged')
686
+ && response.operation === undefined) {
687
+ errors.push('secret version purge preflight purging state requires its operation');
688
+ }
689
+ return errors;
690
+ } catch {
691
+ return ['secret version purge preflight response must be safely inspectable'];
692
+ }
693
+ };