@highstate/common 0.9.3 → 0.9.5

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.
Files changed (48) hide show
  1. package/dist/chunk-USV6SHAU.js +530 -0
  2. package/dist/chunk-USV6SHAU.js.map +1 -0
  3. package/dist/highstate.manifest.json +9 -5
  4. package/dist/index.js +33 -7
  5. package/dist/units/dns/record-set/index.js +18 -0
  6. package/dist/units/dns/record-set/index.js.map +1 -0
  7. package/dist/units/existing-server/index.js +34 -0
  8. package/dist/units/existing-server/index.js.map +1 -0
  9. package/dist/units/network/l3-endpoint/index.js +15 -0
  10. package/dist/units/network/l3-endpoint/index.js.map +1 -0
  11. package/dist/units/network/l4-endpoint/index.js +15 -0
  12. package/dist/units/network/l4-endpoint/index.js.map +1 -0
  13. package/dist/{script → units/script}/index.js +5 -6
  14. package/dist/units/script/index.js.map +1 -0
  15. package/dist/units/server-dns/index.js +30 -0
  16. package/dist/units/server-dns/index.js.map +1 -0
  17. package/dist/units/server-patch/index.js +29 -0
  18. package/dist/units/server-patch/index.js.map +1 -0
  19. package/dist/units/ssh/key-pair/index.js +22 -0
  20. package/dist/units/ssh/key-pair/index.js.map +1 -0
  21. package/package.json +15 -10
  22. package/src/shared/command.ts +132 -0
  23. package/src/shared/dns.ts +209 -21
  24. package/src/shared/index.ts +2 -2
  25. package/src/shared/network.ts +311 -0
  26. package/src/shared/ssh.ts +111 -38
  27. package/src/units/dns/record-set/index.ts +16 -0
  28. package/src/units/existing-server/index.ts +34 -0
  29. package/src/units/network/l3-endpoint/index.ts +9 -0
  30. package/src/units/network/l4-endpoint/index.ts +9 -0
  31. package/src/{script → units/script}/index.ts +3 -5
  32. package/src/units/server-dns/index.ts +26 -0
  33. package/src/units/server-patch/index.ts +25 -0
  34. package/src/units/ssh/key-pair/index.ts +16 -0
  35. package/dist/chunk-ZA27FN5N.js +0 -214
  36. package/dist/chunk-ZA27FN5N.js.map +0 -1
  37. package/dist/dns/record/index.js +0 -1
  38. package/dist/dns/record/index.js.map +0 -1
  39. package/dist/existing-server/index.js +0 -48
  40. package/dist/existing-server/index.js.map +0 -1
  41. package/dist/script/index.js.map +0 -1
  42. package/dist/ssh/key-pair/index.js +0 -30
  43. package/dist/ssh/key-pair/index.js.map +0 -1
  44. package/src/dns/record/index.ts +0 -0
  45. package/src/existing-server/index.ts +0 -46
  46. package/src/shared/server.ts +0 -85
  47. package/src/shared/utils.ts +0 -18
  48. package/src/ssh/key-pair/index.ts +0 -24
package/src/shared/dns.ts CHANGED
@@ -1,14 +1,26 @@
1
- import type { dns } from "@highstate/library"
1
+ import type { ArrayPatchMode, dns, network } from "@highstate/library"
2
2
  import {
3
3
  ComponentResource,
4
+ normalize,
4
5
  output,
5
6
  Output,
6
7
  Resource,
8
+ toPromise,
7
9
  type Input,
10
+ type InputArray,
11
+ type InputOrArray,
8
12
  type ResourceOptions,
9
13
  type Unwrap,
10
14
  } from "@highstate/pulumi"
11
- import { capitalize } from "remeda"
15
+ import { capitalize, groupBy, uniqueBy } from "remeda"
16
+ import { Command, type CommandHost } from "./command"
17
+ import {
18
+ filterEndpoints,
19
+ l34EndpointToString,
20
+ l3EndpointToString,
21
+ parseL3Endpoint,
22
+ type InputL3Endpoint,
23
+ } from "./network"
12
24
 
