@crossdelta/infrastructure 0.1.35

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/dist/index.cjs ADDED
@@ -0,0 +1,260 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
6
+ var __toCommonJS = (from) => {
7
+ var entry = __moduleCache.get(from), desc;
8
+ if (entry)
9
+ return entry;
10
+ entry = __defProp({}, "__esModule", { value: true });
11
+ if (from && typeof from === "object" || typeof from === "function")
12
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
13
+ get: () => from[key],
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ }));
16
+ __moduleCache.set(from, entry);
17
+ return entry;
18
+ };
19
+ var __export = (target, all) => {
20
+ for (var name in all)
21
+ __defProp(target, name, {
22
+ get: all[name],
23
+ enumerable: true,
24
+ configurable: true,
25
+ set: (newValue) => all[name] = () => newValue
26
+ });
27
+ };
28
+
29
+ // lib/index.ts
30
+ var exports_lib = {};
31
+ __export(exports_lib, {
32
+ getServiceUrl: () => getServiceUrl,
33
+ getServicePort: () => getServicePort2,
34
+ getImage: () => getImage,
35
+ ensureDot: () => ensureDot,
36
+ dockerHubImage: () => dockerHubImage,
37
+ discoverServices: () => discoverServices,
38
+ defaultHealthCheck: () => defaultHealthCheck,
39
+ defaultAlerts: () => defaultAlerts,
40
+ createLogDestinations: () => createLogDestinations,
41
+ buildServices: () => buildServices,
42
+ buildServiceUrlEnvs: () => buildServiceUrlEnvs,
43
+ buildServicePortEnvs: () => buildServicePortEnvs,
44
+ buildLocalUrls: () => buildLocalUrls,
45
+ buildInternalUrls: () => buildInternalUrls,
46
+ buildIngressRules: () => buildIngressRules,
47
+ buildExternalUrls: () => buildExternalUrls,
48
+ RegistryType: () => RegistryType
49
+ });
50
+ module.exports = __toCommonJS(exports_lib);
51
+
52
+ // lib/helpers/config.ts
53
+ var ensureDot = (str) => {
54
+ return str.endsWith(".") ? str : `${str}.`;
55
+ };
56
+ var defaultAlerts = [
57
+ {
58
+ rule: "CPU_UTILIZATION",
59
+ operator: "GREATER_THAN",
60
+ value: 70,
61
+ window: "FIVE_MINUTES"
62
+ },
63
+ {
64
+ rule: "MEM_UTILIZATION",
65
+ operator: "GREATER_THAN",
66
+ value: 70,
67
+ window: "FIVE_MINUTES"
68
+ }
69
+ ];
70
+ var createLogDestinations = (logtailToken) => [
71
+ {
72
+ name: "better-stacks-logs",
73
+ logtail: {
74
+ token: logtailToken
75
+ }
76
+ }
77
+ ];
78
+ var defaultHealthCheck = {
79
+ httpPath: "/health"
80
+ };
81
+ // lib/helpers/discover-services.ts
82
+ var import_node_fs = require("node:fs");
83
+ var import_node_path = require("node:path");
84
+ function getServicePort(config) {
85
+ if (config.httpPort)
86
+ return config.httpPort;
87
+ const internalPorts = config.internalPorts;
88
+ if (internalPorts?.[0])
89
+ return internalPorts[0];
90
+ return 8080;
91
+ }
92
+ function validateNoDuplicatePorts(configs) {
93
+ const portMap = new Map;
94
+ for (const config of configs) {
95
+ const port = getServicePort(config);
96
+ const existing = portMap.get(port) || [];
97
+ existing.push(config.name);
98
+ portMap.set(port, existing);
99
+ }
100
+ const conflicts = [...portMap.entries()].filter(([, services]) => services.length > 1).map(([port, services]) => `Port ${port}: ${services.join(", ")}`);
101
+ if (conflicts.length > 0) {
102
+ throw new Error(`Port conflicts detected:
103
+ ${conflicts.join(`
104
+ `)}`);
105
+ }
106
+ }
107
+ function discoverServices(servicesDir) {
108
+ const files = import_node_fs.readdirSync(servicesDir).filter((file) => file.endsWith(".ts") && file !== "index.ts");
109
+ const configs = files.map((file) => {
110
+ const module2 = require(import_node_path.join(servicesDir, file));
111
+ return module2.default;
112
+ });
113
+ validateNoDuplicatePorts(configs);
114
+ return configs;
115
+ }
116
+ // lib/types/index.ts
117
+ var RegistryType = {
118
+ DOCKER_HUB: "DOCKER_HUB",
119
+ GHCR: "GHCR",
120
+ DOCR: "DOCR"
121
+ };
122
+
123
+ // lib/helpers/docker-hub-image.ts
124
+ var dockerHubImage = (repository, tag, registry = "library") => ({
125
+ registryType: RegistryType.DOCKER_HUB,
126
+ registry,
127
+ repository,
128
+ tag
129
+ });
130
+ // lib/helpers/image.ts
131
+ var scopeImageTagsRaw = process.env.SCOPE_IMAGE_TAGS ?? "";
132
+ var scopeImageTags = (() => {
133
+ if (!scopeImageTagsRaw.trim()) {
134
+ return {};
135
+ }
136
+ try {
137
+ const parsed = JSON.parse(scopeImageTagsRaw);
138
+ const tags = {};
139
+ for (const [key, value] of Object.entries(parsed ?? {})) {
140
+ if (typeof value === "string" && value.trim().length > 0) {
141
+ tags[key] = value.trim();
142
+ }
143
+ }
144
+ return tags;
145
+ } catch (error) {
146
+ console.warn("Unable to parse scope image tags from environment:", error);
147
+ return {};
148
+ }
149
+ })();
150
+ var resolveImageTag = (scopeName) => {
151
+ const tag = scopeImageTags[scopeName];
152
+ if (!tag) {
153
+ console.warn(`No image tag for "${scopeName}", using "latest" as fallback`);
154
+ return "latest";
155
+ }
156
+ return tag;
157
+ };
158
+ var getImage = (repository, registryCredentials) => {
159
+ const scopeName = repository.split("/").pop();
160
+ if (!scopeName) {
161
+ throw new Error(`Invalid repository name: ${repository}`);
162
+ }
163
+ return {
164
+ registryType: "GHCR",
165
+ registry: "orderboss",
166
+ repository,
167
+ registryCredentials,
168
+ tag: resolveImageTag(scopeName)
169
+ };
170
+ };
171
+ // lib/helpers/service-builder.ts
172
+ function buildServices(options) {
173
+ const { serviceConfigs, registryCredentials, logtailToken } = options;
174
+ const logDestinations = createLogDestinations(logtailToken);
175
+ return serviceConfigs.map((config) => {
176
+ const {
177
+ ingressPrefix: _ingressPrefix,
178
+ skip: _skip,
179
+ image: configImage,
180
+ internalUrl: _internalUrl,
181
+ ...serviceConfig
182
+ } = config;
183
+ const image = configImage ?? getImage(`platform/${config.name}`, registryCredentials);
184
+ return {
185
+ healthCheck: defaultHealthCheck,
186
+ alerts: defaultAlerts,
187
+ logDestinations,
188
+ ...serviceConfig,
189
+ image
190
+ };
191
+ });
192
+ }
193
+ function buildIngressRules(serviceConfigs) {
194
+ return serviceConfigs.filter((config) => config.ingressPrefix !== undefined && config.httpPort !== undefined).map((config) => ({
195
+ component: { name: config.name },
196
+ match: {
197
+ path: {
198
+ prefix: config.ingressPrefix
199
+ }
200
+ }
201
+ }));
202
+ }
203
+ // lib/helpers/service-runtime.ts
204
+ var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
205
+ function getServicePort2(serviceName, defaultPort = 8080) {
206
+ const envKey = `${toEnvKey(serviceName)}_PORT`;
207
+ const envValue = process.env[envKey];
208
+ if (envValue) {
209
+ const parsed = Number(envValue);
210
+ if (!Number.isNaN(parsed)) {
211
+ return parsed;
212
+ }
213
+ }
214
+ return defaultPort;
215
+ }
216
+ function getServiceUrl(serviceName) {
217
+ const envKey = `${toEnvKey(serviceName)}_URL`;
218
+ return process.env[envKey];
219
+ }
220
+ // lib/helpers/service-urls.ts
221
+ function getServicePort3(config) {
222
+ if (config.httpPort)
223
+ return config.httpPort;
224
+ const internalPorts = config.internalPorts;
225
+ if (internalPorts?.[0])
226
+ return internalPorts[0];
227
+ return 8080;
228
+ }
229
+ function buildInternalUrls(serviceConfigs) {
230
+ return Object.fromEntries(serviceConfigs.map((config) => {
231
+ const url = config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`;
232
+ return [config.name, url];
233
+ }));
234
+ }
235
+ function buildExternalUrls(serviceConfigs, baseUrl) {
236
+ return Object.fromEntries(serviceConfigs.filter((config) => config.ingressPrefix !== undefined).map((config) => {
237
+ const path = config.ingressPrefix === "/" ? "" : config.ingressPrefix;
238
+ return [config.name, `${baseUrl}${path}`];
239
+ }));
240
+ }
241
+ function buildLocalUrls(serviceConfigs) {
242
+ return Object.fromEntries(serviceConfigs.map((config) => {
243
+ const port = getServicePort3(config);
244
+ return [config.name, `http://localhost:${port}`];
245
+ }));
246
+ }
247
+ function buildServiceUrlEnvs(serviceConfigs) {
248
+ return serviceConfigs.map((config) => ({
249
+ key: `${config.name.toUpperCase().replace(/-/g, "_")}_URL`,
250
+ scope: "RUN_TIME",
251
+ value: config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`
252
+ }));
253
+ }
254
+ function buildServicePortEnvs(serviceConfigs) {
255
+ return serviceConfigs.map((config) => ({
256
+ key: `${config.name.toUpperCase().replace(/-/g, "_")}_PORT`,
257
+ scope: "RUN_TIME",
258
+ value: String(getServicePort3(config))
259
+ }));
260
+ }
@@ -0,0 +1,2 @@
1
+ export * from './helpers';
2
+ export * from './types';
package/dist/index.js ADDED
@@ -0,0 +1,231 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // lib/helpers/config.ts
5
+ var ensureDot = (str) => {
6
+ return str.endsWith(".") ? str : `${str}.`;
7
+ };
8
+ var defaultAlerts = [
9
+ {
10
+ rule: "CPU_UTILIZATION",
11
+ operator: "GREATER_THAN",
12
+ value: 70,
13
+ window: "FIVE_MINUTES"
14
+ },
15
+ {
16
+ rule: "MEM_UTILIZATION",
17
+ operator: "GREATER_THAN",
18
+ value: 70,
19
+ window: "FIVE_MINUTES"
20
+ }
21
+ ];
22
+ var createLogDestinations = (logtailToken) => [
23
+ {
24
+ name: "better-stacks-logs",
25
+ logtail: {
26
+ token: logtailToken
27
+ }
28
+ }
29
+ ];
30
+ var defaultHealthCheck = {
31
+ httpPath: "/health"
32
+ };
33
+ // lib/helpers/discover-services.ts
34
+ import { readdirSync } from "node:fs";
35
+ import { join } from "node:path";
36
+ function getServicePort(config) {
37
+ if (config.httpPort)
38
+ return config.httpPort;
39
+ const internalPorts = config.internalPorts;
40
+ if (internalPorts?.[0])
41
+ return internalPorts[0];
42
+ return 8080;
43
+ }
44
+ function validateNoDuplicatePorts(configs) {
45
+ const portMap = new Map;
46
+ for (const config of configs) {
47
+ const port = getServicePort(config);
48
+ const existing = portMap.get(port) || [];
49
+ existing.push(config.name);
50
+ portMap.set(port, existing);
51
+ }
52
+ const conflicts = [...portMap.entries()].filter(([, services]) => services.length > 1).map(([port, services]) => `Port ${port}: ${services.join(", ")}`);
53
+ if (conflicts.length > 0) {
54
+ throw new Error(`Port conflicts detected:
55
+ ${conflicts.join(`
56
+ `)}`);
57
+ }
58
+ }
59
+ function discoverServices(servicesDir) {
60
+ const files = readdirSync(servicesDir).filter((file) => file.endsWith(".ts") && file !== "index.ts");
61
+ const configs = files.map((file) => {
62
+ const module = __require(join(servicesDir, file));
63
+ return module.default;
64
+ });
65
+ validateNoDuplicatePorts(configs);
66
+ return configs;
67
+ }
68
+ // lib/types/index.ts
69
+ var RegistryType = {
70
+ DOCKER_HUB: "DOCKER_HUB",
71
+ GHCR: "GHCR",
72
+ DOCR: "DOCR"
73
+ };
74
+
75
+ // lib/helpers/docker-hub-image.ts
76
+ var dockerHubImage = (repository, tag, registry = "library") => ({
77
+ registryType: RegistryType.DOCKER_HUB,
78
+ registry,
79
+ repository,
80
+ tag
81
+ });
82
+ // lib/helpers/image.ts
83
+ var scopeImageTagsRaw = process.env.SCOPE_IMAGE_TAGS ?? "";
84
+ var scopeImageTags = (() => {
85
+ if (!scopeImageTagsRaw.trim()) {
86
+ return {};
87
+ }
88
+ try {
89
+ const parsed = JSON.parse(scopeImageTagsRaw);
90
+ const tags = {};
91
+ for (const [key, value] of Object.entries(parsed ?? {})) {
92
+ if (typeof value === "string" && value.trim().length > 0) {
93
+ tags[key] = value.trim();
94
+ }
95
+ }
96
+ return tags;
97
+ } catch (error) {
98
+ console.warn("Unable to parse scope image tags from environment:", error);
99
+ return {};
100
+ }
101
+ })();
102
+ var resolveImageTag = (scopeName) => {
103
+ const tag = scopeImageTags[scopeName];
104
+ if (!tag) {
105
+ console.warn(`No image tag for "${scopeName}", using "latest" as fallback`);
106
+ return "latest";
107
+ }
108
+ return tag;
109
+ };
110
+ var getImage = (repository, registryCredentials) => {
111
+ const scopeName = repository.split("/").pop();
112
+ if (!scopeName) {
113
+ throw new Error(`Invalid repository name: ${repository}`);
114
+ }
115
+ return {
116
+ registryType: "GHCR",
117
+ registry: "orderboss",
118
+ repository,
119
+ registryCredentials,
120
+ tag: resolveImageTag(scopeName)
121
+ };
122
+ };
123
+ // lib/helpers/service-builder.ts
124
+ function buildServices(options) {
125
+ const { serviceConfigs, registryCredentials, logtailToken } = options;
126
+ const logDestinations = createLogDestinations(logtailToken);
127
+ return serviceConfigs.map((config) => {
128
+ const {
129
+ ingressPrefix: _ingressPrefix,
130
+ skip: _skip,
131
+ image: configImage,
132
+ internalUrl: _internalUrl,
133
+ ...serviceConfig
134
+ } = config;
135
+ const image = configImage ?? getImage(`platform/${config.name}`, registryCredentials);
136
+ return {
137
+ healthCheck: defaultHealthCheck,
138
+ alerts: defaultAlerts,
139
+ logDestinations,
140
+ ...serviceConfig,
141
+ image
142
+ };
143
+ });
144
+ }
145
+ function buildIngressRules(serviceConfigs) {
146
+ return serviceConfigs.filter((config) => config.ingressPrefix !== undefined && config.httpPort !== undefined).map((config) => ({
147
+ component: { name: config.name },
148
+ match: {
149
+ path: {
150
+ prefix: config.ingressPrefix
151
+ }
152
+ }
153
+ }));
154
+ }
155
+ // lib/helpers/service-runtime.ts
156
+ var toEnvKey = (name) => name.toUpperCase().replace(/-/g, "_");
157
+ function getServicePort2(serviceName, defaultPort = 8080) {
158
+ const envKey = `${toEnvKey(serviceName)}_PORT`;
159
+ const envValue = process.env[envKey];
160
+ if (envValue) {
161
+ const parsed = Number(envValue);
162
+ if (!Number.isNaN(parsed)) {
163
+ return parsed;
164
+ }
165
+ }
166
+ return defaultPort;
167
+ }
168
+ function getServiceUrl(serviceName) {
169
+ const envKey = `${toEnvKey(serviceName)}_URL`;
170
+ return process.env[envKey];
171
+ }
172
+ // lib/helpers/service-urls.ts
173
+ function getServicePort3(config) {
174
+ if (config.httpPort)
175
+ return config.httpPort;
176
+ const internalPorts = config.internalPorts;
177
+ if (internalPorts?.[0])
178
+ return internalPorts[0];
179
+ return 8080;
180
+ }
181
+ function buildInternalUrls(serviceConfigs) {
182
+ return Object.fromEntries(serviceConfigs.map((config) => {
183
+ const url = config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`;
184
+ return [config.name, url];
185
+ }));
186
+ }
187
+ function buildExternalUrls(serviceConfigs, baseUrl) {
188
+ return Object.fromEntries(serviceConfigs.filter((config) => config.ingressPrefix !== undefined).map((config) => {
189
+ const path = config.ingressPrefix === "/" ? "" : config.ingressPrefix;
190
+ return [config.name, `${baseUrl}${path}`];
191
+ }));
192
+ }
193
+ function buildLocalUrls(serviceConfigs) {
194
+ return Object.fromEntries(serviceConfigs.map((config) => {
195
+ const port = getServicePort3(config);
196
+ return [config.name, `http://localhost:${port}`];
197
+ }));
198
+ }
199
+ function buildServiceUrlEnvs(serviceConfigs) {
200
+ return serviceConfigs.map((config) => ({
201
+ key: `${config.name.toUpperCase().replace(/-/g, "_")}_URL`,
202
+ scope: "RUN_TIME",
203
+ value: config.internalUrl ?? `http://${config.name}:${getServicePort3(config)}`
204
+ }));
205
+ }
206
+ function buildServicePortEnvs(serviceConfigs) {
207
+ return serviceConfigs.map((config) => ({
208
+ key: `${config.name.toUpperCase().replace(/-/g, "_")}_PORT`,
209
+ scope: "RUN_TIME",
210
+ value: String(getServicePort3(config))
211
+ }));
212
+ }
213
+ export {
214
+ getServiceUrl,
215
+ getServicePort2 as getServicePort,
216
+ getImage,
217
+ ensureDot,
218
+ dockerHubImage,
219
+ discoverServices,
220
+ defaultHealthCheck,
221
+ defaultAlerts,
222
+ createLogDestinations,
223
+ buildServices,
224
+ buildServiceUrlEnvs,
225
+ buildServicePortEnvs,
226
+ buildLocalUrls,
227
+ buildInternalUrls,
228
+ buildIngressRules,
229
+ buildExternalUrls,
230
+ RegistryType
231
+ };
@@ -0,0 +1,81 @@
1
+ import type { AppSpecService } from '@pulumi/digitalocean/types/input';
2
+ /**
3
+ * Supported container registry types for DigitalOcean App Platform.
4
+ */
5
+ export declare const RegistryType: {
6
+ /** Docker Hub - public or private images */
7
+ readonly DOCKER_HUB: "DOCKER_HUB";
8
+ /** GitHub Container Registry */
9
+ readonly GHCR: "GHCR";
10
+ /** DigitalOcean Container Registry */
11
+ readonly DOCR: "DOCR";
12
+ };
13
+ export type RegistryType = (typeof RegistryType)[keyof typeof RegistryType];
14
+ /**
15
+ * Image configuration for a service.
16
+ */
17
+ export interface ImageConfig {
18
+ /** Registry type */
19
+ registryType: RegistryType;
20
+ /** Registry name (e.g., 'library' for official Docker Hub images, 'orderboss' for GHCR) */
21
+ registry: string;
22
+ /** Repository name (e.g., 'nats', 'platform/storefront') */
23
+ repository: string;
24
+ /** Image tag (e.g., '2.10-alpine', 'latest') */
25
+ tag: string;
26
+ }
27
+ /**
28
+ * Configuration for a service that will be deployed to DigitalOcean App Platform.
29
+ * Each service in infra/services/*.ts should export a default object of this type.
30
+ *
31
+ * ## Port Configuration
32
+ *
33
+ * Services can expose ports in two ways:
34
+ *
35
+ * - `httpPort`: Public HTTP port with ingress routing (requires `ingressPrefix`)
36
+ * - `internalPorts`: Internal-only ports, not publicly accessible
37
+ *
38
+ * The **first port** (httpPort or internalPorts[0]) is used as the primary port for:
39
+ * - `<SERVICE>_PORT` environment variable
40
+ * - `<SERVICE>_URL` environment variable (unless `internalUrl` is set)
41
+ *
42
+ * ## Examples
43
+ *
44
+ * ```ts
45
+ * // Public service (API Gateway)
46
+ * { httpPort: 4000, ingressPrefix: '/api' }
47
+ *
48
+ * // Internal-only service (Orders)
49
+ * { internalPorts: [4001] }
50
+ *
51
+ * // Internal service with custom protocol (NATS)
52
+ * { internalPorts: [4222, 8222], internalUrl: 'nats://nats:4222' }
53
+ * ```
54
+ *
55
+ * @see https://docs.digitalocean.com/reference/api/digitalocean/#tag/Apps
56
+ */
57
+ export type ServiceConfig = Partial<Omit<AppSpecService, 'image'>> & {
58
+ /** Unique name of the service (required) */
59
+ name: string;
60
+ /**
61
+ * Ingress path prefix for public routing (e.g., '/api').
62
+ * Only used when `httpPort` is set. Services with only `internalPorts` cannot have ingress.
63
+ */
64
+ ingressPrefix?: string;
65
+ /** Set to true to exclude this service from deployment */
66
+ skip?: boolean;
67
+ /** Custom image configuration (defaults to GHCR image based on service name) */
68
+ image?: ImageConfig;
69
+ /**
70
+ * Override the internal URL for service-to-service communication.
71
+ * Use this for non-HTTP protocols (e.g., 'nats://nats:4222').
72
+ * If not set, defaults to `http://{name}:{primaryPort}`.
73
+ */
74
+ internalUrl?: string;
75
+ };
76
+ /**
77
+ * Full service spec after merging with common config.
78
+ * This is what gets passed to the DigitalOcean App spec.
79
+ */
80
+ export type FullServiceSpec = AppSpecService;
81
+ export * from './service-names';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Auto-generated file - do not edit manually!
3
+ * Generated by: bun run generate-env
4
+ */
5
+ /**
6
+ * Known service names in the platform.
7
+ * This type is auto-generated from infra/services/*.ts
8
+ */
9
+ export type ServiceName = 'nats' | 'orders' | 'storefront' | 'api-gateway';
package/lib/env.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Environment helpers for services to read configuration.
3
+ * Use this entry point in production services (Node.js, NestJS, etc.)
4
+ *
5
+ * @example
6
+ * import { getServicePort, getServiceUrl } from '@crossdelta/infrastructure/env'
7
+ */
8
+
9
+ export * from './helpers/service-runtime'
10
+ export type { ServiceName } from './types/service-names'
@@ -0,0 +1,45 @@
1
+ import type { Input } from '@pulumi/pulumi'
2
+
3
+ /**
4
+ * Ensures that a string ends with a dot (useful for DNS CNAME values).
5
+ */
6
+ export const ensureDot = (str: string) => {
7
+ return str.endsWith('.') ? str : `${str}.`
8
+ }
9
+
10
+ /**
11
+ * Common alert configuration for services
12
+ */
13
+ export const defaultAlerts = [
14
+ {
15
+ rule: 'CPU_UTILIZATION',
16
+ operator: 'GREATER_THAN',
17
+ value: 70,
18
+ window: 'FIVE_MINUTES',
19
+ },
20
+ {
21
+ rule: 'MEM_UTILIZATION',
22
+ operator: 'GREATER_THAN',
23
+ value: 70,
24
+ window: 'FIVE_MINUTES',
25
+ },
26
+ ]
27
+
28
+ /**
29
+ * Creates log destinations config with Logtail token
30
+ */
31
+ export const createLogDestinations = (logtailToken: Input<string>) => [
32
+ {
33
+ name: 'better-stacks-logs',
34
+ logtail: {
35
+ token: logtailToken,
36
+ },
37
+ },
38
+ ]
39
+
40
+ /**
41
+ * Default health check configuration
42
+ */
43
+ export const defaultHealthCheck = {
44
+ httpPath: '/health',
45
+ }
@@ -0,0 +1,57 @@
1
+ import { readdirSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import type { ServiceConfig } from '../types'
4
+
5
+ /**
6
+ * Get the primary port for a service (for validation).
7
+ * Prefers httpPort, falls back to first internalPort, then 8080.
8
+ */
9
+ function getServicePort(config: ServiceConfig): number {
10
+ if (config.httpPort) return config.httpPort as number
11
+ const internalPorts = config.internalPorts as number[] | undefined
12
+ if (internalPorts?.[0]) return internalPorts[0]
13
+ return 8080
14
+ }
15
+
16
+ /**
17
+ * Validates that no two services use the same port.
18
+ * @throws Error if duplicate ports are found
19
+ */
20
+ function validateNoDuplicatePorts(configs: ServiceConfig[]): void {
21
+ const portMap = new Map<number, string[]>()
22
+
23
+ for (const config of configs) {
24
+ const port = getServicePort(config)
25
+ const existing = portMap.get(port) || []
26
+ existing.push(config.name)
27
+ portMap.set(port, existing)
28
+ }
29
+
30
+ const conflicts = [...portMap.entries()]
31
+ .filter(([, services]) => services.length > 1)
32
+ .map(([port, services]) => `Port ${port}: ${services.join(', ')}`)
33
+
34
+ if (conflicts.length > 0) {
35
+ throw new Error(`Port conflicts detected:\n${conflicts.join('\n')}`)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Auto-discovers all service configurations from a directory.
41
+ * Each .ts file (except index.ts) should export a ServiceConfig as default.
42
+ * @throws Error if duplicate ports are detected
43
+ */
44
+ export function discoverServices(servicesDir: string): ServiceConfig[] {
45
+ const files = readdirSync(servicesDir).filter(
46
+ (file) => file.endsWith('.ts') && file !== 'index.ts'
47
+ )
48
+
49
+ const configs = files.map((file) => {
50
+ const module = require(join(servicesDir, file))
51
+ return module.default as ServiceConfig
52
+ })
53
+
54
+ validateNoDuplicatePorts(configs)
55
+
56
+ return configs
57
+ }