@serve.zone/interfaces 32.20.0 → 32.21.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 +38 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/data/gateway.d.ts +169 -0
- package/dist_ts/data/gateway.js +115 -2
- package/dist_ts/private/ipaddress.d.ts +29 -0
- package/dist_ts/private/ipaddress.js +120 -0
- package/dist_ts/requests/gateway.d.ts +58 -1
- package/package.json +2 -2
- package/readme.md +32 -0
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/data/gateway.ts +308 -0
- package/ts/private/ipaddress.ts +140 -0
- package/ts/requests/gateway.ts +71 -0
package/changelog.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2026-09-25 - 32.21.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add gateway-client hostname records and certificates that need no gateway route.
|
|
8
|
+
- `syncGatewayClientDnsRecord` (`IReq_SyncGatewayClientDnsRecord`) publishes, replaces or
|
|
9
|
+
withdraws the one `A` or `AAAA` record a `IGatewayClientHostnameOwnership` (`appId`,
|
|
10
|
+
`hostname`) holds, at a value the client chooses. The value must lie inside the new
|
|
11
|
+
gateway-client policy field `allowedDnsRecordAddresses`, a list of canonical CIDR prefixes on
|
|
12
|
+
`IGatewayClient`, `IGatewayClientProvisioningSpec` and `IGatewayClientContext`. An absent or
|
|
13
|
+
empty list allows no record.
|
|
14
|
+
- `getGatewayClientCertificate` (`IReq_GetGatewayClientCertificate`) answers the exact
|
|
15
|
+
certificate for the owned hostname as `ready` (`IGatewayClientCertificate` with its PEM key
|
|
16
|
+
and `renewAfter`), `pending` or `refused` (`TGatewayClientCertificateRefusal`).
|
|
17
|
+
`releaseGatewayClientCertificate` (`IReq_ReleaseGatewayClientCertificate`) removes that
|
|
18
|
+
material.
|
|
19
|
+
- The three requests accept gateway-client credentials only and reuse the existing token
|
|
20
|
+
capabilities `syncDnsRecords`, `requestCertificates` and `readCertificates`.
|
|
21
|
+
- Gateways advertise support with the optional capability flags `dns.clientRecords` and
|
|
22
|
+
`certificates.exactIssuance`.
|
|
23
|
+
- `TGatewayClientDnsRecordStatus` reuses the route DNS vocabulary, and
|
|
24
|
+
`gatewayClientHostnameContract` states the TTL, allowance and length bounds.
|
|
25
|
+
- Add the shared validators `validateGatewayClientHostnameOwnership`,
|
|
26
|
+
`validateGatewayClientDnsRecord`, `validateGatewayAllowedDnsRecordAddresses`,
|
|
27
|
+
`isGatewayClientDnsRecordAllowed`, `isGatewayAddressPrefix` and `isGatewayClientHostname`.
|
|
28
|
+
They accept only the canonical spelling of each address: the dotted quad, or RFC 5952 text
|
|
29
|
+
for IPv6.
|
|
30
|
+
- Every addition is optional or new, so no existing shape narrows.
|
|
31
|
+
|
|
32
|
+
## 2026-09-24 - 32.20.1
|
|
33
|
+
|
|
34
|
+
### Maintenance
|
|
35
|
+
|
|
36
|
+
- Update `@push.rocks/smartlog` `^3.2.2` → `^4.0.0`. The package exports only the
|
|
37
|
+
`@push.rocks/smartlog/interfaces` types (`ILogPackage`), which are unchanged in 4.0.0, and
|
|
38
|
+
4.0.0 no longer depends on the deprecated `@push.rocks/smartclickhouse`, so that package leaves
|
|
39
|
+
the production dependency tree of every consumer.
|
|
40
|
+
|
|
3
41
|
## 2026-09-24 - 32.20.0
|
|
4
42
|
|
|
5
43
|
### Features
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export const commitinfo = {
|
|
5
5
|
name: '@serve.zone/interfaces',
|
|
6
|
-
version: '32.
|
|
6
|
+
version: '32.21.0',
|
|
7
7
|
description: 'Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.'
|
|
8
8
|
};
|
|
9
9
|
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSx3QkFBd0I7SUFDOUIsT0FBTyxFQUFFLFNBQVM7SUFDbEIsV0FBVyxFQUFFLHVGQUF1RjtDQUNyRyxDQUFBIn0=
|
|
@@ -90,6 +90,11 @@ export interface IGatewayCapabilities {
|
|
|
90
90
|
read: boolean;
|
|
91
91
|
export: boolean;
|
|
92
92
|
forceRenew: boolean;
|
|
93
|
+
/**
|
|
94
|
+
* `getGatewayClientCertificate` and `releaseGatewayClientCertificate` are served: an exact
|
|
95
|
+
* certificate for one owned hostname, with no route behind it. Absent means unsupported.
|
|
96
|
+
*/
|
|
97
|
+
exactIssuance?: boolean;
|
|
93
98
|
};
|
|
94
99
|
email: {
|
|
95
100
|
domains: boolean;
|
|
@@ -102,6 +107,11 @@ export interface IGatewayCapabilities {
|
|
|
102
107
|
dns: {
|
|
103
108
|
authoritative: boolean;
|
|
104
109
|
providerManaged: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* `syncGatewayClientDnsRecord` is served: an address record at a value the client chooses
|
|
112
|
+
* inside its `allowedDnsRecordAddresses`. Absent means unsupported.
|
|
113
|
+
*/
|
|
114
|
+
clientRecords?: boolean;
|
|
105
115
|
};
|
|
106
116
|
http3: {
|
|
107
117
|
enabled: boolean;
|
|
@@ -148,6 +158,11 @@ export interface IGatewayClient {
|
|
|
148
158
|
/** `*` matches any hostname under a managed zone; `*.zone` matches exactly one label. */
|
|
149
159
|
hostnamePatterns: string[];
|
|
150
160
|
allowedRouteTargets: IGatewayRouteTargetAllowance[];
|
|
161
|
+
/**
|
|
162
|
+
* Canonical address prefixes the client's own address records may point into; see
|
|
163
|
+
* `validateGatewayAllowedDnsRecordAddresses`. Absent or empty allows no address record.
|
|
164
|
+
*/
|
|
165
|
+
allowedDnsRecordAddresses?: string[];
|
|
151
166
|
capabilities: IGatewayTokenCapabilities;
|
|
152
167
|
enabled: boolean;
|
|
153
168
|
tokenCount?: number;
|
|
@@ -166,6 +181,11 @@ export interface IGatewayClientProvisioningSpec {
|
|
|
166
181
|
/** `*` matches any hostname under a managed zone; `*.zone` matches exactly one label. */
|
|
167
182
|
hostnamePatterns: string[];
|
|
168
183
|
allowedRouteTargets: IGatewayRouteTargetAllowance[];
|
|
184
|
+
/**
|
|
185
|
+
* Canonical address prefixes the client's own address records may point into; see
|
|
186
|
+
* `validateGatewayAllowedDnsRecordAddresses`. Absent or empty allows no address record.
|
|
187
|
+
*/
|
|
188
|
+
allowedDnsRecordAddresses?: string[];
|
|
169
189
|
capabilities: IGatewayTokenCapabilities;
|
|
170
190
|
enabled?: boolean;
|
|
171
191
|
}
|
|
@@ -194,6 +214,8 @@ interface IGatewayClientContextBase {
|
|
|
194
214
|
/** `*` matches any hostname under a managed zone; `*.zone` matches exactly one label. */
|
|
195
215
|
hostnamePatterns: string[];
|
|
196
216
|
allowedRouteTargets: IGatewayRouteTargetAllowance[];
|
|
217
|
+
/** Absent on gateways that predate address records; read it as an empty allowance. */
|
|
218
|
+
allowedDnsRecordAddresses?: string[];
|
|
197
219
|
capabilities: IGatewayTokenCapabilities;
|
|
198
220
|
}
|
|
199
221
|
/** Server-authoritative lifecycle of an authenticated gateway-client credential. */
|
|
@@ -410,4 +432,151 @@ export interface IGatewayRouteSyncResult {
|
|
|
410
432
|
dns?: IGatewayRouteDnsResult;
|
|
411
433
|
message?: string;
|
|
412
434
|
}
|
|
435
|
+
/**
|
|
436
|
+
* Bounds of the gateway-client address record and certificate contracts.
|
|
437
|
+
*
|
|
438
|
+
* A gateway applies `defaultTtl` to a record that names none, and refuses a TTL outside
|
|
439
|
+
* `minTtl`..`maxTtl` as it refuses an allowance list longer than `maxAllowedDnsRecordAddresses`.
|
|
440
|
+
*/
|
|
441
|
+
export declare const gatewayClientHostnameContract: Readonly<{
|
|
442
|
+
minTtl: 60;
|
|
443
|
+
maxTtl: 86400;
|
|
444
|
+
defaultTtl: 300;
|
|
445
|
+
maxAllowedDnsRecordAddresses: 64;
|
|
446
|
+
maxAppIdLength: 128;
|
|
447
|
+
maxHostnameLength: 253;
|
|
448
|
+
}>;
|
|
449
|
+
/**
|
|
450
|
+
* What one gateway client owns a hostname for: its app and the exact hostname.
|
|
451
|
+
*
|
|
452
|
+
* The gateway client itself is never named here. The gateway resolves it from the authenticating
|
|
453
|
+
* gateway-client credential, so a client cannot act for another one. One ownership holds at most
|
|
454
|
+
* one address record and one certificate for its hostname, and a hostname is held by at most one
|
|
455
|
+
* ownership: a second client or app, a gateway route claim, or a record the gateway does not
|
|
456
|
+
* manage makes the hostname a conflict rather than a shared name.
|
|
457
|
+
*/
|
|
458
|
+
export interface IGatewayClientHostnameOwnership {
|
|
459
|
+
/** 1 to 128 characters of letters, digits, `.`, `_`, `:` or `-`, starting with a letter or digit. */
|
|
460
|
+
appId: string;
|
|
461
|
+
/** Lowercase fully qualified hostname without a trailing dot and without a wildcard. */
|
|
462
|
+
hostname: string;
|
|
463
|
+
}
|
|
464
|
+
/** Address record types a gateway client may publish at a value it chooses. */
|
|
465
|
+
export type TGatewayClientDnsRecordType = 'A' | 'AAAA';
|
|
466
|
+
/**
|
|
467
|
+
* One address record a gateway client asks the gateway to publish for its hostname.
|
|
468
|
+
*
|
|
469
|
+
* The value is the canonical literal of the record's family (a dotted quad for `A`, RFC 5952 text
|
|
470
|
+
* for `AAAA`) and must lie inside the client's `allowedDnsRecordAddresses`. The record is never
|
|
471
|
+
* proxied: a client that chooses its own address asks for exactly that address to be served.
|
|
472
|
+
*/
|
|
473
|
+
export interface IGatewayClientDnsRecord {
|
|
474
|
+
type: TGatewayClientDnsRecordType;
|
|
475
|
+
value: string;
|
|
476
|
+
/** Seconds, `minTtl`..`maxTtl`; `defaultTtl` when omitted. */
|
|
477
|
+
ttl?: number;
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Outcome of one address record synchronization, in the gateway route DNS vocabulary.
|
|
481
|
+
*
|
|
482
|
+
* - `managed-created` / `managed-updated` / `managed-unchanged` / `managed-deleted`: done.
|
|
483
|
+
* - `target-invalid`: the value is outside the client's address allowance.
|
|
484
|
+
* - `zone-unmanaged`: no zone the gateway manages holds the hostname.
|
|
485
|
+
* - `zone-unavailable`: the zone's DNS provider or authority cannot be reached; retryable.
|
|
486
|
+
* - `record-conflict`: another ownership, a gateway route claim, or an unmanaged record holds
|
|
487
|
+
* the hostname. The gateway never overwrites or adopts it.
|
|
488
|
+
* - `mutation-failed`: writing the record failed; retryable.
|
|
489
|
+
*/
|
|
490
|
+
export type TGatewayClientDnsRecordStatus = Extract<TGatewayRouteDnsStatus, 'managed-created' | 'managed-updated' | 'managed-unchanged' | 'managed-deleted' | 'target-invalid' | 'zone-unmanaged' | 'zone-unavailable' | 'record-conflict' | 'mutation-failed'>;
|
|
491
|
+
/** Result of `syncGatewayClientDnsRecord`. */
|
|
492
|
+
export interface IGatewayClientDnsRecordSyncResult {
|
|
493
|
+
success: boolean;
|
|
494
|
+
/** True when the same request may succeed later without any change on the client's side. */
|
|
495
|
+
retryable: boolean;
|
|
496
|
+
status: TGatewayClientDnsRecordStatus;
|
|
497
|
+
hostname: string;
|
|
498
|
+
zoneName?: string;
|
|
499
|
+
/** The record this ownership holds after the call; absent after a deletion or a refusal. */
|
|
500
|
+
record?: IGatewayRouteDnsRecord;
|
|
501
|
+
/** The records that hold the hostname, as evidence for `record-conflict`. */
|
|
502
|
+
conflictingRecords?: IGatewayRouteDnsRecord[];
|
|
503
|
+
/** Unix epoch milliseconds at which the gateway checked the DNS state. */
|
|
504
|
+
checkedAt: number;
|
|
505
|
+
/** Unix epoch milliseconds at which authoritative DNS confirmed the desired state. */
|
|
506
|
+
authoritativeVerifiedAt?: number;
|
|
507
|
+
message?: string;
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* The certificate a gateway issued for one owned hostname, with its private key.
|
|
511
|
+
*
|
|
512
|
+
* Secret-bearing: a receiver keeps the key sealed and never logs or caches the response as is.
|
|
513
|
+
*/
|
|
514
|
+
export interface IGatewayClientCertificate {
|
|
515
|
+
hostname: string;
|
|
516
|
+
/** PEM leaf certificate followed by its chain. */
|
|
517
|
+
certificatePem: string;
|
|
518
|
+
/** PEM private key of the leaf certificate. */
|
|
519
|
+
privateKeyPem: string;
|
|
520
|
+
/** Unix epoch milliseconds. */
|
|
521
|
+
validFrom: number;
|
|
522
|
+
/** Unix epoch milliseconds. */
|
|
523
|
+
validUntil: number;
|
|
524
|
+
/** Unix epoch milliseconds after which asking again yields a renewed certificate. */
|
|
525
|
+
renewAfter: number;
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Why a gateway does not issue a certificate for an ownership.
|
|
529
|
+
*
|
|
530
|
+
* - `zone-unmanaged`: no zone the gateway manages with verified authority holds the hostname.
|
|
531
|
+
* - `hostname-conflict`: another ownership or a gateway route claim holds the hostname.
|
|
532
|
+
* - `issuance-failed`: the certificate authority refused the order; `retryAfter` says when a new
|
|
533
|
+
* order is placed.
|
|
534
|
+
* - `issuance-unavailable`: the gateway's ACME configuration or DNS authority is not ready.
|
|
535
|
+
*/
|
|
536
|
+
export type TGatewayClientCertificateRefusal = 'zone-unmanaged' | 'hostname-conflict' | 'issuance-failed' | 'issuance-unavailable';
|
|
537
|
+
/** Result of `getGatewayClientCertificate`. */
|
|
538
|
+
export type TGatewayClientCertificateResult = {
|
|
539
|
+
status: 'ready';
|
|
540
|
+
certificate: IGatewayClientCertificate;
|
|
541
|
+
} | {
|
|
542
|
+
/** An issuance runs; ask again after `retryAfterMs`. */
|
|
543
|
+
status: 'pending';
|
|
544
|
+
hostname: string;
|
|
545
|
+
retryAfterMs: number;
|
|
546
|
+
} | {
|
|
547
|
+
status: 'refused';
|
|
548
|
+
hostname: string;
|
|
549
|
+
code: TGatewayClientCertificateRefusal;
|
|
550
|
+
retryable: boolean;
|
|
551
|
+
/** Unix epoch milliseconds before which asking again cannot change the answer. */
|
|
552
|
+
retryAfter?: number;
|
|
553
|
+
message?: string;
|
|
554
|
+
};
|
|
555
|
+
/** Result of `releaseGatewayClientCertificate`. */
|
|
556
|
+
export type TGatewayClientCertificateReleaseResult = {
|
|
557
|
+
success: true;
|
|
558
|
+
/** `released` when material or an issuance was held; `unchanged` when nothing was. */
|
|
559
|
+
action: 'released' | 'unchanged';
|
|
560
|
+
} | {
|
|
561
|
+
success: false;
|
|
562
|
+
retryable: boolean;
|
|
563
|
+
message: string;
|
|
564
|
+
};
|
|
565
|
+
/** True for a lowercase fully qualified hostname of two or more labels, without wildcard or dot. */
|
|
566
|
+
export declare const isGatewayClientHostname: (valueArg: unknown) => valueArg is string;
|
|
567
|
+
/** Validates one hostname ownership and names every reason it is refused. */
|
|
568
|
+
export declare const validateGatewayClientHostnameOwnership: (valueArg: unknown, pathArg?: string) => string[];
|
|
569
|
+
/** True for one canonical address prefix of either family whose host bits are zero. */
|
|
570
|
+
export declare const isGatewayAddressPrefix: (valueArg: unknown) => valueArg is string;
|
|
571
|
+
/** Validates a client's address allowance and names every reason it is refused. */
|
|
572
|
+
export declare const validateGatewayAllowedDnsRecordAddresses: (valueArg: unknown, pathArg?: string) => string[];
|
|
573
|
+
/** Validates one requested address record and names every reason it is refused. */
|
|
574
|
+
export declare const validateGatewayClientDnsRecord: (valueArg: unknown, pathArg?: string) => string[];
|
|
575
|
+
/**
|
|
576
|
+
* True when a valid record's value lies inside one of the allowance's prefixes of its family.
|
|
577
|
+
*
|
|
578
|
+
* A prefix that is not canonical allows nothing, so an allowance a gateway would refuse never
|
|
579
|
+
* widens what a record may point at.
|
|
580
|
+
*/
|
|
581
|
+
export declare const isGatewayClientDnsRecordAllowed: (recordArg: IGatewayClientDnsRecord, allowedDnsRecordAddressesArg: readonly string[] | undefined) => boolean;
|
|
413
582
|
export {};
|
package/dist_ts/data/gateway.js
CHANGED
|
@@ -1,2 +1,115 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { hasExactKeys, readDataElements, readDataProperties } from '../private/dataproperties.js';
|
|
2
|
+
import { isPlausibleUnicastIpv4 } from '../private/ipv4.js';
|
|
3
|
+
import { ipPrefixContains, parseCanonicalIpAddress, parseCanonicalIpPrefix, } from '../private/ipaddress.js';
|
|
4
|
+
/**
|
|
5
|
+
* Bounds of the gateway-client address record and certificate contracts.
|
|
6
|
+
*
|
|
7
|
+
* A gateway applies `defaultTtl` to a record that names none, and refuses a TTL outside
|
|
8
|
+
* `minTtl`..`maxTtl` as it refuses an allowance list longer than `maxAllowedDnsRecordAddresses`.
|
|
9
|
+
*/
|
|
10
|
+
export const gatewayClientHostnameContract = Object.freeze({
|
|
11
|
+
minTtl: 60,
|
|
12
|
+
maxTtl: 86_400,
|
|
13
|
+
defaultTtl: 300,
|
|
14
|
+
maxAllowedDnsRecordAddresses: 64,
|
|
15
|
+
maxAppIdLength: 128,
|
|
16
|
+
maxHostnameLength: 253,
|
|
17
|
+
});
|
|
18
|
+
const gatewayHostnameLabel = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
19
|
+
const gatewayAppId = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
20
|
+
/** True for a lowercase fully qualified hostname of two or more labels, without wildcard or dot. */
|
|
21
|
+
export const isGatewayClientHostname = (valueArg) => (typeof valueArg === 'string'
|
|
22
|
+
&& valueArg.length <= gatewayClientHostnameContract.maxHostnameLength
|
|
23
|
+
&& valueArg.split('.').length >= 2
|
|
24
|
+
&& valueArg.split('.').every((labelArg) => gatewayHostnameLabel.test(labelArg)));
|
|
25
|
+
/** Validates one hostname ownership and names every reason it is refused. */
|
|
26
|
+
export const validateGatewayClientHostnameOwnership = (valueArg, pathArg = 'ownership') => {
|
|
27
|
+
const ownership = readDataProperties(valueArg);
|
|
28
|
+
if (ownership === null || !hasExactKeys(ownership, ['appId', 'hostname'])) {
|
|
29
|
+
return [`${pathArg} must use its exact schema`];
|
|
30
|
+
}
|
|
31
|
+
const errors = [];
|
|
32
|
+
const appId = ownership.appId;
|
|
33
|
+
if (typeof appId !== 'string' || appId.length > gatewayClientHostnameContract.maxAppIdLength
|
|
34
|
+
|| !gatewayAppId.test(appId)) {
|
|
35
|
+
errors.push(`${pathArg} appId must be 1 to ${gatewayClientHostnameContract.maxAppIdLength} `
|
|
36
|
+
+ 'characters of letters, digits, dot, underscore, colon or hyphen, starting with a letter or digit');
|
|
37
|
+
}
|
|
38
|
+
if (!isGatewayClientHostname(ownership.hostname)) {
|
|
39
|
+
errors.push(`${pathArg} hostname must be a lowercase fully qualified hostname `
|
|
40
|
+
+ 'without a trailing dot or wildcard');
|
|
41
|
+
}
|
|
42
|
+
return errors;
|
|
43
|
+
};
|
|
44
|
+
/** True for one canonical address prefix of either family whose host bits are zero. */
|
|
45
|
+
export const isGatewayAddressPrefix = (valueArg) => parseCanonicalIpPrefix(valueArg) !== null;
|
|
46
|
+
/** Validates a client's address allowance and names every reason it is refused. */
|
|
47
|
+
export const validateGatewayAllowedDnsRecordAddresses = (valueArg, pathArg = 'allowedDnsRecordAddresses') => {
|
|
48
|
+
const maximum = gatewayClientHostnameContract.maxAllowedDnsRecordAddresses;
|
|
49
|
+
const prefixes = readDataElements(valueArg, maximum);
|
|
50
|
+
if (prefixes === null) {
|
|
51
|
+
return [`${pathArg} must be a list of at most ${maximum} address prefixes`];
|
|
52
|
+
}
|
|
53
|
+
const errors = [];
|
|
54
|
+
prefixes.forEach((prefixArg, indexArg) => {
|
|
55
|
+
if (!isGatewayAddressPrefix(prefixArg)) {
|
|
56
|
+
errors.push(`${pathArg}[${indexArg}] must be a canonical address prefix whose host bits are zero`);
|
|
57
|
+
}
|
|
58
|
+
else if (prefixes.indexOf(prefixArg) !== indexArg) {
|
|
59
|
+
errors.push(`${pathArg}[${indexArg}] repeats ${prefixArg}`);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return errors;
|
|
63
|
+
};
|
|
64
|
+
/** True when the value is a canonical literal of the record type that can address one host. */
|
|
65
|
+
const isGatewayClientDnsRecordAddress = (typeArg, valueArg) => {
|
|
66
|
+
if (typeArg === 'A') {
|
|
67
|
+
return isPlausibleUnicastIpv4(valueArg);
|
|
68
|
+
}
|
|
69
|
+
const address = parseCanonicalIpAddress(valueArg);
|
|
70
|
+
// `::` is unspecified and `ff00::/8` is multicast; neither addresses one host.
|
|
71
|
+
return address?.family === 6 && address.value !== 0n && address.value >> 120n !== 0xffn;
|
|
72
|
+
};
|
|
73
|
+
/** Validates one requested address record and names every reason it is refused. */
|
|
74
|
+
export const validateGatewayClientDnsRecord = (valueArg, pathArg = 'record') => {
|
|
75
|
+
const record = readDataProperties(valueArg);
|
|
76
|
+
if (record === null || !hasExactKeys(record, ['type', 'value'], ['ttl'])) {
|
|
77
|
+
return [`${pathArg} must use its exact schema`];
|
|
78
|
+
}
|
|
79
|
+
const type = record.type;
|
|
80
|
+
if (type !== 'A' && type !== 'AAAA') {
|
|
81
|
+
return [`${pathArg} type must be A or AAAA`];
|
|
82
|
+
}
|
|
83
|
+
const errors = [];
|
|
84
|
+
if (!isGatewayClientDnsRecordAddress(type, record.value)) {
|
|
85
|
+
errors.push(type === 'A'
|
|
86
|
+
? `${pathArg} value must be a canonical IPv4 host address (not 0.0.0.0/8, multicast or reserved)`
|
|
87
|
+
: `${pathArg} value must be a canonical RFC 5952 IPv6 host address (not unspecified or multicast)`);
|
|
88
|
+
}
|
|
89
|
+
const { minTtl, maxTtl } = gatewayClientHostnameContract;
|
|
90
|
+
if (Object.hasOwn(record, 'ttl') && !(typeof record.ttl === 'number'
|
|
91
|
+
&& Number.isSafeInteger(record.ttl) && record.ttl >= minTtl && record.ttl <= maxTtl)) {
|
|
92
|
+
errors.push(`${pathArg} ttl must be an integer from ${minTtl} to ${maxTtl} seconds`);
|
|
93
|
+
}
|
|
94
|
+
return errors;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* True when a valid record's value lies inside one of the allowance's prefixes of its family.
|
|
98
|
+
*
|
|
99
|
+
* A prefix that is not canonical allows nothing, so an allowance a gateway would refuse never
|
|
100
|
+
* widens what a record may point at.
|
|
101
|
+
*/
|
|
102
|
+
export const isGatewayClientDnsRecordAllowed = (recordArg, allowedDnsRecordAddressesArg) => {
|
|
103
|
+
if (validateGatewayClientDnsRecord(recordArg).length > 0) {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
const address = parseCanonicalIpAddress(recordArg.value);
|
|
107
|
+
const allowance = allowedDnsRecordAddressesArg === undefined
|
|
108
|
+
? []
|
|
109
|
+
: readDataElements(allowedDnsRecordAddressesArg, gatewayClientHostnameContract.maxAllowedDnsRecordAddresses);
|
|
110
|
+
return address !== null && allowance !== null && allowance.some((prefixArg) => {
|
|
111
|
+
const prefix = parseCanonicalIpPrefix(prefixArg);
|
|
112
|
+
return prefix !== null && ipPrefixContains(prefix, address);
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2F0ZXdheS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL2RhdGEvZ2F0ZXdheS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxPQUFPLEVBQUUsWUFBWSxFQUFFLGdCQUFnQixFQUFFLGtCQUFrQixFQUFFLE1BQU0sOEJBQThCLENBQUM7QUFDbEcsT0FBTyxFQUFFLHNCQUFzQixFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDNUQsT0FBTyxFQUNMLGdCQUFnQixFQUNoQix1QkFBdUIsRUFDdkIsc0JBQXNCLEdBQ3ZCLE1BQU0seUJBQXlCLENBQUM7QUFvZWpDOzs7OztHQUtHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sNkJBQTZCLEdBQUcsTUFBTSxDQUFDLE1BQU0sQ0FBQztJQUN6RCxNQUFNLEVBQUUsRUFBRTtJQUNWLE1BQU0sRUFBRSxNQUFNO0lBQ2QsVUFBVSxFQUFFLEdBQUc7SUFDZiw0QkFBNEIsRUFBRSxFQUFFO0lBQ2hDLGNBQWMsRUFBRSxHQUFHO0lBQ25CLGlCQUFpQixFQUFFLEdBQUc7Q0FDdkIsQ0FBQyxDQUFDO0FBbUpILE1BQU0sb0JBQW9CLEdBQUcsd0NBQXdDLENBQUM7QUFDdEUsTUFBTSxZQUFZLEdBQUcsK0JBQStCLENBQUM7QUFFckQsb0dBQW9HO0FBQ3BHLE1BQU0sQ0FBQyxNQUFNLHVCQUF1QixHQUFHLENBQUMsUUFBaUIsRUFBc0IsRUFBRSxDQUFDLENBQ2hGLE9BQU8sUUFBUSxLQUFLLFFBQVE7T0FDekIsUUFBUSxDQUFDLE1BQU0sSUFBSSw2QkFBNkIsQ0FBQyxpQkFBaUI7T0FDbEUsUUFBUSxDQUFDLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLElBQUksQ0FBQztPQUMvQixRQUFRLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMsb0JBQW9CLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQ2hGLENBQUM7QUFFRiw2RUFBNkU7QUFDN0UsTUFBTSxDQUFDLE1BQU0sc0NBQXNDLEdBQUcsQ0FDcEQsUUFBaUIsRUFDakIsT0FBTyxHQUFHLFdBQVcsRUFDWCxFQUFFO0lBQ1osTUFBTSxTQUFTLEdBQUcsa0JBQWtCLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDL0MsSUFBSSxTQUFTLEtBQUssSUFBSSxJQUFJLENBQUMsWUFBWSxDQUFDLFNBQVMsRUFBRSxDQUFDLE9BQU8sRUFBRSxVQUFVLENBQUMsQ0FBQyxFQUFFLENBQUM7UUFDMUUsT0FBTyxDQUFDLEdBQUcsT0FBTyw0QkFBNEIsQ0FBQyxDQUFDO0lBQ2xELENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBYSxFQUFFLENBQUM7SUFDNUIsTUFBTSxLQUFLLEdBQUcsU0FBUyxDQUFDLEtBQUssQ0FBQztJQUM5QixJQUFJLE9BQU8sS0FBSyxLQUFLLFFBQVEsSUFBSSxLQUFLLENBQUMsTUFBTSxHQUFHLDZCQUE2QixDQUFDLGNBQWM7V0FDdkYsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDL0IsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8sdUJBQXVCLDZCQUE2QixDQUFDLGNBQWMsR0FBRztjQUN4RixrR0FBa0csQ0FBQyxDQUFDO0lBQzFHLENBQUM7SUFDRCxJQUFJLENBQUMsdUJBQXVCLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxFQUFFLENBQUM7UUFDakQsTUFBTSxDQUFDLElBQUksQ0FBQyxHQUFHLE9BQU8seURBQXlEO2NBQzNFLG9DQUFvQyxDQUFDLENBQUM7SUFDNUMsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDO0FBQ2hCLENBQUMsQ0FBQztBQUVGLHVGQUF1RjtBQUN2RixNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBRyxDQUFDLFFBQWlCLEVBQXNCLEVBQUUsQ0FDOUUsc0JBQXNCLENBQUMsUUFBUSxDQUFDLEtBQUssSUFBSSxDQUFDO0FBRTVDLG1GQUFtRjtBQUNuRixNQUFNLENBQUMsTUFBTSx3Q0FBd0MsR0FBRyxDQUN0RCxRQUFpQixFQUNqQixPQUFPLEdBQUcsMkJBQTJCLEVBQzNCLEVBQUU7SUFDWixNQUFNLE9BQU8sR0FBRyw2QkFBNkIsQ0FBQyw0QkFBNEIsQ0FBQztJQUMzRSxNQUFNLFFBQVEsR0FBRyxnQkFBZ0IsQ0FBQyxRQUFRLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDckQsSUFBSSxRQUFRLEtBQUssSUFBSSxFQUFFLENBQUM7UUFDdEIsT0FBTyxDQUFDLEdBQUcsT0FBTyw4QkFBOEIsT0FBTyxtQkFBbUIsQ0FBQyxDQUFDO0lBQzlFLENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBYSxFQUFFLENBQUM7SUFDNUIsUUFBUSxDQUFDLE9BQU8sQ0FBQyxDQUFDLFNBQVMsRUFBRSxRQUFRLEVBQUUsRUFBRTtRQUN2QyxJQUFJLENBQUMsc0JBQXNCLENBQUMsU0FBUyxDQUFDLEVBQUUsQ0FBQztZQUN2QyxNQUFNLENBQUMsSUFBSSxDQUFDLEdBQUcsT0FBTyxJQUFJLFFBQVEsK0RBQStELENBQUMsQ0FBQztRQUNyRyxDQUFDO2FBQU0sSUFBSSxRQUFRLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxLQUFLLFFBQVEsRUFBRSxDQUFDO1lBQ3BELE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLElBQUksUUFBUSxhQUFhLFNBQVMsRUFBRSxDQUFDLENBQUM7UUFDOUQsQ0FBQztJQUNILENBQUMsQ0FBQyxDQUFDO0lBQ0gsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQyxDQUFDO0FBRUYsK0ZBQStGO0FBQy9GLE1BQU0sK0JBQStCLEdBQUcsQ0FDdEMsT0FBb0MsRUFDcEMsUUFBaUIsRUFDRyxFQUFFO0lBQ3RCLElBQUksT0FBTyxLQUFLLEdBQUcsRUFBRSxDQUFDO1FBQ3BCLE9BQU8sc0JBQXNCLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDMUMsQ0FBQztJQUNELE1BQU0sT0FBTyxHQUFHLHVCQUF1QixDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ2xELCtFQUErRTtJQUMvRSxPQUFPLE9BQU8sRUFBRSxNQUFNLEtBQUssQ0FBQyxJQUFJLE9BQU8sQ0FBQyxLQUFLLEtBQUssRUFBRSxJQUFJLE9BQU8sQ0FBQyxLQUFLLElBQUksSUFBSSxLQUFLLEtBQUssQ0FBQztBQUMxRixDQUFDLENBQUM7QUFFRixtRkFBbUY7QUFDbkYsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUcsQ0FBQyxRQUFpQixFQUFFLE9BQU8sR0FBRyxRQUFRLEVBQVksRUFBRTtJQUNoRyxNQUFNLE1BQU0sR0FBRyxrQkFBa0IsQ0FBQyxRQUFRLENBQUMsQ0FBQztJQUM1QyxJQUFJLE1BQU0sS0FBSyxJQUFJLElBQUksQ0FBQyxZQUFZLENBQUMsTUFBTSxFQUFFLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxFQUFFLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQ3pFLE9BQU8sQ0FBQyxHQUFHLE9BQU8sNEJBQTRCLENBQUMsQ0FBQztJQUNsRCxDQUFDO0lBQ0QsTUFBTSxJQUFJLEdBQUcsTUFBTSxDQUFDLElBQUksQ0FBQztJQUN6QixJQUFJLElBQUksS0FBSyxHQUFHLElBQUksSUFBSSxLQUFLLE1BQU0sRUFBRSxDQUFDO1FBQ3BDLE9BQU8sQ0FBQyxHQUFHLE9BQU8seUJBQXlCLENBQUMsQ0FBQztJQUMvQyxDQUFDO0lBQ0QsTUFBTSxNQUFNLEdBQWEsRUFBRSxDQUFDO0lBQzVCLElBQUksQ0FBQywrQkFBK0IsQ0FBQyxJQUFJLEVBQUUsTUFBTSxDQUFDLEtBQUssQ0FBQyxFQUFFLENBQUM7UUFDekQsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLEtBQUssR0FBRztZQUN0QixDQUFDLENBQUMsR0FBRyxPQUFPLHFGQUFxRjtZQUNqRyxDQUFDLENBQUMsR0FBRyxPQUFPLHNGQUFzRixDQUFDLENBQUM7SUFDeEcsQ0FBQztJQUNELE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLEdBQUcsNkJBQTZCLENBQUM7SUFDekQsSUFBSSxNQUFNLENBQUMsTUFBTSxDQUFDLE1BQU0sRUFBRSxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsT0FBTyxNQUFNLENBQUMsR0FBRyxLQUFLLFFBQVE7V0FDL0QsTUFBTSxDQUFDLGFBQWEsQ0FBQyxNQUFNLENBQUMsR0FBRyxDQUFDLElBQUksTUFBTSxDQUFDLEdBQUcsSUFBSSxNQUFNLElBQUksTUFBTSxDQUFDLEdBQUcsSUFBSSxNQUFNLENBQUMsRUFBRSxDQUFDO1FBQ3ZGLE1BQU0sQ0FBQyxJQUFJLENBQUMsR0FBRyxPQUFPLGdDQUFnQyxNQUFNLE9BQU8sTUFBTSxVQUFVLENBQUMsQ0FBQztJQUN2RixDQUFDO0lBQ0QsT0FBTyxNQUFNLENBQUM7QUFDaEIsQ0FBQyxDQUFDO0FBRUY7Ozs7O0dBS0c7QUFDSCxNQUFNLENBQUMsTUFBTSwrQkFBK0IsR0FBRyxDQUM3QyxTQUFrQyxFQUNsQyw0QkFBMkQsRUFDbEQsRUFBRTtJQUNYLElBQUksOEJBQThCLENBQUMsU0FBUyxDQUFDLENBQUMsTUFBTSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3pELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE1BQU0sT0FBTyxHQUFHLHVCQUF1QixDQUFDLFNBQVMsQ0FBQyxLQUFLLENBQUMsQ0FBQztJQUN6RCxNQUFNLFNBQVMsR0FBRyw0QkFBNEIsS0FBSyxTQUFTO1FBQzFELENBQUMsQ0FBQyxFQUFFO1FBQ0osQ0FBQyxDQUFDLGdCQUFnQixDQUFDLDRCQUE0QixFQUFFLDZCQUE2QixDQUFDLDRCQUE0QixDQUFDLENBQUM7SUFDL0csT0FBTyxPQUFPLEtBQUssSUFBSSxJQUFJLFNBQVMsS0FBSyxJQUFJLElBQUksU0FBUyxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVMsRUFBRSxFQUFFO1FBQzVFLE1BQU0sTUFBTSxHQUFHLHNCQUFzQixDQUFDLFNBQVMsQ0FBQyxDQUFDO1FBQ2pELE9BQU8sTUFBTSxLQUFLLElBQUksSUFBSSxnQkFBZ0IsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLENBQUM7SUFDOUQsQ0FBQyxDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMifQ==
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical IP address and prefix text, read into numbers a prefix test can compare.
|
|
3
|
+
*
|
|
4
|
+
* Only the one canonical spelling of each value is accepted: a dotted quad without leading zeros
|
|
5
|
+
* for IPv4, and the RFC 5952 form for IPv6 (lowercase, no leading zeros, the longest run of two or
|
|
6
|
+
* more zero groups compressed, the first run on a tie). A value two peers could spell differently
|
|
7
|
+
* would let the same address pass one comparison and fail another, so a non-canonical spelling is
|
|
8
|
+
* refused rather than normalized. IPv6 text with an embedded dotted quad or a zone index is refused
|
|
9
|
+
* for the same reason.
|
|
10
|
+
*/
|
|
11
|
+
export type TIpAddressFamily = 4 | 6;
|
|
12
|
+
export interface IIpAddressValue {
|
|
13
|
+
family: TIpAddressFamily;
|
|
14
|
+
value: bigint;
|
|
15
|
+
}
|
|
16
|
+
export interface IIpPrefixValue {
|
|
17
|
+
family: TIpAddressFamily;
|
|
18
|
+
network: bigint;
|
|
19
|
+
length: number;
|
|
20
|
+
}
|
|
21
|
+
/** One canonical address literal of either family, or `null` for anything else. */
|
|
22
|
+
export declare const parseCanonicalIpAddress: (valueArg: unknown) => IIpAddressValue | null;
|
|
23
|
+
/**
|
|
24
|
+
* One canonical prefix `<address>/<length>` of either family whose host bits are all zero, or
|
|
25
|
+
* `null` for anything else. The length is written without leading zeros.
|
|
26
|
+
*/
|
|
27
|
+
export declare const parseCanonicalIpPrefix: (valueArg: unknown) => IIpPrefixValue | null;
|
|
28
|
+
/** True when the address lies inside the prefix; families never contain each other. */
|
|
29
|
+
export declare const ipPrefixContains: (prefixArg: IIpPrefixValue, addressArg: IIpAddressValue) => boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical IP address and prefix text, read into numbers a prefix test can compare.
|
|
3
|
+
*
|
|
4
|
+
* Only the one canonical spelling of each value is accepted: a dotted quad without leading zeros
|
|
5
|
+
* for IPv4, and the RFC 5952 form for IPv6 (lowercase, no leading zeros, the longest run of two or
|
|
6
|
+
* more zero groups compressed, the first run on a tie). A value two peers could spell differently
|
|
7
|
+
* would let the same address pass one comparison and fail another, so a non-canonical spelling is
|
|
8
|
+
* refused rather than normalized. IPv6 text with an embedded dotted quad or a zone index is refused
|
|
9
|
+
* for the same reason.
|
|
10
|
+
*/
|
|
11
|
+
const familyBits = (familyArg) => (familyArg === 4 ? 32 : 128);
|
|
12
|
+
const parseIpv4 = (valueArg) => {
|
|
13
|
+
if (valueArg.length > 15) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const octets = valueArg.split('.');
|
|
17
|
+
if (octets.length !== 4
|
|
18
|
+
|| !octets.every((octetArg) => /^(?:0|[1-9][0-9]{0,2})$/.test(octetArg) && Number(octetArg) <= 255)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return octets.reduce((sumArg, octetArg) => (sumArg << 8n) | BigInt(Number(octetArg)), 0n);
|
|
22
|
+
};
|
|
23
|
+
const parseIpv6Groups = (valueArg) => {
|
|
24
|
+
if (valueArg.length > 39 || !/^[0-9a-f:]+$/.test(valueArg)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const halves = valueArg.split('::');
|
|
28
|
+
if (halves.length > 2) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const head = halves[0] === '' ? [] : halves[0].split(':');
|
|
32
|
+
if (halves.length === 1) {
|
|
33
|
+
return head;
|
|
34
|
+
}
|
|
35
|
+
const tail = halves[1] === '' ? [] : halves[1].split(':');
|
|
36
|
+
if (head.length + tail.length > 7) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return [...head, ...new Array(8 - head.length - tail.length).fill('0'), ...tail];
|
|
40
|
+
};
|
|
41
|
+
const parseIpv6 = (valueArg) => {
|
|
42
|
+
const groups = parseIpv6Groups(valueArg);
|
|
43
|
+
if (groups === null || groups.length !== 8
|
|
44
|
+
|| !groups.every((groupArg) => /^[0-9a-f]{1,4}$/.test(groupArg))) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
return groups.reduce((sumArg, groupArg) => (sumArg << 16n) | BigInt(parseInt(groupArg, 16)), 0n);
|
|
48
|
+
};
|
|
49
|
+
/** The RFC 5952 text of one IPv6 address. */
|
|
50
|
+
const formatIpv6 = (valueArg) => {
|
|
51
|
+
const groups = [];
|
|
52
|
+
for (let index = 7; index >= 0; index--) {
|
|
53
|
+
groups.push(Number((valueArg >> BigInt(index * 16)) & 0xffffn));
|
|
54
|
+
}
|
|
55
|
+
let bestStart = -1;
|
|
56
|
+
let bestLength = 0;
|
|
57
|
+
for (let start = 0; start < 8;) {
|
|
58
|
+
if (groups[start] !== 0) {
|
|
59
|
+
start++;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
let end = start;
|
|
63
|
+
while (end < 8 && groups[end] === 0) {
|
|
64
|
+
end++;
|
|
65
|
+
}
|
|
66
|
+
if (end - start > bestLength) {
|
|
67
|
+
bestStart = start;
|
|
68
|
+
bestLength = end - start;
|
|
69
|
+
}
|
|
70
|
+
start = end;
|
|
71
|
+
}
|
|
72
|
+
const text = groups.map((groupArg) => groupArg.toString(16));
|
|
73
|
+
if (bestLength < 2) {
|
|
74
|
+
return text.join(':');
|
|
75
|
+
}
|
|
76
|
+
return `${text.slice(0, bestStart).join(':')}::${text.slice(bestStart + bestLength).join(':')}`;
|
|
77
|
+
};
|
|
78
|
+
/** One canonical address literal of either family, or `null` for anything else. */
|
|
79
|
+
export const parseCanonicalIpAddress = (valueArg) => {
|
|
80
|
+
if (typeof valueArg !== 'string') {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const ipv4 = parseIpv4(valueArg);
|
|
84
|
+
if (ipv4 !== null) {
|
|
85
|
+
return { family: 4, value: ipv4 };
|
|
86
|
+
}
|
|
87
|
+
const ipv6 = parseIpv6(valueArg);
|
|
88
|
+
return ipv6 !== null && formatIpv6(ipv6) === valueArg ? { family: 6, value: ipv6 } : null;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* One canonical prefix `<address>/<length>` of either family whose host bits are all zero, or
|
|
92
|
+
* `null` for anything else. The length is written without leading zeros.
|
|
93
|
+
*/
|
|
94
|
+
export const parseCanonicalIpPrefix = (valueArg) => {
|
|
95
|
+
if (typeof valueArg !== 'string') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const parts = valueArg.split('/');
|
|
99
|
+
if (parts.length !== 2 || !/^(?:0|[1-9][0-9]{0,2})$/.test(parts[1])) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const address = parseCanonicalIpAddress(parts[0]);
|
|
103
|
+
const length = Number(parts[1]);
|
|
104
|
+
if (address === null || length > familyBits(address.family)) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const hostBits = BigInt(familyBits(address.family) - length);
|
|
108
|
+
return address.value % (1n << hostBits) === 0n
|
|
109
|
+
? { family: address.family, network: address.value, length }
|
|
110
|
+
: null;
|
|
111
|
+
};
|
|
112
|
+
/** True when the address lies inside the prefix; families never contain each other. */
|
|
113
|
+
export const ipPrefixContains = (prefixArg, addressArg) => {
|
|
114
|
+
if (prefixArg.family !== addressArg.family) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const hostBits = BigInt(familyBits(prefixArg.family) - prefixArg.length);
|
|
118
|
+
return addressArg.value >> hostBits === prefixArg.network >> hostBits;
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaXBhZGRyZXNzLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vdHMvcHJpdmF0ZS9pcGFkZHJlc3MudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7OztHQVNHO0FBZUgsTUFBTSxVQUFVLEdBQUcsQ0FBQyxTQUEyQixFQUFVLEVBQUUsQ0FBQyxDQUFDLFNBQVMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUM7QUFFekYsTUFBTSxTQUFTLEdBQUcsQ0FBQyxRQUFnQixFQUFpQixFQUFFO0lBQ3BELElBQUksUUFBUSxDQUFDLE1BQU0sR0FBRyxFQUFFLEVBQUUsQ0FBQztRQUN6QixPQUFPLElBQUksQ0FBQztJQUNkLENBQUM7SUFDRCxNQUFNLE1BQU0sR0FBRyxRQUFRLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDO0lBQ25DLElBQUksTUFBTSxDQUFDLE1BQU0sS0FBSyxDQUFDO1dBQ2xCLENBQUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMseUJBQXlCLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxRQUFRLENBQUMsSUFBSSxHQUFHLENBQUMsRUFBRSxDQUFDO1FBQ3RHLE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUNELE9BQU8sTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE1BQU0sRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsTUFBTSxJQUFJLEVBQUUsQ0FBQyxHQUFHLE1BQU0sQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQztBQUM1RixDQUFDLENBQUM7QUFFRixNQUFNLGVBQWUsR0FBRyxDQUFDLFFBQWdCLEVBQW1CLEVBQUU7SUFDNUQsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLEVBQUUsSUFBSSxDQUFDLGNBQWMsQ0FBQyxJQUFJ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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as plugins from '../plugins.js';
|
|
2
|
-
import type { IGatewayCapabilities, IGatewayClient, IGatewayClientCredential, IGatewayClientProvisioningSpec, IGatewayClientContext, IGatewayClientOwnership, IGatewayClientRoute, IGatewayDnsRecord, IGatewayDomain, IGatewayIdentity, IGatewayRouteConfig, IGatewayRouteSyncResult, TGatewayRouteDnsMode, TGatewayClientProvisioningAction } from '../data/gateway.js';
|
|
2
|
+
import type { IGatewayCapabilities, IGatewayClient, IGatewayClientCredential, IGatewayClientProvisioningSpec, IGatewayClientContext, IGatewayClientDnsRecord, IGatewayClientDnsRecordSyncResult, IGatewayClientHostnameOwnership, IGatewayClientOwnership, IGatewayClientRoute, IGatewayDnsRecord, IGatewayDomain, IGatewayIdentity, IGatewayRouteConfig, IGatewayRouteSyncResult, TGatewayRouteDnsMode, TGatewayClientCertificateReleaseResult, TGatewayClientCertificateResult, TGatewayClientProvisioningAction } from '../data/gateway.js';
|
|
3
3
|
import type { IMailOverviewDomain, IMailOverviewEmail } from './mail.js';
|
|
4
4
|
/**
|
|
5
5
|
* Gateway client contracts: the machine-facing API a gateway (dcrouter)
|
|
@@ -172,6 +172,63 @@ export interface IReq_SyncGatewayClientRoute extends plugins.typedrequestInterfa
|
|
|
172
172
|
};
|
|
173
173
|
response: IGatewayRouteSyncResult;
|
|
174
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Publishes, replaces or withdraws the one address record a gateway-client ownership holds.
|
|
177
|
+
*
|
|
178
|
+
* Gateway-client credentials only, with the `syncDnsRecords` capability; the hostname must match
|
|
179
|
+
* the client's `hostnamePatterns`. Idempotent: the same record again answers `managed-unchanged`,
|
|
180
|
+
* and deleting a record the ownership does not hold answers `managed-deleted`. The value must lie
|
|
181
|
+
* inside the client's `allowedDnsRecordAddresses` (`target-invalid` otherwise). A hostname held by
|
|
182
|
+
* anything else is `record-conflict` and stays untouched. Requires `dns.clientRecords`.
|
|
183
|
+
*/
|
|
184
|
+
export interface IReq_SyncGatewayClientDnsRecord extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_SyncGatewayClientDnsRecord> {
|
|
185
|
+
method: 'syncGatewayClientDnsRecord';
|
|
186
|
+
request: {
|
|
187
|
+
apiToken: string;
|
|
188
|
+
identity?: never;
|
|
189
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
190
|
+
} & ({
|
|
191
|
+
record: IGatewayClientDnsRecord;
|
|
192
|
+
delete?: never;
|
|
193
|
+
} | {
|
|
194
|
+
delete: true;
|
|
195
|
+
record?: never;
|
|
196
|
+
});
|
|
197
|
+
response: IGatewayClientDnsRecordSyncResult;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Answers the exact certificate for one owned hostname, starting its issuance when none is valid.
|
|
201
|
+
*
|
|
202
|
+
* Gateway-client credentials only, with `requestCertificates` and `readCertificates`; the hostname
|
|
203
|
+
* must match the client's `hostnamePatterns`. No gateway route is needed. The certificate is issued
|
|
204
|
+
* for exactly this one identifier in the client's own namespace, and asking again after
|
|
205
|
+
* `renewAfter` answers a renewed one. The `ready` answer carries the private key. Requires
|
|
206
|
+
* `certificates.exactIssuance`.
|
|
207
|
+
*/
|
|
208
|
+
export interface IReq_GetGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_GetGatewayClientCertificate> {
|
|
209
|
+
method: 'getGatewayClientCertificate';
|
|
210
|
+
request: {
|
|
211
|
+
apiToken: string;
|
|
212
|
+
identity?: never;
|
|
213
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
214
|
+
};
|
|
215
|
+
response: TGatewayClientCertificateResult;
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Removes the certificate material and any issuance an ownership holds, releasing its certificate claim; an address record under the same ownership is unaffected.
|
|
219
|
+
*
|
|
220
|
+
* Gateway-client credentials only, with `requestCertificates`. Idempotent: an ownership that holds
|
|
221
|
+
* nothing answers `unchanged`. Requires `certificates.exactIssuance`.
|
|
222
|
+
*/
|
|
223
|
+
export interface IReq_ReleaseGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ReleaseGatewayClientCertificate> {
|
|
224
|
+
method: 'releaseGatewayClientCertificate';
|
|
225
|
+
request: {
|
|
226
|
+
apiToken: string;
|
|
227
|
+
identity?: never;
|
|
228
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
229
|
+
};
|
|
230
|
+
response: TGatewayClientCertificateReleaseResult;
|
|
231
|
+
}
|
|
175
232
|
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<plugins.typedrequestInterfaces.ITypedRequest, IReq_ReprovisionCertificateDomain> {
|
|
176
233
|
method: 'reprovisionCertificateDomain';
|
|
177
234
|
request: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@serve.zone/interfaces",
|
|
3
|
-
"version": "32.
|
|
3
|
+
"version": "32.21.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Shared TypeScript interfaces and TypedRequest contracts for the serve.zone ecosystem.",
|
|
6
6
|
"exports": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@api.global/typedrequest-interfaces": "^7.1.0",
|
|
25
25
|
"@push.rocks/smartcrypto": "^2.2.0",
|
|
26
|
-
"@push.rocks/smartlog": "^
|
|
26
|
+
"@push.rocks/smartlog": "^4.0.0",
|
|
27
27
|
"@tsclass/tsclass": "^9.5.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
package/readme.md
CHANGED
|
@@ -2684,6 +2684,38 @@ const provisioning: requests.gateway.IReq_ProvisionGatewayClientCredential['requ
|
|
|
2684
2684
|
|
|
2685
2685
|
`getGatewayClientContext` returns effective live policy. A `gatewayClient` role necessarily includes the credential ID, bound client ID/type, and `policyGeneration`; consumers should reject admin/operator or mismatched contexts rather than falling back to a caller-supplied owner ID. `getGatewayClientMailOverview` provides an owner-scoped domain and recent-message summary. `getGatewayClientMailDomainCount` derives ownership exclusively from the authenticating gateway credential and returns `{ count: number }` for its distinct configured mail domains. Cloudly therefore uses only `dcrouterGatewayApiToken`; the former `dcrouterOpsApiToken` setting is not part of `data.ICloudlySettings`.
|
|
2686
2686
|
|
|
2687
|
+
### Gateway Client Hostname Records and Certificates
|
|
2688
|
+
|
|
2689
|
+
A gateway client can hold an exact hostname without a gateway route: an address record at a value it chooses, and an exact certificate for that one name. Cloudly uses this for its cluster relay names, whose A record must carry the relay node's own address and whose certificate the relay serves itself. All three requests accept only a gateway-client credential (`identity` is `never`). The gateway resolves the client from that credential, and the hostname must match the client's `hostnamePatterns`. `data.IGatewayClientHostnameOwnership` names only `{ appId, hostname }`. One ownership holds at most one address record and one certificate. A hostname held by another ownership, by a gateway route claim, or by a record the gateway does not manage is a conflict. The gateway never overwrites or adopts it.
|
|
2690
|
+
|
|
2691
|
+
- `syncGatewayClientDnsRecord` (`requests.gateway.IReq_SyncGatewayClientDnsRecord`) publishes, replaces or withdraws the ownership's `A` or `AAAA` record. It needs the token capability `syncDnsRecords`, and the gateway advertises it as `capabilities.dns.clientRecords`. The value must be a canonical unicast literal inside the client's policy field `allowedDnsRecordAddresses`, a list of canonical CIDR prefixes. An absent or empty list allows no record. The result reuses the route DNS vocabulary (`data.TGatewayClientDnsRecordStatus`): `target-invalid` for an address outside the allowance, `record-conflict` with `conflictingRecords` as evidence, and `zone-unavailable` or `mutation-failed` when retryable. Records are never proxied.
|
|
2692
|
+
- `getGatewayClientCertificate` (`requests.gateway.IReq_GetGatewayClientCertificate`) answers `ready` with `data.IGatewayClientCertificate` (PEM chain, PEM private key, `validFrom`, `validUntil`, `renewAfter`), `pending` with `retryAfterMs` while an issuance runs, or `refused` with a `data.TGatewayClientCertificateRefusal`. Asking again after `renewAfter` yields a renewed certificate. It needs `requestCertificates` and `readCertificates`, and the gateway advertises it as `capabilities.certificates.exactIssuance`. The `ready` answer carries a private key; keep it sealed and never log it.
|
|
2693
|
+
- `releaseGatewayClientCertificate` (`requests.gateway.IReq_ReleaseGatewayClientCertificate`) removes the ownership's certificate material and any issuance in flight. It is idempotent and answers `released` or `unchanged`.
|
|
2694
|
+
|
|
2695
|
+
Both sides judge values with the same helpers:
|
|
2696
|
+
|
|
2697
|
+
- `data.validateGatewayClientHostnameOwnership`
|
|
2698
|
+
- `data.validateGatewayClientDnsRecord`
|
|
2699
|
+
- `data.validateGatewayAllowedDnsRecordAddresses`
|
|
2700
|
+
- `data.isGatewayClientDnsRecordAllowed`
|
|
2701
|
+
- `data.isGatewayAddressPrefix`
|
|
2702
|
+
- `data.isGatewayClientHostname`
|
|
2703
|
+
|
|
2704
|
+
Only one spelling of each address is accepted: the dotted quad, or RFC 5952 text for IPv6. `data.gatewayClientHostnameContract` states the bounds: TTL 60 to 86400 seconds with a default of 300, at most 64 allowance prefixes, an `appId` of at most 128 characters, and a hostname of at most 253 characters.
|
|
2705
|
+
|
|
2706
|
+
```typescript
|
|
2707
|
+
import { data, requests } from '@serve.zone/interfaces';
|
|
2708
|
+
|
|
2709
|
+
const record: data.IGatewayClientDnsRecord = { type: 'A', value: '198.51.100.85', ttl: 60 };
|
|
2710
|
+
data.isGatewayClientDnsRecordAllowed(record, ['198.51.100.0/24']); // true
|
|
2711
|
+
|
|
2712
|
+
const request: requests.gateway.IReq_SyncGatewayClientDnsRecord['request'] = {
|
|
2713
|
+
apiToken: gatewayClientToken,
|
|
2714
|
+
ownership: { appId: 'cluster-relay:clusterexample', hostname: 'relay.clusterexample.example.com' },
|
|
2715
|
+
record,
|
|
2716
|
+
};
|
|
2717
|
+
```
|
|
2718
|
+
|
|
2687
2719
|
### CoreMail contracts
|
|
2688
2720
|
|
|
2689
2721
|
`requests.coremail` is the shared contract boundary for authenticated workload
|
package/ts/00_commitinfo_data.ts
CHANGED
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
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical IP address and prefix text, read into numbers a prefix test can compare.
|
|
3
|
+
*
|
|
4
|
+
* Only the one canonical spelling of each value is accepted: a dotted quad without leading zeros
|
|
5
|
+
* for IPv4, and the RFC 5952 form for IPv6 (lowercase, no leading zeros, the longest run of two or
|
|
6
|
+
* more zero groups compressed, the first run on a tie). A value two peers could spell differently
|
|
7
|
+
* would let the same address pass one comparison and fail another, so a non-canonical spelling is
|
|
8
|
+
* refused rather than normalized. IPv6 text with an embedded dotted quad or a zone index is refused
|
|
9
|
+
* for the same reason.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type TIpAddressFamily = 4 | 6;
|
|
13
|
+
|
|
14
|
+
export interface IIpAddressValue {
|
|
15
|
+
family: TIpAddressFamily;
|
|
16
|
+
value: bigint;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IIpPrefixValue {
|
|
20
|
+
family: TIpAddressFamily;
|
|
21
|
+
network: bigint;
|
|
22
|
+
length: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const familyBits = (familyArg: TIpAddressFamily): number => (familyArg === 4 ? 32 : 128);
|
|
26
|
+
|
|
27
|
+
const parseIpv4 = (valueArg: string): bigint | null => {
|
|
28
|
+
if (valueArg.length > 15) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const octets = valueArg.split('.');
|
|
32
|
+
if (octets.length !== 4
|
|
33
|
+
|| !octets.every((octetArg) => /^(?:0|[1-9][0-9]{0,2})$/.test(octetArg) && Number(octetArg) <= 255)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return octets.reduce((sumArg, octetArg) => (sumArg << 8n) | BigInt(Number(octetArg)), 0n);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseIpv6Groups = (valueArg: string): string[] | null => {
|
|
40
|
+
if (valueArg.length > 39 || !/^[0-9a-f:]+$/.test(valueArg)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const halves = valueArg.split('::');
|
|
44
|
+
if (halves.length > 2) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const head = halves[0] === '' ? [] : halves[0].split(':');
|
|
48
|
+
if (halves.length === 1) {
|
|
49
|
+
return head;
|
|
50
|
+
}
|
|
51
|
+
const tail = halves[1] === '' ? [] : halves[1].split(':');
|
|
52
|
+
if (head.length + tail.length > 7) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return [...head, ...new Array<string>(8 - head.length - tail.length).fill('0'), ...tail];
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const parseIpv6 = (valueArg: string): bigint | null => {
|
|
59
|
+
const groups = parseIpv6Groups(valueArg);
|
|
60
|
+
if (groups === null || groups.length !== 8
|
|
61
|
+
|| !groups.every((groupArg) => /^[0-9a-f]{1,4}$/.test(groupArg))) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
return groups.reduce((sumArg, groupArg) => (sumArg << 16n) | BigInt(parseInt(groupArg, 16)), 0n);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The RFC 5952 text of one IPv6 address. */
|
|
68
|
+
const formatIpv6 = (valueArg: bigint): string => {
|
|
69
|
+
const groups: number[] = [];
|
|
70
|
+
for (let index = 7; index >= 0; index--) {
|
|
71
|
+
groups.push(Number((valueArg >> BigInt(index * 16)) & 0xffffn));
|
|
72
|
+
}
|
|
73
|
+
let bestStart = -1;
|
|
74
|
+
let bestLength = 0;
|
|
75
|
+
for (let start = 0; start < 8;) {
|
|
76
|
+
if (groups[start] !== 0) {
|
|
77
|
+
start++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
let end = start;
|
|
81
|
+
while (end < 8 && groups[end] === 0) {
|
|
82
|
+
end++;
|
|
83
|
+
}
|
|
84
|
+
if (end - start > bestLength) {
|
|
85
|
+
bestStart = start;
|
|
86
|
+
bestLength = end - start;
|
|
87
|
+
}
|
|
88
|
+
start = end;
|
|
89
|
+
}
|
|
90
|
+
const text = groups.map((groupArg) => groupArg.toString(16));
|
|
91
|
+
if (bestLength < 2) {
|
|
92
|
+
return text.join(':');
|
|
93
|
+
}
|
|
94
|
+
return `${text.slice(0, bestStart).join(':')}::${text.slice(bestStart + bestLength).join(':')}`;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** One canonical address literal of either family, or `null` for anything else. */
|
|
98
|
+
export const parseCanonicalIpAddress = (valueArg: unknown): IIpAddressValue | null => {
|
|
99
|
+
if (typeof valueArg !== 'string') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const ipv4 = parseIpv4(valueArg);
|
|
103
|
+
if (ipv4 !== null) {
|
|
104
|
+
return { family: 4, value: ipv4 };
|
|
105
|
+
}
|
|
106
|
+
const ipv6 = parseIpv6(valueArg);
|
|
107
|
+
return ipv6 !== null && formatIpv6(ipv6) === valueArg ? { family: 6, value: ipv6 } : null;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* One canonical prefix `<address>/<length>` of either family whose host bits are all zero, or
|
|
112
|
+
* `null` for anything else. The length is written without leading zeros.
|
|
113
|
+
*/
|
|
114
|
+
export const parseCanonicalIpPrefix = (valueArg: unknown): IIpPrefixValue | null => {
|
|
115
|
+
if (typeof valueArg !== 'string') {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const parts = valueArg.split('/');
|
|
119
|
+
if (parts.length !== 2 || !/^(?:0|[1-9][0-9]{0,2})$/.test(parts[1])) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
const address = parseCanonicalIpAddress(parts[0]);
|
|
123
|
+
const length = Number(parts[1]);
|
|
124
|
+
if (address === null || length > familyBits(address.family)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const hostBits = BigInt(familyBits(address.family) - length);
|
|
128
|
+
return address.value % (1n << hostBits) === 0n
|
|
129
|
+
? { family: address.family, network: address.value, length }
|
|
130
|
+
: null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/** True when the address lies inside the prefix; families never contain each other. */
|
|
134
|
+
export const ipPrefixContains = (prefixArg: IIpPrefixValue, addressArg: IIpAddressValue): boolean => {
|
|
135
|
+
if (prefixArg.family !== addressArg.family) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const hostBits = BigInt(familyBits(prefixArg.family) - prefixArg.length);
|
|
139
|
+
return addressArg.value >> hostBits === prefixArg.network >> hostBits;
|
|
140
|
+
};
|
package/ts/requests/gateway.ts
CHANGED
|
@@ -5,6 +5,9 @@ import type {
|
|
|
5
5
|
IGatewayClientCredential,
|
|
6
6
|
IGatewayClientProvisioningSpec,
|
|
7
7
|
IGatewayClientContext,
|
|
8
|
+
IGatewayClientDnsRecord,
|
|
9
|
+
IGatewayClientDnsRecordSyncResult,
|
|
10
|
+
IGatewayClientHostnameOwnership,
|
|
8
11
|
IGatewayClientOwnership,
|
|
9
12
|
IGatewayClientRoute,
|
|
10
13
|
IGatewayDnsRecord,
|
|
@@ -13,6 +16,8 @@ import type {
|
|
|
13
16
|
IGatewayRouteConfig,
|
|
14
17
|
IGatewayRouteSyncResult,
|
|
15
18
|
TGatewayRouteDnsMode,
|
|
19
|
+
TGatewayClientCertificateReleaseResult,
|
|
20
|
+
TGatewayClientCertificateResult,
|
|
16
21
|
TGatewayClientProvisioningAction,
|
|
17
22
|
} from '../data/gateway.js';
|
|
18
23
|
import type { IMailOverviewDomain, IMailOverviewEmail } from './mail.js';
|
|
@@ -233,6 +238,72 @@ export interface IReq_SyncGatewayClientRoute extends plugins.typedrequestInterfa
|
|
|
233
238
|
response: IGatewayRouteSyncResult;
|
|
234
239
|
}
|
|
235
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Publishes, replaces or withdraws the one address record a gateway-client ownership holds.
|
|
243
|
+
*
|
|
244
|
+
* Gateway-client credentials only, with the `syncDnsRecords` capability; the hostname must match
|
|
245
|
+
* the client's `hostnamePatterns`. Idempotent: the same record again answers `managed-unchanged`,
|
|
246
|
+
* and deleting a record the ownership does not hold answers `managed-deleted`. The value must lie
|
|
247
|
+
* inside the client's `allowedDnsRecordAddresses` (`target-invalid` otherwise). A hostname held by
|
|
248
|
+
* anything else is `record-conflict` and stays untouched. Requires `dns.clientRecords`.
|
|
249
|
+
*/
|
|
250
|
+
export interface IReq_SyncGatewayClientDnsRecord extends plugins.typedrequestInterfaces.implementsTR<
|
|
251
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
252
|
+
IReq_SyncGatewayClientDnsRecord
|
|
253
|
+
> {
|
|
254
|
+
method: 'syncGatewayClientDnsRecord';
|
|
255
|
+
request: {
|
|
256
|
+
apiToken: string;
|
|
257
|
+
identity?: never;
|
|
258
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
259
|
+
} & (
|
|
260
|
+
| { record: IGatewayClientDnsRecord; delete?: never }
|
|
261
|
+
| { delete: true; record?: never }
|
|
262
|
+
);
|
|
263
|
+
response: IGatewayClientDnsRecordSyncResult;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Answers the exact certificate for one owned hostname, starting its issuance when none is valid.
|
|
268
|
+
*
|
|
269
|
+
* Gateway-client credentials only, with `requestCertificates` and `readCertificates`; the hostname
|
|
270
|
+
* must match the client's `hostnamePatterns`. No gateway route is needed. The certificate is issued
|
|
271
|
+
* for exactly this one identifier in the client's own namespace, and asking again after
|
|
272
|
+
* `renewAfter` answers a renewed one. The `ready` answer carries the private key. Requires
|
|
273
|
+
* `certificates.exactIssuance`.
|
|
274
|
+
*/
|
|
275
|
+
export interface IReq_GetGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
|
276
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
277
|
+
IReq_GetGatewayClientCertificate
|
|
278
|
+
> {
|
|
279
|
+
method: 'getGatewayClientCertificate';
|
|
280
|
+
request: {
|
|
281
|
+
apiToken: string;
|
|
282
|
+
identity?: never;
|
|
283
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
284
|
+
};
|
|
285
|
+
response: TGatewayClientCertificateResult;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Removes the certificate material and any issuance an ownership holds, releasing its certificate claim; an address record under the same ownership is unaffected.
|
|
290
|
+
*
|
|
291
|
+
* Gateway-client credentials only, with `requestCertificates`. Idempotent: an ownership that holds
|
|
292
|
+
* nothing answers `unchanged`. Requires `certificates.exactIssuance`.
|
|
293
|
+
*/
|
|
294
|
+
export interface IReq_ReleaseGatewayClientCertificate extends plugins.typedrequestInterfaces.implementsTR<
|
|
295
|
+
plugins.typedrequestInterfaces.ITypedRequest,
|
|
296
|
+
IReq_ReleaseGatewayClientCertificate
|
|
297
|
+
> {
|
|
298
|
+
method: 'releaseGatewayClientCertificate';
|
|
299
|
+
request: {
|
|
300
|
+
apiToken: string;
|
|
301
|
+
identity?: never;
|
|
302
|
+
ownership: IGatewayClientHostnameOwnership;
|
|
303
|
+
};
|
|
304
|
+
response: TGatewayClientCertificateReleaseResult;
|
|
305
|
+
}
|
|
306
|
+
|
|
236
307
|
export interface IReq_ReprovisionCertificateDomain extends plugins.typedrequestInterfaces.implementsTR<
|
|
237
308
|
plugins.typedrequestInterfaces.ITypedRequest,
|
|
238
309
|
IReq_ReprovisionCertificateDomain
|