@serve.zone/interfaces 32.20.1 → 32.22.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 +74 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/clusteringress.d.ts +58 -0
- package/dist_ts/data/clusteringress.js +96 -2
- package/dist_ts/data/clusteringressdesignation.d.ts +64 -0
- package/dist_ts/data/clusteringressdesignation.js +84 -0
- package/dist_ts/data/gateway.d.ts +169 -0
- package/dist_ts/data/gateway.js +115 -2
- package/dist_ts/data/index.d.ts +4 -0
- package/dist_ts/data/index.js +4 -1
- package/dist_ts/data/networkprojection.d.ts +18 -0
- package/dist_ts/data/networkprojection.js +31 -2
- package/dist_ts/data/networkworkloadingress.d.ts +61 -0
- package/dist_ts/data/networkworkloadingress.js +54 -0
- package/dist_ts/data/runtimelocalstorageclaim.d.ts +124 -0
- package/dist_ts/data/runtimelocalstorageclaim.js +167 -0
- package/dist_ts/data/serviceruntime.d.ts +10 -3
- package/dist_ts/data/serviceruntime.js +6 -2
- package/dist_ts/private/ipaddress.d.ts +29 -0
- package/dist_ts/private/ipaddress.js +120 -0
- package/dist_ts/private/storagecontract.d.ts +1 -0
- package/dist_ts/private/storagecontract.js +2 -1
- package/dist_ts/requests/clusteringress.d.ts +52 -0
- package/dist_ts/requests/clusteringress.js +1 -1
- package/dist_ts/requests/gateway.d.ts +58 -1
- package/dist_ts/requests/runtimesession.d.ts +12 -0
- package/package.json +1 -1
- package/readme.md +167 -2
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/clusteringress.ts +136 -1
- package/ts/data/clusteringressdesignation.ts +141 -0
- package/ts/data/gateway.ts +308 -0
- package/ts/data/index.ts +5 -0
- package/ts/data/networkprojection.ts +49 -1
- package/ts/data/networkworkloadingress.ts +107 -0
- package/ts/data/runtimelocalstorageclaim.ts +275 -0
- package/ts/data/serviceruntime.ts +13 -2
- package/ts/private/ipaddress.ts +140 -0
- package/ts/private/storagecontract.ts +1 -0
- package/ts/requests/clusteringress.ts +65 -0
- package/ts/requests/gateway.ts +71 -0
- package/ts/requests/runtimesession.ts +16 -0
package/ts/data/gateway.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import type { TDnsRecordType } from './dns.js';
|
|
2
|
+
import { hasExactKeys, readDataElements, readDataProperties } from '../private/dataproperties.js';
|
|
3
|
+
import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
|
|
4
|
+
import {
|
|
5
|
+
ipPrefixContains,
|
|
6
|
+
parseCanonicalIpAddress,
|
|
7
|
+
parseCanonicalIpPrefix,
|
|
8
|
+
} from '../private/ipaddress.js';
|
|
2
9
|
|
|
3
10
|
/**
|
|
4
11
|
* Type of platform acting as a gateway client against a gateway (dcrouter).
|
|
@@ -115,6 +122,11 @@ export interface IGatewayCapabilities {
|
|
|
115
122
|
read: boolean;
|
|
116
123
|
export: boolean;
|
|
117
124
|
forceRenew: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* `getGatewayClientCertificate` and `releaseGatewayClientCertificate` are served: an exact
|
|
127
|
+
* certificate for one owned hostname, with no route behind it. Absent means unsupported.
|
|
128
|
+
*/
|
|
129
|
+
exactIssuance?: boolean;
|
|
118
130
|
};
|
|
119
131
|
email: {
|
|
120
132
|
domains: boolean;
|
|
@@ -127,6 +139,11 @@ export interface IGatewayCapabilities {
|
|
|
127
139
|
dns: {
|
|
128
140
|
authoritative: boolean;
|
|
129
141
|
providerManaged: boolean;
|
|
142
|
+
/**
|
|
143
|
+
* `syncGatewayClientDnsRecord` is served: an address record at a value the client chooses
|
|
144
|
+
* inside its `allowedDnsRecordAddresses`. Absent means unsupported.
|
|
145
|
+
*/
|
|
146
|
+
clientRecords?: boolean;
|
|
130
147
|
};
|
|
131
148
|
http3: {
|
|
132
149
|
enabled: boolean;
|
|
@@ -176,6 +193,11 @@ export interface IGatewayClient {
|
|
|
176
193
|
/** `*` matches any hostname under a managed zone; `*.zone` matches exactly one label. */
|
|
177
194
|
hostnamePatterns: string[];
|
|
178
195
|
allowedRouteTargets: IGatewayRouteTargetAllowance[];
|
|
196
|
+
/**
|
|
197
|
+
* Canonical address prefixes the client's own address records may point into; see
|
|
198
|
+
* `validateGatewayAllowedDnsRecordAddresses`. Absent or empty allows no address record.
|
|
199
|
+
*/
|
|
200
|
+
allowedDnsRecordAddresses?: string[];
|
|
179
201
|
capabilities: IGatewayTokenCapabilities;
|
|
180
202
|
enabled: boolean;
|
|
181
203
|
tokenCount?: number;
|
|
@@ -195,6 +217,11 @@ export interface IGatewayClientProvisioningSpec {
|
|
|
195
217
|
/** `*` matches any hostname under a managed zone; `*.zone` matches exactly one label. */
|
|
196
218
|
hostnamePatterns: string[];
|
|
197
219
|
allowedRouteTargets: IGatewayRouteTargetAllowance[];
|
|
220
|
+
/**
|
|
221
|
+
* Canonical address prefixes the client's own address records may point into; see
|
|
222
|
+
* `validateGatewayAllowedDnsRecordAddresses`. Absent or empty allows no address record.
|
|
223
|
+
*/
|
|
224
|
+
allowedDnsRecordAddresses?: string[];
|
|
198
225
|
capabilities: IGatewayTokenCapabilities;
|
|
199
226
|
enabled?: boolean;
|
|
200
227
|
}
|
|
@@ -226,6 +253,8 @@ interface IGatewayClientContextBase {
|
|
|
226
253
|
/** `*` matches any hostname under a managed zone; `*.zone` matches exactly one label. */
|
|
227
254
|
hostnamePatterns: string[];
|
|
228
255
|
allowedRouteTargets: IGatewayRouteTargetAllowance[];
|
|
256
|
+
/** Absent on gateways that predate address records; read it as an empty allowance. */
|
|
257
|
+
allowedDnsRecordAddresses?: string[];
|
|
229
258
|
capabilities: IGatewayTokenCapabilities;
|
|
230
259
|
}
|
|
231
260
|
|
|
@@ -459,3 +488,282 @@ export interface IGatewayRouteSyncResult {
|
|
|
459
488
|
dns?: IGatewayRouteDnsResult;
|
|
460
489
|
message?: string;
|
|
461
490
|
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Bounds of the gateway-client address record and certificate contracts.
|
|
494
|
+
*
|
|
495
|
+
* A gateway applies `defaultTtl` to a record that names none, and refuses a TTL outside
|
|
496
|
+
* `minTtl`..`maxTtl` as it refuses an allowance list longer than `maxAllowedDnsRecordAddresses`.
|
|
497
|
+
*/
|
|
498
|
+
export const gatewayClientHostnameContract = Object.freeze({
|
|
499
|
+
minTtl: 60,
|
|
500
|
+
maxTtl: 86_400,
|
|
501
|
+
defaultTtl: 300,
|
|
502
|
+
maxAllowedDnsRecordAddresses: 64,
|
|
503
|
+
maxAppIdLength: 128,
|
|
504
|
+
maxHostnameLength: 253,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* What one gateway client owns a hostname for: its app and the exact hostname.
|
|
509
|
+
*
|
|
510
|
+
* The gateway client itself is never named here. The gateway resolves it from the authenticating
|
|
511
|
+
* gateway-client credential, so a client cannot act for another one. One ownership holds at most
|
|
512
|
+
* one address record and one certificate for its hostname, and a hostname is held by at most one
|
|
513
|
+
* ownership: a second client or app, a gateway route claim, or a record the gateway does not
|
|
514
|
+
* manage makes the hostname a conflict rather than a shared name.
|
|
515
|
+
*/
|
|
516
|
+
export interface IGatewayClientHostnameOwnership {
|
|
517
|
+
/** 1 to 128 characters of letters, digits, `.`, `_`, `:` or `-`, starting with a letter or digit. */
|
|
518
|
+
appId: string;
|
|
519
|
+
/** Lowercase fully qualified hostname without a trailing dot and without a wildcard. */
|
|
520
|
+
hostname: string;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Address record types a gateway client may publish at a value it chooses. */
|
|
524
|
+
export type TGatewayClientDnsRecordType = 'A' | 'AAAA';
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* One address record a gateway client asks the gateway to publish for its hostname.
|
|
528
|
+
*
|
|
529
|
+
* The value is the canonical literal of the record's family (a dotted quad for `A`, RFC 5952 text
|
|
530
|
+
* for `AAAA`) and must lie inside the client's `allowedDnsRecordAddresses`. The record is never
|
|
531
|
+
* proxied: a client that chooses its own address asks for exactly that address to be served.
|
|
532
|
+
*/
|
|
533
|
+
export interface IGatewayClientDnsRecord {
|
|
534
|
+
type: TGatewayClientDnsRecordType;
|
|
535
|
+
value: string;
|
|
536
|
+
/** Seconds, `minTtl`..`maxTtl`; `defaultTtl` when omitted. */
|
|
537
|
+
ttl?: number;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Outcome of one address record synchronization, in the gateway route DNS vocabulary.
|
|
542
|
+
*
|
|
543
|
+
* - `managed-created` / `managed-updated` / `managed-unchanged` / `managed-deleted`: done.
|
|
544
|
+
* - `target-invalid`: the value is outside the client's address allowance.
|
|
545
|
+
* - `zone-unmanaged`: no zone the gateway manages holds the hostname.
|
|
546
|
+
* - `zone-unavailable`: the zone's DNS provider or authority cannot be reached; retryable.
|
|
547
|
+
* - `record-conflict`: another ownership, a gateway route claim, or an unmanaged record holds
|
|
548
|
+
* the hostname. The gateway never overwrites or adopts it.
|
|
549
|
+
* - `mutation-failed`: writing the record failed; retryable.
|
|
550
|
+
*/
|
|
551
|
+
export type TGatewayClientDnsRecordStatus = Extract<
|
|
552
|
+
TGatewayRouteDnsStatus,
|
|
553
|
+
| 'managed-created'
|
|
554
|
+
| 'managed-updated'
|
|
555
|
+
| 'managed-unchanged'
|
|
556
|
+
| 'managed-deleted'
|
|
557
|
+
| 'target-invalid'
|
|
558
|
+
| 'zone-unmanaged'
|
|
559
|
+
| 'zone-unavailable'
|
|
560
|
+
| 'record-conflict'
|
|
561
|
+
| 'mutation-failed'
|
|
562
|
+
>;
|
|
563
|
+
|
|
564
|
+
/** Result of `syncGatewayClientDnsRecord`. */
|
|
565
|
+
export interface IGatewayClientDnsRecordSyncResult {
|
|
566
|
+
success: boolean;
|
|
567
|
+
/** True when the same request may succeed later without any change on the client's side. */
|
|
568
|
+
retryable: boolean;
|
|
569
|
+
status: TGatewayClientDnsRecordStatus;
|
|
570
|
+
hostname: string;
|
|
571
|
+
zoneName?: string;
|
|
572
|
+
/** The record this ownership holds after the call; absent after a deletion or a refusal. */
|
|
573
|
+
record?: IGatewayRouteDnsRecord;
|
|
574
|
+
/** The records that hold the hostname, as evidence for `record-conflict`. */
|
|
575
|
+
conflictingRecords?: IGatewayRouteDnsRecord[];
|
|
576
|
+
/** Unix epoch milliseconds at which the gateway checked the DNS state. */
|
|
577
|
+
checkedAt: number;
|
|
578
|
+
/** Unix epoch milliseconds at which authoritative DNS confirmed the desired state. */
|
|
579
|
+
authoritativeVerifiedAt?: number;
|
|
580
|
+
message?: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* The certificate a gateway issued for one owned hostname, with its private key.
|
|
585
|
+
*
|
|
586
|
+
* Secret-bearing: a receiver keeps the key sealed and never logs or caches the response as is.
|
|
587
|
+
*/
|
|
588
|
+
export interface IGatewayClientCertificate {
|
|
589
|
+
hostname: string;
|
|
590
|
+
/** PEM leaf certificate followed by its chain. */
|
|
591
|
+
certificatePem: string;
|
|
592
|
+
/** PEM private key of the leaf certificate. */
|
|
593
|
+
privateKeyPem: string;
|
|
594
|
+
/** Unix epoch milliseconds. */
|
|
595
|
+
validFrom: number;
|
|
596
|
+
/** Unix epoch milliseconds. */
|
|
597
|
+
validUntil: number;
|
|
598
|
+
/** Unix epoch milliseconds after which asking again yields a renewed certificate. */
|
|
599
|
+
renewAfter: number;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Why a gateway does not issue a certificate for an ownership.
|
|
604
|
+
*
|
|
605
|
+
* - `zone-unmanaged`: no zone the gateway manages with verified authority holds the hostname.
|
|
606
|
+
* - `hostname-conflict`: another ownership or a gateway route claim holds the hostname.
|
|
607
|
+
* - `issuance-failed`: the certificate authority refused the order; `retryAfter` says when a new
|
|
608
|
+
* order is placed.
|
|
609
|
+
* - `issuance-unavailable`: the gateway's ACME configuration or DNS authority is not ready.
|
|
610
|
+
*/
|
|
611
|
+
export type TGatewayClientCertificateRefusal =
|
|
612
|
+
| 'zone-unmanaged'
|
|
613
|
+
| 'hostname-conflict'
|
|
614
|
+
| 'issuance-failed'
|
|
615
|
+
| 'issuance-unavailable';
|
|
616
|
+
|
|
617
|
+
/** Result of `getGatewayClientCertificate`. */
|
|
618
|
+
export type TGatewayClientCertificateResult =
|
|
619
|
+
| {
|
|
620
|
+
status: 'ready';
|
|
621
|
+
certificate: IGatewayClientCertificate;
|
|
622
|
+
}
|
|
623
|
+
| {
|
|
624
|
+
/** An issuance runs; ask again after `retryAfterMs`. */
|
|
625
|
+
status: 'pending';
|
|
626
|
+
hostname: string;
|
|
627
|
+
retryAfterMs: number;
|
|
628
|
+
}
|
|
629
|
+
| {
|
|
630
|
+
status: 'refused';
|
|
631
|
+
hostname: string;
|
|
632
|
+
code: TGatewayClientCertificateRefusal;
|
|
633
|
+
retryable: boolean;
|
|
634
|
+
/** Unix epoch milliseconds before which asking again cannot change the answer. */
|
|
635
|
+
retryAfter?: number;
|
|
636
|
+
message?: string;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/** Result of `releaseGatewayClientCertificate`. */
|
|
640
|
+
export type TGatewayClientCertificateReleaseResult =
|
|
641
|
+
| {
|
|
642
|
+
success: true;
|
|
643
|
+
/** `released` when material or an issuance was held; `unchanged` when nothing was. */
|
|
644
|
+
action: 'released' | 'unchanged';
|
|
645
|
+
}
|
|
646
|
+
| {
|
|
647
|
+
success: false;
|
|
648
|
+
retryable: boolean;
|
|
649
|
+
message: string;
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const gatewayHostnameLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
653
|
+
const gatewayAppId = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
654
|
+
|
|
655
|
+
/** True for a lowercase fully qualified hostname of two or more labels, without wildcard or dot. */
|
|
656
|
+
export const isGatewayClientHostname = (valueArg: unknown): valueArg is string => (
|
|
657
|
+
typeof valueArg === 'string'
|
|
658
|
+
&& valueArg.length <= gatewayClientHostnameContract.maxHostnameLength
|
|
659
|
+
&& valueArg.split('.').length >= 2
|
|
660
|
+
&& valueArg.split('.').every((labelArg) => gatewayHostnameLabel.test(labelArg))
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
/** Validates one hostname ownership and names every reason it is refused. */
|
|
664
|
+
export const validateGatewayClientHostnameOwnership = (
|
|
665
|
+
valueArg: unknown,
|
|
666
|
+
pathArg = 'ownership',
|
|
667
|
+
): string[] => {
|
|
668
|
+
const ownership = readDataProperties(valueArg);
|
|
669
|
+
if (ownership === null || !hasExactKeys(ownership, ['appId', 'hostname'])) {
|
|
670
|
+
return [`${pathArg} must use its exact schema`];
|
|
671
|
+
}
|
|
672
|
+
const errors: string[] = [];
|
|
673
|
+
const appId = ownership.appId;
|
|
674
|
+
if (typeof appId !== 'string' || appId.length > gatewayClientHostnameContract.maxAppIdLength
|
|
675
|
+
|| !gatewayAppId.test(appId)) {
|
|
676
|
+
errors.push(`${pathArg} appId must be 1 to ${gatewayClientHostnameContract.maxAppIdLength} `
|
|
677
|
+
+ 'characters of letters, digits, dot, underscore, colon or hyphen, starting with a letter or digit');
|
|
678
|
+
}
|
|
679
|
+
if (!isGatewayClientHostname(ownership.hostname)) {
|
|
680
|
+
errors.push(`${pathArg} hostname must be a lowercase fully qualified hostname `
|
|
681
|
+
+ 'without a trailing dot or wildcard');
|
|
682
|
+
}
|
|
683
|
+
return errors;
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
/** True for one canonical address prefix of either family whose host bits are zero. */
|
|
687
|
+
export const isGatewayAddressPrefix = (valueArg: unknown): valueArg is string =>
|
|
688
|
+
parseCanonicalIpPrefix(valueArg) !== null;
|
|
689
|
+
|
|
690
|
+
/** Validates a client's address allowance and names every reason it is refused. */
|
|
691
|
+
export const validateGatewayAllowedDnsRecordAddresses = (
|
|
692
|
+
valueArg: unknown,
|
|
693
|
+
pathArg = 'allowedDnsRecordAddresses',
|
|
694
|
+
): string[] => {
|
|
695
|
+
const maximum = gatewayClientHostnameContract.maxAllowedDnsRecordAddresses;
|
|
696
|
+
const prefixes = readDataElements(valueArg, maximum);
|
|
697
|
+
if (prefixes === null) {
|
|
698
|
+
return [`${pathArg} must be a list of at most ${maximum} address prefixes`];
|
|
699
|
+
}
|
|
700
|
+
const errors: string[] = [];
|
|
701
|
+
prefixes.forEach((prefixArg, indexArg) => {
|
|
702
|
+
if (!isGatewayAddressPrefix(prefixArg)) {
|
|
703
|
+
errors.push(`${pathArg}[${indexArg}] must be a canonical address prefix whose host bits are zero`);
|
|
704
|
+
} else if (prefixes.indexOf(prefixArg) !== indexArg) {
|
|
705
|
+
errors.push(`${pathArg}[${indexArg}] repeats ${prefixArg}`);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
return errors;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
/** True when the value is a canonical literal of the record type that can address one host. */
|
|
712
|
+
const isGatewayClientDnsRecordAddress = (
|
|
713
|
+
typeArg: TGatewayClientDnsRecordType,
|
|
714
|
+
valueArg: unknown,
|
|
715
|
+
): valueArg is string => {
|
|
716
|
+
if (typeArg === 'A') {
|
|
717
|
+
return isPlausibleUnicastIpv4(valueArg);
|
|
718
|
+
}
|
|
719
|
+
const address = parseCanonicalIpAddress(valueArg);
|
|
720
|
+
// `::` is unspecified and `ff00::/8` is multicast; neither addresses one host.
|
|
721
|
+
return address?.family === 6 && address.value !== 0n && address.value >> 120n !== 0xffn;
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
/** Validates one requested address record and names every reason it is refused. */
|
|
725
|
+
export const validateGatewayClientDnsRecord = (valueArg: unknown, pathArg = 'record'): string[] => {
|
|
726
|
+
const record = readDataProperties(valueArg);
|
|
727
|
+
if (record === null || !hasExactKeys(record, ['type', 'value'], ['ttl'])) {
|
|
728
|
+
return [`${pathArg} must use its exact schema`];
|
|
729
|
+
}
|
|
730
|
+
const type = record.type;
|
|
731
|
+
if (type !== 'A' && type !== 'AAAA') {
|
|
732
|
+
return [`${pathArg} type must be A or AAAA`];
|
|
733
|
+
}
|
|
734
|
+
const errors: string[] = [];
|
|
735
|
+
if (!isGatewayClientDnsRecordAddress(type, record.value)) {
|
|
736
|
+
errors.push(type === 'A'
|
|
737
|
+
? `${pathArg} value must be a canonical IPv4 host address (not 0.0.0.0/8, multicast or reserved)`
|
|
738
|
+
: `${pathArg} value must be a canonical RFC 5952 IPv6 host address (not unspecified or multicast)`);
|
|
739
|
+
}
|
|
740
|
+
const { minTtl, maxTtl } = gatewayClientHostnameContract;
|
|
741
|
+
if (Object.hasOwn(record, 'ttl') && !(typeof record.ttl === 'number'
|
|
742
|
+
&& Number.isSafeInteger(record.ttl) && record.ttl >= minTtl && record.ttl <= maxTtl)) {
|
|
743
|
+
errors.push(`${pathArg} ttl must be an integer from ${minTtl} to ${maxTtl} seconds`);
|
|
744
|
+
}
|
|
745
|
+
return errors;
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* True when a valid record's value lies inside one of the allowance's prefixes of its family.
|
|
750
|
+
*
|
|
751
|
+
* A prefix that is not canonical allows nothing, so an allowance a gateway would refuse never
|
|
752
|
+
* widens what a record may point at.
|
|
753
|
+
*/
|
|
754
|
+
export const isGatewayClientDnsRecordAllowed = (
|
|
755
|
+
recordArg: IGatewayClientDnsRecord,
|
|
756
|
+
allowedDnsRecordAddressesArg: readonly string[] | undefined,
|
|
757
|
+
): boolean => {
|
|
758
|
+
if (validateGatewayClientDnsRecord(recordArg).length > 0) {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
const address = parseCanonicalIpAddress(recordArg.value);
|
|
762
|
+
const allowance = allowedDnsRecordAddressesArg === undefined
|
|
763
|
+
? []
|
|
764
|
+
: readDataElements(allowedDnsRecordAddressesArg, gatewayClientHostnameContract.maxAllowedDnsRecordAddresses);
|
|
765
|
+
return address !== null && allowance !== null && allowance.some((prefixArg) => {
|
|
766
|
+
const prefix = parseCanonicalIpPrefix(prefixArg);
|
|
767
|
+
return prefix !== null && ipPrefixContains(prefix, address);
|
|
768
|
+
});
|
|
769
|
+
};
|
package/ts/data/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './cluster.js';
|
|
|
3
3
|
export * from './clusterruntime.js';
|
|
4
4
|
export * from './clustervpn.js';
|
|
5
5
|
export * from './clusteringress.js';
|
|
6
|
+
export * from './clusteringressdesignation.js';
|
|
6
7
|
export * from './clusterrelaycredential.js';
|
|
7
8
|
export * from './config.js';
|
|
8
9
|
export * from './corestore.js';
|
|
@@ -40,6 +41,9 @@ export * from './networkprojection.js';
|
|
|
40
41
|
export type { IRuntimeNetworkRouterPacketGrant, IRuntimeNetworkRouterProjection } from './networkrouter.js';
|
|
41
42
|
export type { IRuntimeNetworkHostIngress, IRuntimeNetworkHostPacketGrant, IRuntimeNetworkHostProjection } from './networkhost.js';
|
|
42
43
|
export { runtimeNetworkHostContract } from './networkhost.js';
|
|
44
|
+
export type { IRuntimeNetworkWorkloadIngress, IRuntimeNetworkWorkloadIngressPacketGrant,
|
|
45
|
+
IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
|
|
46
|
+
export { runtimeNetworkWorkloadIngressContract } from './networkworkloadingress.js';
|
|
43
47
|
export * from './networkprotection.js';
|
|
44
48
|
export * from './networksigning.js';
|
|
45
49
|
export * from './privatenetwork.js';
|
|
@@ -48,6 +52,7 @@ export * from './releaseversion.js';
|
|
|
48
52
|
export * from './runtimeassignment.js';
|
|
49
53
|
export * from './runtimeconfig.js';
|
|
50
54
|
export * from './runtimestorageclaim.js';
|
|
55
|
+
export * from './runtimelocalstorageclaim.js';
|
|
51
56
|
export * from './runtimesession.js';
|
|
52
57
|
export * from './runtimeterminalreceipt.js';
|
|
53
58
|
export * from './secret.js';
|
|
@@ -9,6 +9,8 @@ import { routerPacketGrantsOf, snapshotRouterProjection,
|
|
|
9
9
|
type IRuntimeNetworkRouterPacketGrant, type IRuntimeNetworkRouterProjection } from './networkrouter.js';
|
|
10
10
|
import { hostPacketGrantsOf, snapshotHostProjection,
|
|
11
11
|
type IRuntimeNetworkHostPacketGrant, type IRuntimeNetworkHostProjection } from './networkhost.js';
|
|
12
|
+
import { snapshotWorkloadIngressProjection, workloadIngressPacketGrantsOf,
|
|
13
|
+
type IRuntimeNetworkWorkloadIngressPacketGrant, type IRuntimeNetworkWorkloadIngressProjection } from './networkworkloadingress.js';
|
|
12
14
|
import { buildPrivateNetworkFqdn, composeServicePrivateNetworkDnsPolicy,
|
|
13
15
|
snapshotPrivateNetwork, snapshotServicePrivateNetworks } from './privatenetwork.js';
|
|
14
16
|
import { captureNetwork, failNetwork, networkCanonical, networkContains, networkCounter, networkDigest,
|
|
@@ -167,6 +169,21 @@ export interface IRuntimeNetworkProjection extends IRuntimeNetworkNodeScope {
|
|
|
167
169
|
* host-origin flow; any other controller kind carrying it is refused.
|
|
168
170
|
*/
|
|
169
171
|
host?: IRuntimeNetworkHostProjection;
|
|
172
|
+
/**
|
|
173
|
+
* One-way workload ingress selections of a `cloudly` projection: the cluster ingress workload dials
|
|
174
|
+
* exact target workload ports across organizations (`IRuntimeNetworkWorkloadIngressPacketGrant`).
|
|
175
|
+
* Omitted, it keeps the canonical bytes and grants no such flow; any other controller kind
|
|
176
|
+
* carrying it is refused.
|
|
177
|
+
*/
|
|
178
|
+
workloadIngress?: IRuntimeNetworkWorkloadIngressProjection;
|
|
179
|
+
/**
|
|
180
|
+
* Ids of the protected authority's platform endpoints whose address this projection's node's own
|
|
181
|
+
* host carries, such as a relay listener or a Corestore beside the node on one machine. The node
|
|
182
|
+
* delivers workload flows to them locally instead of forwarding them. Sorted, unique and never
|
|
183
|
+
* empty when present; omitted, it keeps the canonical bytes and states that no endpoint is local.
|
|
184
|
+
* It grants nothing: a workload still reaches only the endpoints it selects.
|
|
185
|
+
*/
|
|
186
|
+
hostPlatformEndpointIds?: string[];
|
|
170
187
|
digest: TSha256Digest;
|
|
171
188
|
}
|
|
172
189
|
|
|
@@ -322,9 +339,16 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
322
339
|
requireNetwork(new TextEncoder().encode(networkCanonical(captured)).length <= runtimeNetworkProjectionContract.maximumCanonicalBytes);
|
|
323
340
|
const hasRouter = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'router');
|
|
324
341
|
const hasHost = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'host');
|
|
342
|
+
const hasWorkloadIngress = captured !== null && typeof captured === 'object' && Object.hasOwn(captured, 'workloadIngress');
|
|
343
|
+
const hasHostPlatformEndpoints = captured !== null && typeof captured === 'object'
|
|
344
|
+
&& Object.hasOwn(captured, 'hostPlatformEndpointIds');
|
|
325
345
|
const item = networkRecord(captured, ['id', 'generation', 'previous', 'controller', 'nodeId', 'runtimeNamespace',
|
|
326
346
|
'protectedAuthority', 'allocationAuthorities', 'handoff', 'networks', 'memberships', 'endpoints', 'dns', 'withdrawals', 'retiredLeases', 'retiredHandoffs', 'digest',
|
|
327
|
-
...(hasRouter ? ['router'] : []), ...(hasHost ? ['host'] : [])])
|
|
347
|
+
...(hasRouter ? ['router'] : []), ...(hasHost ? ['host'] : []), ...(hasWorkloadIngress ? ['workloadIngress'] : []),
|
|
348
|
+
...(hasHostPlatformEndpoints ? ['hostPlatformEndpointIds'] : [])]);
|
|
349
|
+
const hostPlatformEndpointIds = hasHostPlatformEndpoints
|
|
350
|
+
? networkSorted(networkList(item.hostPlatformEndpointIds, 64, networkId), value => value) : [];
|
|
351
|
+
requireNetwork(!hasHostPlatformEndpoints || hostPlatformEndpointIds.length > 0);
|
|
328
352
|
return { ...networkRevision(item), ...networkScope(item),
|
|
329
353
|
protectedAuthority: snapshotRuntimeNetworkProtectedAuthority(item.protectedAuthority),
|
|
330
354
|
allocationAuthorities: networkSorted(networkList(item.allocationAuthorities, 16, snapshotRuntimeNetworkProtectedAuthority), refKey),
|
|
@@ -335,6 +359,8 @@ export const snapshotRuntimeNetworkProjection = (value: unknown): IRuntimeNetwor
|
|
|
335
359
|
withdrawals: networkSorted(networkList(item.withdrawals, 4096, packetGrant), networkCanonical),
|
|
336
360
|
...(hasRouter ? { router: snapshotRouterProjection(item.router) } : {}),
|
|
337
361
|
...(hasHost ? { host: snapshotHostProjection(item.host) } : {}),
|
|
362
|
+
...(hasWorkloadIngress ? { workloadIngress: snapshotWorkloadIngressProjection(item.workloadIngress) } : {}),
|
|
363
|
+
...(hasHostPlatformEndpoints ? { hostPlatformEndpointIds } : {}),
|
|
338
364
|
retiredLeases: sortedReferences(item.retiredLeases), retiredHandoffs: sortedReferences(item.retiredHandoffs), digest: networkDigest(item.digest) };
|
|
339
365
|
});
|
|
340
366
|
|
|
@@ -434,6 +460,7 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
434
460
|
}
|
|
435
461
|
requireNetwork(!projection.retiredLeases.some(retired => networkEqual(retired, networkReferenceOf(lease))));
|
|
436
462
|
}
|
|
463
|
+
for (const id of projection.hostPlatformEndpointIds ?? []) requireNetwork(current.platformEndpoints.some(platform => platform.id === id));
|
|
437
464
|
for (let index = 0; index < projection.endpoints.length; index++) for (let previous = 0; previous < index; previous++) {
|
|
438
465
|
requireNetwork(!networkOverlaps(projection.endpoints[index].lease.ipv4.linkSubnet, projection.endpoints[previous].lease.ipv4.linkSubnet));
|
|
439
466
|
}
|
|
@@ -444,6 +471,8 @@ export const validateRuntimeNetworkProjection = async (value: unknown): Promise<
|
|
|
444
471
|
requireNetwork((projection.router?.withdrawals ?? []).every(grant => !routerGrants.has(networkCanonical(grant))));
|
|
445
472
|
const hostGrants = new Set(hostPacketGrantsOf(projection).map(networkCanonical));
|
|
446
473
|
requireNetwork((projection.host?.withdrawals ?? []).every(grant => !hostGrants.has(networkCanonical(grant))));
|
|
474
|
+
const workloadIngressGrants = new Set(workloadIngressPacketGrantsOf(projection).map(networkCanonical));
|
|
475
|
+
requireNetwork((projection.workloadIngress?.withdrawals ?? []).every(grant => !workloadIngressGrants.has(networkCanonical(grant))));
|
|
447
476
|
requireNetwork(projection.digest === await computeRuntimeNetworkProjectionDigest(projection));
|
|
448
477
|
return projection;
|
|
449
478
|
} catch { return failNetwork(); }
|
|
@@ -461,6 +490,12 @@ export const getRuntimeNetworkProjectionRouterPacketGrants = async (value: unkno
|
|
|
461
490
|
export const getRuntimeNetworkProjectionHostPacketGrants = async (value: unknown): Promise<IRuntimeNetworkHostPacketGrant[]> =>
|
|
462
491
|
hostPacketGrantsOf(await validateRuntimeNetworkProjection(value));
|
|
463
492
|
|
|
493
|
+
/** One-way workload ingress authority comes only from explicit signed `cloudly` selections of exact target ports. */
|
|
494
|
+
export const getRuntimeNetworkProjectionWorkloadIngressPacketGrants = async (
|
|
495
|
+
value: unknown,
|
|
496
|
+
): Promise<IRuntimeNetworkWorkloadIngressPacketGrant[]> =>
|
|
497
|
+
workloadIngressPacketGrantsOf(await validateRuntimeNetworkProjection(value));
|
|
498
|
+
|
|
464
499
|
/** Complete offline DNS material, including authorized names never previously queried. */
|
|
465
500
|
/** Select exact local receivers before expansion; all targets still come from
|
|
466
501
|
* the complete validated projection. Selection supplies no attachment authority. */
|
|
@@ -526,6 +561,7 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
526
561
|
if (previousInput === null) {
|
|
527
562
|
requireNetwork(projection.generation === 1 && applied === null && projection.withdrawals.length === 0
|
|
528
563
|
&& (projection.router?.withdrawals.length ?? 0) === 0 && (projection.host?.withdrawals.length ?? 0) === 0
|
|
564
|
+
&& (projection.workloadIngress?.withdrawals.length ?? 0) === 0
|
|
529
565
|
&& projection.retiredLeases.length === 0 && projection.retiredHandoffs.length === 0);
|
|
530
566
|
return { status: 'accepted', projection };
|
|
531
567
|
}
|
|
@@ -567,6 +603,15 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
567
603
|
for (const grant of previousHostGrants) requireNetwork(hostGrants.has(networkCanonical(grant)) || hostWithdrawals.has(networkCanonical(grant)));
|
|
568
604
|
const knownHostWithdrawals = new Set([...previousHostGrants, ...(previous.host?.withdrawals ?? [])].map(networkCanonical));
|
|
569
605
|
requireNetwork((projection.host?.withdrawals ?? []).every(grant => knownHostWithdrawals.has(networkCanonical(grant))));
|
|
606
|
+
const workloadIngressGrants = new Set(workloadIngressPacketGrantsOf(projection).map(networkCanonical));
|
|
607
|
+
const workloadIngressWithdrawals = new Set((projection.workloadIngress?.withdrawals ?? []).map(networkCanonical));
|
|
608
|
+
const previousWorkloadIngressGrants = workloadIngressPacketGrantsOf(previous);
|
|
609
|
+
for (const grant of previousWorkloadIngressGrants) requireNetwork(workloadIngressGrants.has(networkCanonical(grant))
|
|
610
|
+
|| workloadIngressWithdrawals.has(networkCanonical(grant)));
|
|
611
|
+
const knownWorkloadIngressWithdrawals = new Set([...previousWorkloadIngressGrants,
|
|
612
|
+
...(previous.workloadIngress?.withdrawals ?? [])].map(networkCanonical));
|
|
613
|
+
requireNetwork((projection.workloadIngress?.withdrawals ?? []).every(grant =>
|
|
614
|
+
knownWorkloadIngressWithdrawals.has(networkCanonical(grant))));
|
|
570
615
|
const knownLeases = [...previous.endpoints.map(endpoint => networkReferenceOf(endpoint.lease)), ...previous.retiredLeases];
|
|
571
616
|
const knownHandoffs = [...(previous.handoff ? [networkReferenceOf(previous.handoff)] : []), ...previous.retiredHandoffs];
|
|
572
617
|
requireNetwork(projection.retiredLeases.every(retired => knownLeases.some(known => networkEqual(known, retired)))
|
|
@@ -578,6 +623,9 @@ export const admitRuntimeNetworkProjection = async (
|
|
|
578
623
|
for (const grant of previous.withdrawals) requireNetwork(withdrawn.has(networkCanonical(grant)));
|
|
579
624
|
for (const grant of previous.router?.withdrawals ?? []) requireNetwork(routerWithdrawals.has(networkCanonical(grant)));
|
|
580
625
|
for (const grant of previous.host?.withdrawals ?? []) requireNetwork(hostWithdrawals.has(networkCanonical(grant)));
|
|
626
|
+
for (const grant of previous.workloadIngress?.withdrawals ?? []) {
|
|
627
|
+
requireNetwork(workloadIngressWithdrawals.has(networkCanonical(grant)));
|
|
628
|
+
}
|
|
581
629
|
requireCarried(previous.retiredLeases, projection.retiredLeases); requireCarried(previous.retiredHandoffs, projection.retiredHandoffs);
|
|
582
630
|
}
|
|
583
631
|
for (const old of previous.endpoints) {
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { IRuntimeAuthorityReference, IRuntimeController } from './runtimeassignment.js';
|
|
2
|
+
import type { IRuntimeNetworkWorkloadLease } from './networkprojection.js';
|
|
3
|
+
import { networkCanonical, networkCounter, networkEqual, networkList, networkRecord, networkReference,
|
|
4
|
+
networkReferenceOf, networkSameScope, networkSorted, requireNetwork } from '../private/networkcontract.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Exact one-way flow authority from one workload to one port of another: the cluster ingress
|
|
8
|
+
* workload dials the target workload's lease address on exactly this protocol and port.
|
|
9
|
+
*
|
|
10
|
+
* Unlike the projection's workload grants it needs neither the same organization nor a shared
|
|
11
|
+
* private network, and it opens one port instead of every port; nothing flows the other way except
|
|
12
|
+
* the replies of a connection the source opened.
|
|
13
|
+
*/
|
|
14
|
+
export interface IRuntimeNetworkWorkloadIngressPacketGrant {
|
|
15
|
+
source: IRuntimeAuthorityReference;
|
|
16
|
+
destination: { lease: IRuntimeAuthorityReference; protocol: 'tcp' | 'udp'; port: number };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** One target port the ingress workload may dial, and the Cloudly decision that authorized it. */
|
|
20
|
+
export interface IRuntimeNetworkWorkloadIngress {
|
|
21
|
+
/** Exact lease of the ingress workload, an endpoint of this projection. */
|
|
22
|
+
source: IRuntimeAuthorityReference;
|
|
23
|
+
/** Exact lease of the target workload, another endpoint of this projection. */
|
|
24
|
+
destination: IRuntimeAuthorityReference;
|
|
25
|
+
protocol: 'tcp' | 'udp';
|
|
26
|
+
/** The target workload's own listening port inside its network namespace. */
|
|
27
|
+
port: number;
|
|
28
|
+
/** Covered by the projection signature; it never enters the packet grant's identity. */
|
|
29
|
+
authorization: IRuntimeAuthorityReference;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Workload ingress selections plus retained denials. Only a projection of a `cloudly` controller may
|
|
34
|
+
* carry it; omitting it keeps the projection's canonical bytes and grants no workload ingress flow.
|
|
35
|
+
*/
|
|
36
|
+
export interface IRuntimeNetworkWorkloadIngressProjection {
|
|
37
|
+
/**
|
|
38
|
+
* Strictly increasing by the canonical form of `[source.id, destination.id, protocol, port]`, so
|
|
39
|
+
* one entry per tuple. Each entry names at least one endpoint placed on this projection's node.
|
|
40
|
+
*/
|
|
41
|
+
grants: IRuntimeNetworkWorkloadIngress[];
|
|
42
|
+
withdrawals: IRuntimeNetworkWorkloadIngressPacketGrant[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const runtimeNetworkWorkloadIngressContract = Object.freeze({
|
|
46
|
+
controllerKind: 'cloudly',
|
|
47
|
+
source: 'ingress-workload-lease',
|
|
48
|
+
destination: 'target-workload-lease-address-exact-port',
|
|
49
|
+
maximumGrants: 1024,
|
|
50
|
+
maximumWithdrawals: 4096,
|
|
51
|
+
} as const);
|
|
52
|
+
|
|
53
|
+
const grantKey = (value: {
|
|
54
|
+
source: IRuntimeAuthorityReference; destination: IRuntimeAuthorityReference; protocol: string; port: number;
|
|
55
|
+
}): string => networkCanonical([value.source.id, value.destination.id, value.protocol, value.port]);
|
|
56
|
+
const protocol = (value: unknown): 'tcp' | 'udp' => {
|
|
57
|
+
requireNetwork(value === 'tcp' || value === 'udp'); return value;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const packetGrant = (value: unknown): IRuntimeNetworkWorkloadIngressPacketGrant => {
|
|
61
|
+
const item = networkRecord(value, ['source', 'destination']);
|
|
62
|
+
const destination = networkRecord(item.destination, ['lease', 'protocol', 'port']);
|
|
63
|
+
return { source: networkReference(item.source), destination: { lease: networkReference(destination.lease),
|
|
64
|
+
protocol: protocol(destination.protocol), port: networkCounter(destination.port, 65535) } };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const grant = (value: unknown): IRuntimeNetworkWorkloadIngress => {
|
|
68
|
+
const item = networkRecord(value, ['source', 'destination', 'protocol', 'port', 'authorization']);
|
|
69
|
+
const result = { source: networkReference(item.source), destination: networkReference(item.destination),
|
|
70
|
+
protocol: protocol(item.protocol), port: networkCounter(item.port, 65535), authorization: networkReference(item.authorization) };
|
|
71
|
+
requireNetwork(result.source.id !== result.destination.id);
|
|
72
|
+
return result;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Internal parser: the enclosing projection has already captured caller input. */
|
|
76
|
+
export const snapshotWorkloadIngressProjection = (value: unknown): IRuntimeNetworkWorkloadIngressProjection => {
|
|
77
|
+
const item = networkRecord(value, ['grants', 'withdrawals']);
|
|
78
|
+
return {
|
|
79
|
+
grants: networkSorted(networkList(item.grants, runtimeNetworkWorkloadIngressContract.maximumGrants, grant), grantKey),
|
|
80
|
+
withdrawals: networkSorted(networkList(item.withdrawals, runtimeNetworkWorkloadIngressContract.maximumWithdrawals,
|
|
81
|
+
packetGrant), networkCanonical),
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Internal composition after capture. The public helper validates the whole projection. */
|
|
86
|
+
export const workloadIngressPacketGrantsOf = (projection: {
|
|
87
|
+
controller: IRuntimeController;
|
|
88
|
+
nodeId: string;
|
|
89
|
+
runtimeNamespace: string;
|
|
90
|
+
workloadIngress?: IRuntimeNetworkWorkloadIngressProjection;
|
|
91
|
+
endpoints: Array<{ lease: IRuntimeNetworkWorkloadLease }>;
|
|
92
|
+
}): IRuntimeNetworkWorkloadIngressPacketGrant[] => {
|
|
93
|
+
const selected = projection.workloadIngress;
|
|
94
|
+
if (!selected) return [];
|
|
95
|
+
requireNetwork(projection.controller.kind === runtimeNetworkWorkloadIngressContract.controllerKind);
|
|
96
|
+
const leaseOf = (reference: IRuntimeAuthorityReference): IRuntimeNetworkWorkloadLease => {
|
|
97
|
+
const endpoint = projection.endpoints.find(candidate => networkEqual(networkReferenceOf(candidate.lease), reference));
|
|
98
|
+
requireNetwork(endpoint); return endpoint.lease;
|
|
99
|
+
};
|
|
100
|
+
const result = selected.grants.map((entry): IRuntimeNetworkWorkloadIngressPacketGrant => {
|
|
101
|
+
const source = leaseOf(entry.source), destination = leaseOf(entry.destination);
|
|
102
|
+
requireNetwork(networkSameScope(source, projection) || networkSameScope(destination, projection));
|
|
103
|
+
return { source: networkReferenceOf(source),
|
|
104
|
+
destination: { lease: networkReferenceOf(destination), protocol: entry.protocol, port: entry.port } };
|
|
105
|
+
});
|
|
106
|
+
return result.sort((left, right) => networkCanonical(left) < networkCanonical(right) ? -1 : 1);
|
|
107
|
+
};
|