@serve.zone/interfaces 32.23.0 → 32.25.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.
@@ -370,3 +370,144 @@ export const validatePlatformBindingObjectStorageRetention = async (
370
370
  }
371
371
  return validateObjectStorageRetentionBinding(bindingArg.objectstorageRetention, expectedArg);
372
372
  };
373
+
374
+ /**
375
+ * The non-secret environment names that tell a workload which retention its bucket carries.
376
+ *
377
+ * The names are the ones the cluster relay handed workloads on the Docker path, so an application
378
+ * that read them keeps working unchanged. Every value is a projection of verified
379
+ * `IObjectStorageRetentionEvidence`; none is a credential.
380
+ *
381
+ * - `S3_RETENTION_MODE`: `intent.mode`, always `compliance`.
382
+ * - `S3_RETENTION_POLICY_ID`: `intent.policyId`.
383
+ * - `S3_RETENTION_DURATION_SECONDS`: `intent.retentionDurationSeconds` in decimal.
384
+ * - `S3_RETENTION_INTENT_SHA256`: `intentSha256`.
385
+ * - `S3_RETENTION_SENTINEL_KEY`: `receipt.sentinelKey`.
386
+ * - `S3_RETENTION_CONFIGURED_AT`: `receipt.configuredAt`, Unix milliseconds in decimal.
387
+ * - `S3_RETENTION_SENTINEL_CREATED_AT`: `receipt.sentinel.createdAt`, Unix milliseconds in decimal.
388
+ * - `S3_RETENTION_SENTINEL_RETAIN_UNTIL`: `receipt.sentinel.retainUntil`, Unix milliseconds in
389
+ * decimal.
390
+ * - `S3_RETENTION_SENTINEL_PAYLOAD_SHA256`: `receipt.sentinel.payloadSha256`.
391
+ * - `S3_RETENTION_SENTINEL_METADATA_SHA256`: `receipt.sentinel.metadataSha256`.
392
+ * - `S3_RETENTION_SENTINEL_ETAG`: `receipt.sentinel.etag`.
393
+ */
394
+ export const objectStorageRetentionEnvironmentKeys = Object.freeze([
395
+ 'S3_RETENTION_MODE',
396
+ 'S3_RETENTION_POLICY_ID',
397
+ 'S3_RETENTION_DURATION_SECONDS',
398
+ 'S3_RETENTION_INTENT_SHA256',
399
+ 'S3_RETENTION_SENTINEL_KEY',
400
+ 'S3_RETENTION_CONFIGURED_AT',
401
+ 'S3_RETENTION_SENTINEL_CREATED_AT',
402
+ 'S3_RETENTION_SENTINEL_RETAIN_UNTIL',
403
+ 'S3_RETENTION_SENTINEL_PAYLOAD_SHA256',
404
+ 'S3_RETENTION_SENTINEL_METADATA_SHA256',
405
+ 'S3_RETENTION_SENTINEL_ETAG',
406
+ ] as const);
407
+
408
+ export type TObjectStorageRetentionEnvironmentKey =
409
+ typeof objectStorageRetentionEnvironmentKeys[number];
410
+
411
+ export type TObjectStorageRetentionEnvironment = {
412
+ [TKey in TObjectStorageRetentionEnvironmentKey]: string;
413
+ };
414
+
415
+ /**
416
+ * The retention environment of evidence that proves `expectedArg`.
417
+ *
418
+ * The evidence is validated against the expectation first, with the same validator every other
419
+ * consumer of retention evidence uses; the first reason it is refused is thrown. A workload is
420
+ * never told about a retention nobody proved.
421
+ */
422
+ export const createObjectStorageRetentionEnvironment = async (
423
+ evidenceArg: IObjectStorageRetentionEvidence,
424
+ expectedArg: IObjectStorageRetentionEvidenceExpectation,
425
+ ): Promise<TObjectStorageRetentionEnvironment> => {
426
+ const errors = await validateObjectStorageRetentionEvidence(evidenceArg, expectedArg);
427
+ if (errors.length > 0) throw new Error(errors[0]);
428
+ return {
429
+ S3_RETENTION_MODE: evidenceArg.intent.mode,
430
+ S3_RETENTION_POLICY_ID: evidenceArg.intent.policyId,
431
+ S3_RETENTION_DURATION_SECONDS: String(evidenceArg.intent.retentionDurationSeconds),
432
+ S3_RETENTION_INTENT_SHA256: evidenceArg.intentSha256,
433
+ S3_RETENTION_SENTINEL_KEY: evidenceArg.receipt.sentinelKey,
434
+ S3_RETENTION_CONFIGURED_AT: String(evidenceArg.receipt.configuredAt),
435
+ S3_RETENTION_SENTINEL_CREATED_AT: String(evidenceArg.receipt.sentinel.createdAt),
436
+ S3_RETENTION_SENTINEL_RETAIN_UNTIL: String(evidenceArg.receipt.sentinel.retainUntil),
437
+ S3_RETENTION_SENTINEL_PAYLOAD_SHA256: evidenceArg.receipt.sentinel.payloadSha256,
438
+ S3_RETENTION_SENTINEL_METADATA_SHA256: evidenceArg.receipt.sentinel.metadataSha256,
439
+ S3_RETENTION_SENTINEL_ETAG: evidenceArg.receipt.sentinel.etag,
440
+ };
441
+ };
442
+
443
+ const canonicalDecimalPattern = /^[1-9][0-9]*$/;
444
+
445
+ const readCanonicalPositiveInteger = (valueArg: unknown): number | undefined => (
446
+ typeof valueArg === 'string'
447
+ && canonicalDecimalPattern.test(valueArg)
448
+ && Number.isSafeInteger(Number(valueArg))
449
+ ? Number(valueArg)
450
+ : undefined
451
+ );
452
+
453
+ /**
454
+ * Validates a retention environment on its own: exactly its eleven keys, canonical values, an
455
+ * intent digest that matches the stated intent, and sentinel timestamps that agree with the
456
+ * duration. It cannot prove the authority or the bucket the evidence was bound to — the
457
+ * environment does not carry them — so it judges the shape a workload reads, not the proof.
458
+ */
459
+ export const validateObjectStorageRetentionEnvironment = async (
460
+ environmentArg: unknown,
461
+ pathArg = 'objectstorage retention environment',
462
+ ): Promise<string[]> => {
463
+ if (!isRecord(environmentArg)
464
+ || !hasExactKeys(environmentArg, objectStorageRetentionEnvironmentKeys)
465
+ || objectStorageRetentionEnvironmentKeys.some((keyArg) => (
466
+ typeof environmentArg[keyArg] !== 'string'
467
+ ))) {
468
+ return [`${pathArg} must contain exactly its string keys`];
469
+ }
470
+ const environment = environmentArg as TObjectStorageRetentionEnvironment;
471
+ const errors: string[] = [];
472
+ const duration = readCanonicalPositiveInteger(environment.S3_RETENTION_DURATION_SECONDS);
473
+ const intent = {
474
+ mode: environment.S3_RETENTION_MODE,
475
+ policyId: environment.S3_RETENTION_POLICY_ID,
476
+ retentionDurationSeconds: duration,
477
+ };
478
+ const intentErrors = validateObjectStorageRetentionIntent(intent, `${pathArg} intent`);
479
+ errors.push(...intentErrors);
480
+ if (!sha256Pattern.test(environment.S3_RETENTION_INTENT_SHA256)
481
+ || intentErrors.length === 0
482
+ && environment.S3_RETENTION_INTENT_SHA256 !== await computeObjectStorageRetentionIntentSha256(
483
+ intent as IObjectStorageRetentionIntent,
484
+ )) {
485
+ errors.push(`${pathArg} S3_RETENTION_INTENT_SHA256 must be the digest of the stated intent`);
486
+ }
487
+ if (!sentinelKeyPattern.test(environment.S3_RETENTION_SENTINEL_KEY)) {
488
+ errors.push(`${pathArg} S3_RETENTION_SENTINEL_KEY must be canonical`);
489
+ }
490
+ const configuredAt = readCanonicalPositiveInteger(environment.S3_RETENTION_CONFIGURED_AT);
491
+ const createdAt = readCanonicalPositiveInteger(environment.S3_RETENTION_SENTINEL_CREATED_AT);
492
+ const retainUntil = readCanonicalPositiveInteger(environment.S3_RETENTION_SENTINEL_RETAIN_UNTIL);
493
+ if (configuredAt === undefined
494
+ || createdAt === undefined
495
+ || retainUntil === undefined
496
+ || createdAt < configuredAt) {
497
+ errors.push(`${pathArg} timestamps must be canonical positive decimals in order`);
498
+ } else if (intentErrors.length === 0) {
499
+ try {
500
+ if (retainUntil !== createObjectStorageRetentionRetainUntil(createdAt, duration as number)) {
501
+ errors.push(`${pathArg} S3_RETENTION_SENTINEL_RETAIN_UNTIL must equal createdAt plus duration`);
502
+ }
503
+ } catch {
504
+ errors.push(`${pathArg} S3_RETENTION_SENTINEL_RETAIN_UNTIL arithmetic must be safe`);
505
+ }
506
+ }
507
+ if (!sha256Pattern.test(environment.S3_RETENTION_SENTINEL_PAYLOAD_SHA256)
508
+ || !sha256Pattern.test(environment.S3_RETENTION_SENTINEL_METADATA_SHA256)
509
+ || !etagPattern.test(environment.S3_RETENTION_SENTINEL_ETAG)) {
510
+ errors.push(`${pathArg} sentinel digests and ETag must be canonical`);
511
+ }
512
+ return errors;
513
+ };
@@ -4,6 +4,7 @@ import type { ICluster } from '../data/cluster.js';
4
4
  import type { IClusterNode, INodeAction, INodeMetricsSample, TNodeActionName } from '../data/clusternode.js';