13
25
  export type DnsRecordArgs = {
14
26
  /**
@@ -24,13 +36,15 @@ export type DnsRecordArgs = {
24
36
 
25
37
  /**
26
38
  * The type of the DNS record.
39
+ *
40
+ * If not provided, will be automatically detected based on the value.
27
41
  */
28
- type: Input<string>
42
+ type?: Input<string>
29
43
 
30
44
  /**
31
45
  * The value of the DNS record.
32
46
  */
33
- value: Input<string>
47
+ value: Input<InputL3Endpoint>
34
48
 
35
49
  /**
36
50
  * Whether the DNS record is proxied (e.g. to provide DDoS protection).
@@ -53,15 +67,53 @@ export type DnsRecordArgs = {
53
67
  * Only used for some DNS record types (e.g. MX).
54
68
  */
55
69
  priority?: Input<number>
70
+
71
+ /**
72
+ * Wait for the DNS record to be created/updated at the specified environment(s) before continuing.
73
+ */
74
+ waitAt?: InputOrArray<CommandHost>
56
75
  }
57
76
 
58
- export type DnsRecordSetArgs = Omit<DnsRecordArgs, "provider"> & {
77
+ export type ResolvedDnsRecordArgs = Unwrap<Omit<DnsRecordArgs, "value" | "type">> & {
78
+ /**
79
+ * The value of the DNS record.
80
+ */
81
+ value: string
82
+
83
+ /**
84
+ * The type of the DNS record.
85
+ */
86
+ type: string
87
+ }
88
+
89
+ export type DnsRecordSetArgs = Omit<DnsRecordArgs, "provider" | "value"> & {
59
90
  /**
60
91
  * The DNS providers to use to create the DNS records.
61
92
  *
62
93
  * If multiple providers matched the specified domain, multiple DNS records will be created.
63
94
  */
64
95
  providers: Input<dns.Provider[]>
96
+
97
+ /**
98
+ * The value of the DNS record.
99
+ */
100
+ value?: Input<InputL3Endpoint>
101
+
102
+ /**
103
+ * The values of the DNS records.
104
+ */
105
+ values?: InputArray<InputL3Endpoint>
106
+ }
107
+
108
+ function getTypeByEndpoint(endpoint: network.L3Endpoint): string {
109
+ switch (endpoint.type) {
110
+ case "ipv4":
111
+ return "A"
112
+ case "ipv6":
113
+ return "AAAA"
114
+ case "hostname":
115
+ return "CNAME"
116
+ }
65
117
  }
66
118
 
67
119
  export abstract class DnsRecord extends ComponentResource {
@@ -70,17 +122,55 @@ export abstract class DnsRecord extends ComponentResource {
70
122
  */
71
123
  public readonly dnsRecord: Output<Resource>
72
124
 
73
- constructor(name: string, args: DnsRecordArgs, opts?: ResourceOptions) {
125
+ /**
126
+ * The wait commands to be executed after the DNS record is created/updated.
127
+ *
128
+ * Use this field as a dependency for other resources.
129
+ */
130
+ public readonly waitCommands: Output<Command[]>
131
+
132
+ protected constructor(name: string, args: DnsRecordArgs, opts?: ResourceOptions) {
74
133
  super("highstate:common:DnsRecord", name, args, opts)
75
134
 
76
135
  this.dnsRecord = output(args).apply(args => {
77
- return output(this.create(name, args, { ...opts, parent: this }))
136
+ const l3Endpoint = parseL3Endpoint(args.value)
137
+ const type = args.type ?? getTypeByEndpoint(l3Endpoint)
138
+
139
+ return output(
140
+ this.create(
141
+ name,
142
+ {
143
+ ...args,
144
+ type,
145
+ value: l3EndpointToString(l3Endpoint),
146
+ },
147
+ { ...opts, parent: this },
148
+ ),
149
+ )
150
+ })
151
+
152
+ this.waitCommands = output(args).apply(args => {
153
+ const waitAt = args.waitAt ? (Array.isArray(args.waitAt) ? args.waitAt : [args.waitAt]) : []
154
+
155
+ return waitAt.map(host => {
156
+ const hostname = host === "local" ? "local" : host.hostname
157
+
158
+ return new Command(
159
+ `${name}-wait-${hostname}`,
160
+ {
161
+ host,
162
+ create: `while ! getent hosts ${args.name} >/dev/null; do echo "Waiting for DNS record ${args.name} to be created"; sleep 5; done`,
163
+ triggers: [args.type, args.ttl, args.priority, args.proxied],
164
+ },
165
+ { parent: this },
166
+ )
167
+ })
78
168
  })
79
169
  }
80
170
 
81
171
  protected abstract create(
82
172
  name: string,
83
- args: Unwrap<DnsRecordArgs>,
173
+ args: ResolvedDnsRecordArgs,
84
174
  opts?: ResourceOptions,
85
175
  ): Input<Resource>
86
176
 
@@ -99,26 +189,124 @@ export abstract class DnsRecord extends ComponentResource {
99
189
  return new implClass(name, args, opts)
100
190
  })
101
191
  }
192
+ }
102
193
 
103
- static createSet(
104
- name: string,
105
- args: DnsRecordSetArgs,
106
- opts?: ResourceOptions,
107
- ): Output<DnsRecord[]> {
108
- return output(args).apply(args => {
194
+ export class DnsRecordSet extends ComponentResource {
195
+ /**
196
+ * The underlying dns record resources.
197
+ */
198
+ public readonly dnsRecords: Output<DnsRecord[]>
199
+
200
+ /**
201
+ * The wait commands to be executed after the DNS records are created/updated.
202
+ */
203
+ public readonly waitCommands: Output<Command[]>
204
+
205
+ private constructor(name: string, records: Output<DnsRecord[]>, opts?: ResourceOptions) {
206
+ super("highstate:common:DnsRecordSet", name, records, opts)
207
+
208
+ this.dnsRecords = records
209
+
210
+ this.waitCommands = records.apply(records =>
211
+ records.flatMap(record => record.waitCommands),
212
+ ) as unknown as Output<Command[]>
213
+ }
214
+
215
+ static create(name: string, args: DnsRecordSetArgs, opts?: ResourceOptions): DnsRecordSet {
216
+ const records = output(args).apply(args => {
109
217
  const recordName = args.name ?? name
218
+ const values = normalize(args.value, args.values)
110
219
 
111
220
  return output(
112
221
  args.providers
113
222
  .filter(provider => recordName.endsWith(provider.domain))
114
- .map(provider =>
115
- DnsRecord.create(
116
- `${name}.${provider.type}`,
117
- { name: recordName, ...args, provider },
118
- opts,
119
- ),
120
- ),
223
+ .flatMap(provider => {
224
+ return values.map(value => {
225
+ const l3Endpoint = parseL3Endpoint(value)
226
+
227
+ return DnsRecord.create(
228
+ `${provider.type}-from-${provider.name}-to-${l3EndpointToString(l3Endpoint)}`,
229
+ { name: recordName, ...args, value: l3Endpoint, provider },
230
+ opts,
231
+ )
232
+ })
233
+ }),
121
234
  )
122
235
  })
236
+
237
+ return new DnsRecordSet(name, records, opts)
238
+ }
239
+ }
240
+
241
+ /**
242
+ * Registers the DNS record set for the given endpoints and prepends the corresponding hostname endpoint to the list.
243
+ *
244
+ * Waits for the DNS record set to be created/updated before continuing.
245
+ *
246
+ * Ignores the "hostname" endpoints in the list.
247
+ *
248
+ * @param endpoints The list of endpoints to register. Will be modified in place.
249
+ * @param fqdn The FQDN to register the DNS record set for. If not provided, no DNS record set will be created and array will not be modified.
250
+ * @param fqdnEndpointFilter The filter to apply to the endpoints before passing them to the DNS record set. Does not apply to the resulted endpoint list.
251
+ * @param dnsProviders The DNS providers to use to create the DNS records.
252
+ */
253
+ export async function updateEndpointsWithFqdn<TEndpoint extends network.L34Endpoint>(
254
+ endpoints: Input<TEndpoint[]>,
255
+ fqdn: string | undefined,
256
+ fqdnEndpointFilter: network.EndpointFilter,
257
+ patchMode: ArrayPatchMode,
258
+ dnsProviders: Input<dns.Provider[]>,
259
+ ): Promise<{ endpoints: TEndpoint[]; dnsRecordSet: DnsRecordSet | undefined }> {
260
+ const resolvedEndpoints = await toPromise(endpoints)
261
+
262
+ if (!fqdn) {
263
+ return {
264
+ endpoints: resolvedEndpoints as TEndpoint[],
265
+ dnsRecordSet: undefined,
266
+ }
267
+ }
268
+
269
+ const filteredEndpoints = filterEndpoints(resolvedEndpoints, fqdnEndpointFilter)
270
+
271
+ const dnsRecordSet = DnsRecordSet.create(fqdn, {
272
+ providers: dnsProviders,
273
+ values: filteredEndpoints,
274
+ waitAt: "local",
275
+ })
276
+
277
+ const portProtocolGroups = groupBy(filteredEndpoints, endpoint =>
278
+ endpoint.port ? `${endpoint.port}-${endpoint.protocol}` : "",
279
+ )
280
+
281
+ const newEndpoints: TEndpoint[] = []
282
+
283
+ for (const group of Object.values(portProtocolGroups)) {
284
+ newEndpoints.unshift({
285
+ type: "hostname",
286
+ hostname: fqdn,
287
+ visibility: group[0].visibility,
288
+ port: group[0].port,
289
+ protocol: group[0].protocol,
290
+ } as TEndpoint)
291
+ }
292
+
293
+ await toPromise(
294
+ dnsRecordSet.waitCommands.apply(waitCommands => waitCommands.map(command => command.stdout)),
295
+ )
296
+
297
+ if (patchMode === "prepend") {
298
+ return {
299
+ endpoints: uniqueBy(
300
+ //
301
+ [...newEndpoints, ...(resolvedEndpoints as TEndpoint[])],
302
+ endpoint => l34EndpointToString(endpoint),
303
+ ),
304
+ dnsRecordSet,
305
+ }
306
+ }
307
+
308
+ return {
309
+ endpoints: newEndpoints,
310
+ dnsRecordSet,
123
311
  }
124
312
  }
@@ -1,5 +1,5 @@
1
- export * from "./server"
1
+ export * from "./command"
2
2
  export * from "./dns"
3
3
  export * from "./passwords"
4
4
  export * from "./ssh"
5
- export * from "./utils"
5
+ export * from "./network"
@@ -0,0 +1,311 @@
1
+ import type { ArrayPatchMode, network } from "@highstate/library"
2
+ import { toPromise, type Input } from "@highstate/pulumi"
3
+ import { uniqueBy } from "remeda"
4
+
5
+ /**
6
+ * The L3 or L4 endpoint for some service.
7
+ *
8
+ * The format is: `[protocol://]endpoint[:port]`
9
+ */
10
+ export type InputL34Endpoint = network.L34Endpoint | string
11
+
12
+ /**
13
+ * The L3 endpoint for some service.
14
+ */
15
+ export type InputL3Endpoint = network.L3Endpoint | string
16
+
17
+ /**
18
+ * The L4 endpoint for some service.
19
+ */
20
+ export type InputL4Endpoint = network.L4Endpoint | string
21
+
22
+ /**
23
+ * Stringifies a L3 endpoint object into a string.
24
+ *
25
+ * @param l3Endpoint The L3 endpoint object to stringify.
26
+ * @returns The string representation of the L3 endpoint.
27
+ */
28
+ export function l3EndpointToString(l3Endpoint: network.L3Endpoint): string {
29
+ switch (l3Endpoint.type) {
30
+ case "ipv4":
31
+ return l3Endpoint.address
32
+ case "ipv6":
33
+ return l3Endpoint.address
34
+ case "hostname":
35
+ return l3Endpoint.hostname
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Stringifies a L4 endpoint object into a string.
41
+ *
42
+ * @param l4Endpoint The L4 endpoint object to stringify.
43
+ *
44
+ * @returns The string representation of the L4 endpoint.
45
+ */
46
+ export function l4EndpointToString(l4Endpoint: network.L4Endpoint): string {
47
+ if (l4Endpoint.type === "ipv6") {
48
+ return `[${l4Endpoint.address}]:${l4Endpoint.port}`
49
+ }
50
+
51
+ return `${l3EndpointToString(l4Endpoint)}:${l4Endpoint.port}`
52
+ }
53
+
54
+ /**
55
+ * Stringifies a L3 or L4 endpoint object into a string.
56
+ *
57
+ * @param l34Endpoint The L3 or L4 endpoint object to stringify.
58
+ * @returns The string representation of the L3 or L4 endpoint.
59
+ */
60
+ export function l34EndpointToString(l34Endpoint: network.L34Endpoint): string {
61
+ if (l34Endpoint.port) {
62
+ return l4EndpointToString(l34Endpoint)
63
+ }
64
+
65
+ return l3EndpointToString(l34Endpoint)
66
+ }
67
+
68
+ const L34_ENDPOINT_RE =
69
+ /^(?:(?<protocol>[a-z]+):\/\/)?(?:(?:\[?(?<ipv6>[0-9A-Fa-f:]+)\]?)|(?<ipv4>(?:\d{1,3}\.){3}\d{1,3})|(?<hostname>[a-zA-Z0-9-*]+(?:\.[a-zA-Z0-9-*]+)*))(?::(?<port>\d{1,5}))?$/
70
+
71
+ /**
72
+ * Parses a L3 or L4 endpoint from a string.
73
+ *
74
+ * The format is `[protocol://]endpoint[:port]`.
75
+ *
76
+ * @param l34Endpoint The L3 or L4 endpoint string to parse.
77
+ * @returns The parsed L3 or L4 endpoint object.
78
+ */
79
+ export function parseL34Endpoint(l34Endpoint: InputL34Endpoint): network.L34Endpoint {
80
+ if (typeof l34Endpoint === "object") {
81
+ return l34Endpoint
82
+ }
83
+
84
+ const match = l34Endpoint.match(L34_ENDPOINT_RE)
85
+ if (!match) {
86
+ throw new Error(`Invalid L3/L4 endpoint: "${l34Endpoint}"`)
87
+ }
88
+
89
+ const { protocol, ipv6, ipv4, hostname, port } = match.groups!
90
+
91
+ if (protocol && protocol !== "tcp" && protocol !== "udp") {
92
+ throw new Error(`Invalid L4 endpoint protocol: "${protocol}"`)
93
+ }
94
+
95
+ let visibility: network.EndpointVisibility = "public"
96
+
97
+ if (ipv4 && IPV4_PRIVATE_REGEX.test(ipv4)) {
98
+ visibility = "external"
99
+ } else if (ipv6 && IPV6_PRIVATE_REGEX.test(ipv6)) {
100
+ visibility = "external"
101
+ }
102
+
103
+ const fallbackProtocol = port ? "tcp" : undefined
104
+
105
+ return {
106
+ type: ipv6 ? "ipv6" : ipv4 ? "ipv4" : "hostname",
107
+ visibility,
108
+ address: ipv6 || ipv4,
109
+ hostname: hostname,
110
+ port: port ? parseInt(port, 10) : undefined,
111
+ protocol: protocol ? (protocol as network.L4Protocol) : fallbackProtocol,
112
+ } as network.L34Endpoint
113
+ }
114
+
115
+ /**
116
+ * Parses a L3 endpoint from a string.
117
+ *
118
+ * The same as `parseL34Endpoint`, but only for L3 endpoints and will throw an error if the endpoint contains a port.
119
+ *
120
+ * @param l3Endpoint The L3 endpoint string to parse.
121
+ * @returns The parsed L3 endpoint object.
122
+ */
123
+ export function parseL3Endpoint(l3Endpoint: InputL3Endpoint): network.L3Endpoint {
124
+ if (typeof l3Endpoint === "object") {
125
+ return l3Endpoint
126
+ }
127
+
128
+ const parsed = parseL34Endpoint(l3Endpoint)
129
+
130
+ if (parsed.port) {
131
+ throw new Error(`Port cannot be specified in L3 endpoint: "${l3Endpoint}"`)
132
+ }
133
+
134
+ return parsed
135
+ }
136
+
137
+ /**
138
+ * Parses a L4 endpoint from a string.
139
+ *
140
+ * The same as `parseL34Endpoint`, but only for L4 endpoints and will throw an error if the endpoint does not contain a port.
141
+ */
142
+ export function parseL4Endpoint(l4Endpoint: InputL4Endpoint): network.L4Endpoint {
143
+ if (typeof l4Endpoint === "object") {
144
+ return l4Endpoint
145
+ }
146
+
147
+ const parsed = parseL34Endpoint(l4Endpoint)
148
+
149
+ if (!parsed.port) {
150
+ throw new Error(`No port found in L4 endpoint: "${l4Endpoint}"`)
151
+ }
152
+
153
+ return parsed
154
+ }
155
+
156
+ const IPV4_PRIVATE_REGEX =
157
+ /^(?:10|127)(?:\.\d{1,3}){3}$|^(?:172\.1[6-9]|172\.2[0-9]|172\.3[0-1])(?:\.\d{1,3}){2}$|^(?:192\.168)(?:\.\d{1,3}){2}$/
158
+
159
+ const IPV6_PRIVATE_REGEX =
160
+ /^(?:fc|fd)(?:[0-9a-f]{2}){0,2}::(?:[0-9a-f]{1,4}:){7}[0-9a-f]{1,4}$|^::(?:ffff:(?:10|127)(?:\.\d{1,3}){3}|(?:172\.1[6-9]|172\.2[0-9]|172\.3[0-1])(?:\.\d{1,3}){2}|(?:192\.168)(?:\.\d{1,3}){2})$/
161
+
162
+ /**
163
+ * Helper function to get the input L3 endpoint from the raw endpoint or input endpoint.
164
+ *
165
+ * If neither is provided, an error is thrown.
166
+ *
167
+ * @param rawEndpoint The raw endpoint string to parse.
168
+ * @param inputEndpoint The input endpoint object to use if the raw endpoint is not provided.
169
+ * @returns The parsed L3 endpoint object.
170
+ */
171
+ export async function requireInputL3Endpoint(
172
+ rawEndpoint: string | undefined,
173
+ inputEndpoint: Input<network.L3Endpoint> | undefined,
174
+ ): Promise<network.L3Endpoint> {
175
+ if (rawEndpoint) {
176
+ return parseL3Endpoint(rawEndpoint)
177
+ }
178
+
179
+ if (inputEndpoint) {
180
+ return toPromise(inputEndpoint)
181
+ }
182
+
183
+ throw new Error("No endpoint provided")
184
+ }
185
+
186
+ /**
187
+ * Helper function to get the input L4 endpoint from the raw endpoint or input endpoint.
188
+ *
189
+ * If neither is provided, an error is thrown.
190
+ *
191
+ * @param rawEndpoint The raw endpoint string to parse.
192
+ * @param inputEndpoint The input endpoint object to use if the raw endpoint is not provided.
193
+ * @returns The parsed L4 endpoint object.
194
+ */
195
+ export async function requireInputL4Endpoint(
196
+ rawEndpoint: string | undefined,
197
+ inputEndpoint: Input<network.L4Endpoint> | undefined,
198
+ ): Promise<network.L4Endpoint> {
199
+ if (rawEndpoint) {
200
+ return parseL4Endpoint(rawEndpoint)
201
+ }
202
+
203
+ if (inputEndpoint) {
204
+ return toPromise(inputEndpoint)
205
+ }
206
+
207
+ throw new Error("No endpoint provided")
208
+ }
209
+
210
+ /**
211
+ * Convers L3 endpoint to L4 endpoint by adding a port and protocol.
212
+ *
213
+ * @param l3Endpoint The L3 endpoint to convert.
214
+ * @param port The port to add to the L3 endpoint.
215
+ * @param protocol The protocol to add to the L3 endpoint. Defaults to "tcp".
216
+ * @returns The L4 endpoint with the port and protocol added.
217
+ */
218
+ export function l3ToL4Endpoint(
219
+ l3Endpoint: InputL3Endpoint,
220
+ port: number,
221
+ protocol: network.L4Protocol = "tcp",
222
+ ): network.L4Endpoint {
223
+ return {
224
+ ...parseL3Endpoint(l3Endpoint),
225
+ port,
226
+ protocol,
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Filters the endpoints based on the given filter.
232
+ *
233
+ * @param endpoints The list of endpoints to filter.
234
+ * @param filter The filter to apply. If not provided, the endpoints will be filtered by the most accessible type: `public` > `external` > `internal`.
235
+ *
236
+ * @returns The filtered list of endpoints.
237
+ */
238
+ export function filterEndpoints<TEndpoint extends network.L34Endpoint>(
239
+ endpoints: TEndpoint[],
240
+ filter?: network.EndpointFilter,
241
+ ): TEndpoint[] {
242
+ if (filter?.length) {
243
+ return endpoints.filter(endpoint => filter.includes(endpoint.visibility))
244
+ }
245
+
246
+ if (endpoints.some(endpoint => endpoint.visibility === "public")) {
247
+ return endpoints.filter(endpoint => endpoint.visibility === "public")
248
+ }
249
+
250
+ if (endpoints.some(endpoint => endpoint.visibility === "external")) {
251
+ return endpoints.filter(endpoint => endpoint.visibility === "external")
252
+ }
253
+
254
+ // in this case all endpoints are already internal
255
+ return endpoints
256
+ }
257
+
258
+ /**
259
+ * Converts a L3 endpoint to CIDR notation.
260
+ *
261
+ * If the endpoint is a hostname, an error is thrown.
262
+ *
263
+ * @param l3Endpoint The L3 endpoint to convert.
264
+ * @returns The CIDR notation of the L3 endpoint.
265
+ */
266
+ export function l3EndpointToCidr(l3Endpoint: network.L3Endpoint): string {
267
+ switch (l3Endpoint.type) {
268
+ case "ipv4":
269
+ return `${l3Endpoint.address}/32`
270
+ case "ipv6":
271
+ return `${l3Endpoint.address}/128`
272
+ case "hostname":
273
+ throw new Error("Cannot convert hostname to CIDR")
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Updates the endpoints based on the given mode.
279
+ *
280
+ * @param currentEndpoints The current endpoints to update.
281
+ * @param endpoints The new endpoints to add in string format.
282
+ * @param inputEndpoints The input endpoints to add in object format.
283
+ * @param mode The mode to use when updating the endpoints. Can be "replace" or "prepend". Defaults to "prepend".
284
+ *
285
+ * @returns The updated list of endpoints.
286
+ */
287
+ export async function updateEndpoints<TEdnpoints extends network.L34Endpoint>(
288
+ currentEndpoints: Input<TEdnpoints[]>,
289
+ endpoints: string[],
290
+ inputEndpoints: Input<TEdnpoints[]>,
291
+ mode: ArrayPatchMode = "prepend",
292
+ ): Promise<TEdnpoints[]> {
293
+ const resolvedCurrentEndpoints = await toPromise(currentEndpoints)
294
+ const resolvedInputEndpoints = await toPromise(inputEndpoints)
295
+
296
+ const newEndpoints = uniqueBy(
297
+ //
298
+ [...endpoints.map(parseL34Endpoint), ...resolvedInputEndpoints],
299
+ endpoint => l34EndpointToString(endpoint),
300
+ )
301
+
302
+ if (mode === "replace") {
303
+ return newEndpoints as TEdnpoints[]
304
+ }
305
+
306
+ return uniqueBy(
307
+ //
308
+ [...newEndpoints, ...resolvedCurrentEndpoints],
309
+ endpoint => l34EndpointToString(endpoint),
310
+ ) as TEdnpoints[]
311
+ }