@highstate/common 0.15.0 → 0.16.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.
Files changed (91) hide show
  1. package/dist/{chunk-WFWXDYUX.js → chunk-X5BK6JSN.js} +877 -194
  2. package/dist/chunk-X5BK6JSN.js.map +1 -0
  3. package/dist/highstate.manifest.json +12 -2
  4. package/dist/index.js +1 -1
  5. package/dist/units/databases/etcd-patch/index.js +20 -0
  6. package/dist/units/databases/etcd-patch/index.js.map +1 -0
  7. package/dist/units/databases/existing-etcd/index.js +14 -0
  8. package/dist/units/databases/existing-etcd/index.js.map +1 -0
  9. package/dist/units/databases/existing-mariadb/index.js +2 -2
  10. package/dist/units/databases/existing-mariadb/index.js.map +1 -1
  11. package/dist/units/databases/existing-mongodb/index.js +2 -2
  12. package/dist/units/databases/existing-mongodb/index.js.map +1 -1
  13. package/dist/units/databases/existing-postgresql/index.js +2 -2
  14. package/dist/units/databases/existing-postgresql/index.js.map +1 -1
  15. package/dist/units/databases/existing-redis/index.js +2 -2
  16. package/dist/units/databases/existing-redis/index.js.map +1 -1
  17. package/dist/units/databases/existing-s3/index.js +18 -0
  18. package/dist/units/databases/existing-s3/index.js.map +1 -0
  19. package/dist/units/databases/mariadb-patch/index.js +24 -0
  20. package/dist/units/databases/mariadb-patch/index.js.map +1 -0
  21. package/dist/units/databases/mongodb-patch/index.js +24 -0
  22. package/dist/units/databases/mongodb-patch/index.js.map +1 -0
  23. package/dist/units/databases/postgresql-patch/index.js +24 -0
  24. package/dist/units/databases/postgresql-patch/index.js.map +1 -0
  25. package/dist/units/databases/redis-patch/index.js +27 -0
  26. package/dist/units/databases/redis-patch/index.js.map +1 -0
  27. package/dist/units/databases/s3-patch/index.js +25 -0
  28. package/dist/units/databases/s3-patch/index.js.map +1 -0
  29. package/dist/units/dns/record-set/index.js +14 -20
  30. package/dist/units/dns/record-set/index.js.map +1 -1
  31. package/dist/units/existing-server/index.js +3 -4
  32. package/dist/units/existing-server/index.js.map +1 -1
  33. package/dist/units/network/address-space/index.js +20 -0
  34. package/dist/units/network/address-space/index.js.map +1 -0
  35. package/dist/units/network/endpoint-filter/index.js +15 -0
  36. package/dist/units/network/endpoint-filter/index.js.map +1 -0
  37. package/dist/units/network/l3-endpoint/index.js +2 -2
  38. package/dist/units/network/l3-endpoint/index.js.map +1 -1
  39. package/dist/units/network/l4-endpoint/index.js +2 -2
  40. package/dist/units/network/l4-endpoint/index.js.map +1 -1
  41. package/dist/units/network/l7-endpoint/index.js +12 -0
  42. package/dist/units/network/l7-endpoint/index.js.map +1 -0
  43. package/dist/units/script/index.js +1 -1
  44. package/dist/units/server-patch/index.js +9 -12
  45. package/dist/units/server-patch/index.js.map +1 -1
  46. package/dist/units/ssh/key-pair/index.js +1 -1
  47. package/package.json +64 -10
  48. package/src/shared/command.ts +1 -1
  49. package/src/shared/dns.ts +11 -93
  50. package/src/shared/files.ts +3 -3
  51. package/src/shared/impl-ref.ts +4 -0
  52. package/src/shared/index.ts +2 -0
  53. package/src/shared/network/address-space.spec.ts +114 -0
  54. package/src/shared/network/address-space.ts +364 -0
  55. package/src/shared/network/address.spec.ts +109 -0
  56. package/src/shared/network/address.ts +119 -0
  57. package/src/shared/network/endpoints.spec.ts +249 -0
  58. package/src/shared/network/endpoints.ts +608 -0
  59. package/src/shared/network/index.ts +4 -0
  60. package/src/shared/network/ip.ts +236 -0
  61. package/src/shared/network/subnet.spec.ts +62 -0
  62. package/src/shared/network/subnet.ts +137 -0
  63. package/src/shared/ssh.ts +1 -1
  64. package/src/shared/tls.ts +21 -5
  65. package/src/shared/utils.ts +93 -0
  66. package/src/units/databases/etcd-patch/index.ts +23 -0
  67. package/src/units/databases/existing-etcd/index.ts +11 -0
  68. package/src/units/databases/existing-mariadb/index.ts +1 -1
  69. package/src/units/databases/existing-mongodb/index.ts +1 -1
  70. package/src/units/databases/existing-postgresql/index.ts +1 -1
  71. package/src/units/databases/existing-redis/index.ts +1 -1
  72. package/src/units/databases/existing-s3/index.ts +1 -1
  73. package/src/units/databases/mariadb-patch/index.ts +27 -0
  74. package/src/units/databases/mongodb-patch/index.ts +27 -0
  75. package/src/units/databases/postgresql-patch/index.ts +27 -0
  76. package/src/units/databases/redis-patch/index.ts +32 -0
  77. package/src/units/databases/s3-patch/index.ts +28 -0
  78. package/src/units/dns/record-set/index.ts +15 -20
  79. package/src/units/existing-server/index.ts +3 -4
  80. package/src/units/network/address-space/index.ts +20 -0
  81. package/src/units/network/endpoint-filter/index.ts +5 -5
  82. package/src/units/network/l3-endpoint/index.ts +2 -2
  83. package/src/units/network/l4-endpoint/index.ts +2 -2
  84. package/src/units/network/l7-endpoint/index.ts +2 -2
  85. package/src/units/remote-file/index.ts +12 -5
  86. package/src/units/server-patch/index.ts +10 -13
  87. package/dist/chunk-WFWXDYUX.js.map +0 -1
  88. package/dist/units/server-dns/index.js +0 -26
  89. package/dist/units/server-dns/index.js.map +0 -1
  90. package/src/shared/network.ts +0 -413
  91. package/src/units/server-dns/index.ts +0 -26