5
5
  import type { IDeployment } from '../data/deployment.js';
6
6
  import type { IIdentity } from '../data/user.js';
7
+ import type { IPlatformObjectStorageEndpoint } from '../platform/objectstorage.js';
7
8
  import type {
8
9
  INodeCredentialMetadata,
9
10
  INodeCredentialMutationPreconditions,
@@ -282,6 +283,38 @@ export interface IReq_Any_Cloudly_SetClusterNodeCorestoreEndpoint extends plugin
282
283
  };
283
284
  }
284
285
 
286
+ /**
287
+ * An administrator states where a workload on one node reaches the node's Corestore object
288
+ * storage, and the region it signs for: `IClusterNode.data.corestoreObjectStorage`.
289
+ *
290
+ * A request of its own, separate from `setClusterNodeCorestoreEndpoint`, because the two are
291
+ * separate facts with separate readers — the relay dials the control endpoint, a workload dials
292
+ * this one — and setting one never touches the other. A caller that states only the control
293
+ * endpoint leaves this one as it stands.
294
+ *
295
+ * Cloudly validates it with `validatePlatformObjectStorageEndpoint` before it writes and takes the
296
+ * node's own real-write fence inside the writing transaction, as for the control endpoint.
297
+ * `objectStorage: null` clears it, which is the same fact as never having set one: no
298
+ * object-storage environment can be composed for a binding on this node. The endpoint says where,
299
+ * never with what — no credential travels in this request or in the node document.
300
+ */
301
+ export interface IReq_Any_Cloudly_SetClusterNodeCorestoreObjectStorageEndpoint extends plugins.typedrequestInterfaces.implementsTR<
302
+ plugins.typedrequestInterfaces.ITypedRequest,
303
+ IReq_Any_Cloudly_SetClusterNodeCorestoreObjectStorageEndpoint
304
+ > {
305
+ method: 'setClusterNodeCorestoreObjectStorageEndpoint';
306
+ request: {
307
+ identity: IIdentity;
308
+ nodeId: string;
309
+ /** The node's object-storage endpoint and region, or `null` to state that it has none. */
310
+ objectStorage: IPlatformObjectStorageEndpoint | null;
311
+ };
312
+ response: {
313
+ /** The node as it now stands, so a caller needs no second read to see what it set. */
314
+ node: IClusterNode;
315
+ };
316
+ }
317
+
285
318
  export interface IReq_Any_Cloudly_CreateNodeAction extends plugins.typedrequestInterfaces.implementsTR<
286
319
  plugins.typedrequestInterfaces.ITypedRequest,
287
320
  IReq_Any_Cloudly_CreateNodeAction