@@ -0,0 +1,608 @@
1
+ import { check } from "@highstate/contract"
2
+ import { type Metadata, metadataSchema, network } from "@highstate/library"
3
+ import { type InputArray, toPromise } from "@highstate/pulumi"
4
+ import { filter, isNonNullish, map, omit, pipe, uniqueBy } from "remeda"
5
+ import { doesAddressBelongToSubnet, parseAddress } from "./address"
6
+ import { privateSubnets } from "./subnet"
7
+
8
+ /**
9
+ * The input L3, L4, or L7 endpoint for some service.
10
+ *
11
+ * Can be provided as a string or an object.
12
+ */
13
+ export type InputEndpoint = network.L3Endpoint | network.Address | string
14
+
15
+ /**
16
+ * The input L4 or L7 endpoint for some service.
17
+ *
18
+ * Can be provided as a string or an object.
19
+ */
20
+ export type InputL4Endpoint = network.L4Endpoint | string
21
+
22
+ /**
23
+ * The input L7 endpoint for some service.
24
+ *
25
+ * Can be provided as a string or an object.
26
+ */
27
+ export type InputL7Endpoint = network.L7Endpoint | string
28
+
29
+ /**
30
+ * Stringifies a L3 endpoint object into a string.
31
+ *
32
+ * The result format is simply the address or hostname.
33
+ * The format does not depend on runtime level and will produce the same output for L3, L4, and L7 endpoints.
34
+ *
35
+ * @param l3Endpoint The L3 endpoint object to stringify.
36
+ * @returns The string representation of the L3 endpoint.
37
+ */
38
+ export function l3EndpointToString(l3Endpoint: network.L3Endpoint): string {
39
+ switch (l3Endpoint.type) {
40
+ case "ipv4":
41
+ return l3Endpoint.address.value
42
+ case "ipv6":
43
+ return l3Endpoint.address.value
44
+ case "hostname":
45
+ return l3Endpoint.hostname
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Stringifies a L4 endpoint object into a string.
51
+ *
52
+ * The result format is `endpoint:port` for IPv4 and hostname, and `[endpoint]:port` for IPv6.
53
+ *
54
+ * @param l4Endpoint The L4 endpoint object to stringify.
55
+ * @returns The string representation of the L4 endpoint.
56
+ */
57
+ export function l4EndpointToString(l4Endpoint: network.L3Endpoint & { port: number }): string {
58
+ const host = l3EndpointToString(l4Endpoint)
59
+ const wrappedHost = l4Endpoint.type === "ipv6" ? `[${host}]` : host
60
+
61
+ return `${wrappedHost}:${l4Endpoint.port}`
62
+ }
63
+
64
+ /**
65
+ * Stringifies a L4 endpoint object into a string with protocol.
66
+ *
67
+ * The result format is:
68
+ * - `protocol://[endpoint]:port` for IPv6;
69
+ * - `protocol://endpoint:port` for IPv4 and hostname.
70
+ *
71
+ * The format does not depend on runtime level and will produce the same output for L4 and L7 endpoints.
72
+ *
73
+ * @param l4Endpoint The L4 endpoint object to stringify.
74
+ * @returns The string representation of the L4 endpoint with protocol.
75
+ */
76
+ export function l4EndpointToFullString(l4Endpoint: network.L4Endpoint): string {
77
+ const protocol = `${l4Endpoint.protocol}://`
78
+
79
+ return `${protocol}${l4EndpointToString(l4Endpoint)}`
80
+ }
81
+
82
+ /**
83
+ * Stringifies a L7 endpoint object into a string.
84
+ *
85
+ * The result format is:
86
+ * - `appProtocol://[endpoint]:port[/path]` for IPv6;
87
+ * - `appProtocol://endpoint:port[/path]` for IPv4 and hostname.
88
+ *
89
+ * @param l7Endpoint The L7 endpoint object to stringify.
90
+ * @returns The string representation of the L7 endpoint.
91
+ */
92
+ export function l7EndpointToString(
93
+ l7Endpoint: network.L3Endpoint & {
94
+ level: 7
95
+ appProtocol: string
96
+ port: number
97
+ path?: string | undefined
98
+ },
99
+ ): string {
100
+ const protocol = `${l7Endpoint.appProtocol}://`
101
+
102
+ let endpoint = l4EndpointToString(l7Endpoint)
103
+
104
+ if (l7Endpoint.path) {
105
+ endpoint += `/${l7Endpoint.path}`
106
+ }
107
+
108
+ return `${protocol}${endpoint}`
109
+ }
110
+
111
+ /**
112
+ * Stringifies any L3, L4, or L7 endpoint object into a string.
113
+ * The result format depends on the endpoint level at runtime.
114
+ *
115
+ * @param endpoint The endpoint object to stringify.
116
+ * @returns The string representation of the endpoint.
117
+ */
118
+ export function endpointToString(endpoint: network.L3Endpoint): string {
119
+ switch (endpoint.level) {
120
+ case 3:
121
+ return l3EndpointToString(endpoint)
122
+ case 4:
123
+ return l4EndpointToString(endpoint)
124
+ case 7:
125
+ return l7EndpointToString(endpoint)
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Checks if the given endpoint meets the minimum level requirement.
131
+ *
132
+ * @param endpoint The endpoint to check.
133
+ * @param minLevel The minimum level of the endpoint to check.
134
+ * @returns True if the endpoint meets the minimum level requirement, false otherwise.
135
+ */
136
+ export function checkEndpointLevel<TMinLevel extends network.EndpointLevel>(
137
+ endpoint: network.L3Endpoint,
138
+ minLevel: TMinLevel,
139
+ ): endpoint is network.EndpointByMinLevel<TMinLevel> {
140
+ return endpoint.level >= minLevel
141
+ }
142
+
143
+ /**
144
+ * Asserts that the given endpoint meets the minimum level requirement.
145
+ *
146
+ * @param endpoint The endpoint to check.
147
+ * @param minLevel The minimum level of the endpoint to check.
148
+ * @throws If the endpoint does not meet the minimum level requirement.
149
+ */
150
+ export function assertEndpointLevel<TMinLevel extends network.EndpointLevel>(
151
+ endpoint: network.L3Endpoint,
152
+ minLevel: TMinLevel,
153
+ ): asserts endpoint is network.EndpointByMinLevel<TMinLevel> {
154
+ if (!checkEndpointLevel(endpoint, minLevel)) {
155
+ throw new Error(
156
+ `The endpoint "${endpointToString(endpoint)}" is L${endpoint.level}, but L${minLevel} is required`,
157
+ )
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Parses an endpoint from a string.
163
+ * If endpoint object is provided, it is returned as is.
164
+ *
165
+ * Supports L3, L4, and L7 endpoints.
166
+ *
167
+ * - L3 format: `endpoint`
168
+ * - L4 format: `[protocol://]endpoint[:port]`
169
+ * - L7 format: `appProtocol://endpoint[:port][/path]`
170
+ *
171
+ * @param endpoint The endpoint string or object to parse.
172
+ * @param minLevel The minimum level of the endpoint to parse. If provided, ensures the returned endpoint is at least this level.
173
+ * @returns The parsed endpoint object.
174
+ */
175
+ export function parseEndpoint<TMinLevel extends network.EndpointLevel = 3>(
176
+ endpoint: InputEndpoint,
177
+ minLevel: TMinLevel = 3 as TMinLevel,
178
+ ): network.EndpointByMinLevel<TMinLevel> {
179
+ type L3EndpointBase =
180
+ | {
181
+ type: "hostname"
182
+ level: 3
183
+ hostname: string
184
+ metadata: network.L3Endpoint["metadata"]
185
+ }
186
+ | {
187
+ type: "ipv4" | "ipv6"
188
+ level: 3
189
+ metadata: network.L3Endpoint["metadata"]
190
+ address: network.Address
191
+ subnet: network.Subnet
192
+ }
193
+
194
+ function validateEndpoint<TEndpoint extends network.L3Endpoint>(value: TEndpoint): TEndpoint {
195
+ const schema =
196
+ value.level === 7
197
+ ? network.l7EndpointEntity.schema
198
+ : value.level === 4
199
+ ? network.l4EndpointEntity.schema
200
+ : network.l3EndpointEntity.schema
201
+
202
+ const result = schema.safeParse(value)
203
+ if (!result.success) {
204
+ throw new Error(`Invalid endpoint "${endpointToString(value)}": ${result.error.message}`)
205
+ }
206
+
207
+ // Important: Zod strips unknown keys by default.
208
+ // We validate here, but return the original value to preserve fields
209
+ // that are not part of the schema (e.g., dynamic.endpoint mirroring).
210
+ return value
211
+ }
212
+
213
+ function parseHostToL3(host: string): L3EndpointBase {
214
+ const trimmed = host.trim()
215
+ if (!trimmed) {
216
+ throw new Error("Empty endpoint host")
217
+ }
218
+
219
+ try {
220
+ const address = parseAddress(trimmed)
221
+
222
+ return {
223
+ type: address.type,
224
+ level: 3,
225
+ metadata: extractMetadata(address),
226
+ address,
227
+ subnet: address.subnet,
228
+ }
229
+ } catch {
230
+ return {
231
+ type: "hostname",
232
+ level: 3,
233
+ hostname: trimmed,
234
+ metadata: {},
235
+ }
236
+ }
237
+ }
238
+
239
+ function splitHostPort(input: string): { host: string; port?: number } {
240
+ const trimmed = input.trim()
241
+
242
+ if (!trimmed) {
243
+ throw new Error("Empty endpoint")
244
+ }
245
+
246
+ if (trimmed.startsWith("[")) {
247
+ const closingIndex = trimmed.indexOf("]")
248
+ if (closingIndex === -1) {
249
+ throw new Error(`Invalid endpoint: "${input}"`)
250
+ }
251
+
252
+ const host = trimmed.slice(1, closingIndex)
253
+ const remainder = trimmed.slice(closingIndex + 1)
254
+
255
+ if (!remainder) {
256
+ return { host }
257
+ }
258
+
259
+ if (!remainder.startsWith(":")) {
260
+ throw new Error(`Invalid endpoint: "${input}"`)
261
+ }
262
+
263
+ const portString = remainder.slice(1)
264
+ if (!/^\d{1,5}$/.test(portString)) {
265
+ throw new Error(`Invalid endpoint port: "${portString}"`)
266
+ }
267
+
268
+ return { host, port: parseInt(portString, 10) }
269
+ }
270
+
271
+ const lastColonIndex = trimmed.lastIndexOf(":")
272
+ if (lastColonIndex === -1) {
273
+ return { host: trimmed }
274
+ }
275
+
276
+ // If it looks like an IPv6 address without brackets, treat it as host-only.
277
+ if (trimmed.includes(":")) {
278
+ const firstColonIndex = trimmed.indexOf(":")
279
+ if (firstColonIndex !== lastColonIndex) {
280
+ return { host: trimmed }
281
+ }
282
+ }
283
+
284
+ const host = trimmed.slice(0, lastColonIndex)
285
+ const portString = trimmed.slice(lastColonIndex + 1)
286
+ if (!/^\d{1,5}$/.test(portString)) {
287
+ return { host: trimmed }
288
+ }
289
+
290
+ return { host, port: parseInt(portString, 10) }
291
+ }
292
+
293
+ if (check(network.l3EndpointEntity.schema, endpoint)) {
294
+ assertEndpointLevel(endpoint, minLevel)
295
+
296
+ return endpoint as network.EndpointByMinLevel<TMinLevel>
297
+ }
298
+
299
+ if (check(network.addressEntity.schema, endpoint)) {
300
+ const address = endpoint
301
+ const built: network.L3Endpoint = {
302
+ type: address.type,
303
+ level: 3,
304
+ metadata: extractMetadata(address),
305
+ address,
306
+ subnet: address.subnet,
307
+ }
308
+
309
+ const withDynamic = syncDynamic(built)
310
+ const validated = validateEndpoint(withDynamic)
311
+
312
+ assertEndpointLevel(validated, minLevel)
313
+ return validated as network.EndpointByMinLevel<TMinLevel>
314
+ }
315
+
316
+ if (typeof endpoint !== "string") {
317
+ throw new Error("Invalid endpoint")
318
+ }
319
+
320
+ const endpointString = endpoint
321
+
322
+ let builtEndpoint: network.L3Endpoint
323
+
324
+ const schemeMatch = /^([a-z]+):\/\/(.*)$/.exec(endpointString)
325
+ if (schemeMatch) {
326
+ const appProtocol = schemeMatch[1]
327
+ const rest = schemeMatch[2]
328
+
329
+ const pathIndex = rest.indexOf("/")
330
+ const hostPortPart = pathIndex === -1 ? rest : rest.slice(0, pathIndex)
331
+ const path = pathIndex === -1 ? undefined : rest.slice(pathIndex + 1)
332
+
333
+ const udpAppProtocols = ["dns", "dhcp"]
334
+ const { host, port } = splitHostPort(hostPortPart)
335
+ const l3Base = parseHostToL3(host)
336
+
337
+ const portNumber = port ?? 443
338
+ const protocol: network.L4Protocol = udpAppProtocols.includes(appProtocol) ? "udp" : "tcp"
339
+
340
+ builtEndpoint =
341
+ l3Base.type === "hostname"
342
+ ? {
343
+ ...l3Base,
344
+ level: 7,
345
+ port: portNumber,
346
+ protocol,
347
+ appProtocol,
348
+ path: path || undefined,
349
+ }
350
+ : {
351
+ ...l3Base,
352
+ level: 7,
353
+ port: portNumber,
354
+ protocol,
355
+ appProtocol,
356
+ path: path || undefined,
357
+ }
358
+ } else {
359
+ const { host, port } = splitHostPort(endpointString)
360
+ const l3Base = parseHostToL3(host)
361
+
362
+ if (port !== undefined) {
363
+ builtEndpoint =
364
+ l3Base.type === "hostname"
365
+ ? {
366
+ ...l3Base,
367
+ level: 4,
368
+ port,
369
+ protocol: "tcp",
370
+ }
371
+ : {
372
+ ...l3Base,
373
+ level: 4,
374
+ port,
375
+ protocol: "tcp",
376
+ }
377
+ } else {
378
+ builtEndpoint = l3Base
379
+ }
380
+ }
381
+
382
+ const withDynamic = syncDynamic(builtEndpoint)
383
+ const validated = validateEndpoint(withDynamic)
384
+
385
+ assertEndpointLevel(validated, minLevel)
386
+ return validated as network.EndpointByMinLevel<TMinLevel>
387
+ }
388
+
389
+ /**
390
+ * Parses L4 protocol from string.
391
+ *
392
+ * @param input The input string to parse.
393
+ * @returns The parsed L4 protocol.
394
+ */
395
+ export function parseL4Protocol(input: string): network.L4Protocol {
396
+ input = input.trim().toLowerCase()
397
+
398
+ if (input === "tcp" || input === "udp") {
399
+ return input
400
+ }
401
+
402
+ throw new Error(`Invalid L4 protocol: "${input}"`)
403
+ }
404
+
405
+ /**
406
+ * Converts L3 endpoint to L4 endpoint by adding a port and protocol.
407
+ *
408
+ * @param l3Endpoint The L3 endpoint to convert.
409
+ * @param port The port to add to the L3 endpoint.
410
+ * @param protocol The protocol to add to the L3 endpoint. Defaults to "tcp".
411
+ * @returns The L4 endpoint with the port and protocol added.
412
+ */
413
+ export function l3EndpointToL4(
414
+ l3Endpoint: InputEndpoint,
415
+ port: number,
416
+ protocol: network.L4Protocol = "tcp",
417
+ ): network.L4Endpoint {
418
+ const parsed = parseEndpoint(l3Endpoint)
419
+
420
+ return {
421
+ ...parsed,
422
+ level: 4,
423
+ port,
424
+ protocol,
425
+ } as network.L4Endpoint
426
+ }
427
+
428
+ /**
429
+ * Converts L4 endpoint to L7 endpoint by adding application protocol and path.
430
+ *
431
+ * @param l4Endpoint The L4 endpoint to convert.
432
+ * @param appProtocol The application protocol to add to the L4 endpoint.
433
+ * @param path The path to add to the L4 endpoint. Defaults to an empty string.
434
+ * @returns The L7 endpoint with the application protocol and path added.
435
+ */
436
+ export function l4EndpointToL7(
437
+ l4Endpoint: InputEndpoint,
438
+ appProtocol: string,
439
+ path: string = "",
440
+ ): network.L7Endpoint {
441
+ const parsed = parseEndpoint(l4Endpoint, 4)
442
+
443
+ return {
444
+ ...parsed,
445
+ level: 7,
446
+ appProtocol,
447
+ path,
448
+ }
449
+ }
450
+
451
+ /**
452
+ * Converts a L3 endpoint to CIDR notation.
453
+ *
454
+ * If the endpoint is a hostname, an error is thrown.
455
+ *
456
+ * @param endpoint The L3 endpoint to convert.
457
+ * @returns The CIDR notation of the L3 endpoint.
458
+ */
459
+ export function l3EndpointToCidr(endpoint: network.L3Endpoint): string {
460
+ switch (endpoint.type) {
461
+ case "ipv4":
462
+ return `${endpoint.address.value}/32`
463
+ case "ipv6":
464
+ return `${endpoint.address.value}/128`
465
+ case "hostname":
466
+ throw new Error("Cannot convert hostname to CIDR")
467
+ }
468
+ }
469
+
470
+ function extractMetadata(address?: network.Address): network.L3Endpoint["metadata"] {
471
+ if (!address) {
472
+ return {}
473
+ }
474
+
475
+ const metadata: network.L3Endpoint["metadata"] = {}
476
+
477
+ if (privateSubnets.some(subnet => doesAddressBelongToSubnet(address, subnet))) {
478
+ metadata["iana.scope"] = "private"
479
+ } else {
480
+ metadata["iana.scope"] = "global"
481
+ }
482
+
483
+ return metadata
484
+ }
485
+
486
+ /**
487
+ * Parses multiple endpoints from strings and input objects.
488
+ *
489
+ * @param endpoints The endpoint strings to parse.
490
+ * @param inputEndpoints The input endpoint objects to use.
491
+ * @returns The parsed list of endpoint objects with duplicates removed.
492
+ */
493
+ export async function parseEndpoints<TMinLevel extends network.EndpointLevel = 3>(
494
+ endpoints: (string | undefined | null)[] | null | undefined,
495
+ inputEndpoints: InputArray<network.L3Endpoint | undefined | null> | null | undefined,
496
+ minLevel: TMinLevel = 3 as TMinLevel,
497
+ ): Promise<network.EndpointByMinLevel<TMinLevel>[]> {
498
+ const resolvedInputEndpoints = await toPromise(inputEndpoints ?? [])
499
+
500
+ return pipe(
501
+ [...(endpoints ?? []), ...resolvedInputEndpoints],
502
+ filter(isNonNullish),
503
+ map(endpoint => parseEndpoint(endpoint, minLevel)),
504
+ uniqueBy(endpointToString),
505
+ )
506
+ }
507
+
508
+ /**
509
+ * Adds the given metadata to the endpoint.
510
+ *
511
+ * @param endpoint The endpoint to add metadataa to.
512
+ * @param newMetadata The metadata to add to the endpoint.
513
+ * @returns The endpoint with the added metadata.
514
+ */
515
+ export function addEndpointMetadata<
516
+ TEndpoint extends network.L3Endpoint,
517
+ TMetadata extends Metadata,
518
+ >(endpoint: TEndpoint, newMetadata: TMetadata): TEndpoint & { metadata: TMetadata } {
519
+ const parsedMetadata = metadataSchema.safeParse(newMetadata)
520
+ if (!parsedMetadata.success) {
521
+ throw new Error(
522
+ `Invalid new metadata for endpoint "${endpointToString(endpoint)}": ${parsedMetadata.error.message}`,
523
+ )
524
+ }
525
+
526
+ return syncDynamic({
527
+ ...endpoint,
528
+ metadata: {
529
+ ...endpoint.metadata,
530
+ ...newMetadata,
531
+ },
532
+ })
533
+ }
534
+
535
+ /**
536
+ * Merges duplicate endpoints by combining their metadata.
537
+ *
538
+ * @param endpoints The list of endpoints to merge.
539
+ * @returns The merged list of endpoints with duplicates removed and metadata combined.
540
+ */
541
+ export function mergeEndpoints<TEndpoint extends network.L3Endpoint>(
542
+ endpoints: TEndpoint[],
543
+ ): TEndpoint[] {
544
+ const mergedMap = new Map<string, TEndpoint>()
545
+
546
+ for (const endpoint of endpoints) {
547
+ const key = endpointToString(endpoint)
548
+ const existing = mergedMap.get(key)
549
+
550
+ if (existing) {
551
+ mergedMap.set(key, addEndpointMetadata(existing, endpoint.metadata ?? {}))
552
+ } else {
553
+ mergedMap.set(key, endpoint)
554
+ }
555
+ }
556
+
557
+ return Array.from(mergedMap.values())
558
+ }
559
+
560
+ function syncDynamic<TEndpoint extends network.L3Endpoint>(endpoint: TEndpoint): TEndpoint {
561
+ return {
562
+ ...endpoint,
563
+ dynamic: {
564
+ type: "static",
565
+ endpoint: omit(endpoint, ["dynamic"]),
566
+ },
567
+ }
568
+ }
569
+
570
+ /**
571
+ * Replaces the base (host) of an endpoint with another endpoint's base.
572
+ *
573
+ * This function can be used to "re-host" an endpoint while keeping its other properties.
574
+ * For example, you can take an existing L7 endpoint and replace its host with a DNS record name.
575
+ *
576
+ * If the base is a hostname, the result becomes a hostname endpoint.
577
+ * If the base is an IP address, the result becomes an IP endpoint of the base's version.
578
+ *
579
+ * The base properties are:
580
+ * - For hostname endpoints: `type: "hostname"` and `hostname`
581
+ * - For IP address endpoints: `type`, `address`, `subnet`, and `metadata`
582
+ *
583
+ * Note: This intentionally may change the endpoint kind (hostname ↔ ip) and IP version (ipv4 ↔ ipv6).
584
+ *
585
+ * @param endpoint The endpoint to replace the base properties of.
586
+ * @param base The endpoint to take the base properties from.
587
+ * @returns The endpoint with the replaced base properties.
588
+ */
589
+ export function replaceEndpointBase<TEndpoint extends network.L3Endpoint>(
590
+ endpoint: TEndpoint,
591
+ base: network.L3Endpoint,
592
+ ): TEndpoint {
593
+ if (base.type === "hostname") {
594
+ return syncDynamic({
595
+ ...omit(endpoint, ["type", "address", "subnet"]),
596
+ type: "hostname",
597
+ hostname: base.hostname,
598
+ } as TEndpoint)
599
+ }
600
+
601
+ return syncDynamic({
602
+ ...omit(endpoint, ["type", "hostname", "metadata"]),
603
+ type: base.type,
604
+ address: base.address,
605
+ subnet: base.subnet,
606
+ metadata: base.metadata ?? extractMetadata(base.address),
607
+ } as TEndpoint)
608
+ }
@@ -0,0 +1,4 @@
1
+ export * from "./address"
2
+ export * from "./address-space"
3
+ export * from "./endpoints"
4
+ export * from "./subnet"