@crossdelta/infrastructure 0.1.35 → 0.1.36

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crossdelta/infrastructure",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -9,18 +9,15 @@
9
9
  "main": "dist/index.cjs",
10
10
  "types": "dist/index.d.ts",
11
11
  "files": [
12
- "dist",
13
- "lib"
12
+ "dist"
14
13
  ],
15
14
  "exports": {
16
15
  ".": {
17
- "bun": "./lib/index.ts",
18
16
  "import": "./dist/index.js",
19
17
  "require": "./dist/index.cjs",
20
18
  "types": "./dist/index.d.ts"
21
19
  },
22
20
  "./env": {
23
- "bun": "./lib/env.ts",
24
21
  "import": "./dist/env.js",
25
22
  "require": "./dist/env.cjs",
26
23
  "types": "./dist/env.d.ts"
package/lib/env.ts DELETED
@@ -1,10 +0,0 @@
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'
@@ -1,45 +0,0 @@
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
- }
@@ -1,57 +0,0 @@
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
- }
@@ -1,24 +0,0 @@
1
- import { type ImageConfig, RegistryType } from '../types'
2
-
3
- /**
4
- * Helper to create a Docker Hub image config.
5
- * For official images, use 'library' as the registry.
6
- *
7
- * @example
8
- * // Official image: library/nats:2.10-alpine
9
- * dockerHubImage('nats', '2.10-alpine')
10
- *
11
- * @example
12
- * // User image: bitnami/redis:7.0
13
- * dockerHubImage('redis', '7.0', 'bitnami')
14
- */
15
- export const dockerHubImage = (
16
- repository: string,
17
- tag: string,
18
- registry = 'library',
19
- ): ImageConfig => ({
20
- registryType: RegistryType.DOCKER_HUB,
21
- registry,
22
- repository,
23
- tag,
24
- })
@@ -1,54 +0,0 @@
1
- import type { Output } from '@pulumi/pulumi'
2
-
3
- const scopeImageTagsRaw = process.env.SCOPE_IMAGE_TAGS ?? ''
4
- const scopeImageTags = (() => {
5
- if (!scopeImageTagsRaw.trim()) {
6
- return {} as Record<string, string>
7
- }
8
-
9
- try {
10
- const parsed = JSON.parse(scopeImageTagsRaw) as Record<string, unknown>
11
- const tags: Record<string, string> = {}
12
- for (const [key, value] of Object.entries(parsed ?? {})) {
13
- if (typeof value === 'string' && value.trim().length > 0) {
14
- tags[key] = value.trim()
15
- }
16
- }
17
- return tags
18
- } catch (error) {
19
- console.warn('Unable to parse scope image tags from environment:', error)
20
- return {} as Record<string, string>
21
- }
22
- })()
23
-
24
- const resolveImageTag = (scopeName: string) => {
25
- const tag = scopeImageTags[scopeName]
26
- if (!tag) {
27
- // Use 'latest' as fallback for local development/preview
28
- console.warn(`No image tag for "${scopeName}", using "latest" as fallback`)
29
- return 'latest'
30
- }
31
-
32
- return tag
33
- }
34
-
35
- /**
36
- * Gets the image configuration for a given repository.
37
- * @param repository - The name of the repository.
38
- * @returns The image configuration.
39
- */
40
- export const getImage = (repository: string, registryCredentials: Output<string>) => {
41
- const scopeName = repository.split('/').pop()
42
-
43
- if (!scopeName) {
44
- throw new Error(`Invalid repository name: ${repository}`)
45
- }
46
-
47
- return {
48
- registryType: 'GHCR' as any,
49
- registry: 'orderboss',
50
- repository,
51
- registryCredentials,
52
- tag: resolveImageTag(scopeName),
53
- }
54
- }
@@ -1,7 +0,0 @@
1
- export * from './config'
2
- export * from './discover-services'
3
- export * from './docker-hub-image'
4
- export * from './image'
5
- export * from './service-builder'
6
- export * from './service-runtime'
7
- export * from './service-urls'
@@ -1,61 +0,0 @@
1
- import type { AppSpecService } from '@pulumi/digitalocean/types/input'
2
- import type { Output } from '@pulumi/pulumi'
3
- import type { ServiceConfig } from '../types'
4
- import { createLogDestinations, defaultAlerts, defaultHealthCheck } from './config'
5
- import { getImage } from './image'
6
-
7
- export interface BuildServicesOptions {
8
- serviceConfigs: ServiceConfig[]
9
- registryCredentials: Output<string>
10
- logtailToken: Output<string>
11
- }
12
-
13
- /**
14
- * Builds the services array from service configs.
15
- */
16
- export function buildServices(options: BuildServicesOptions): AppSpecService[] {
17
- const { serviceConfigs, registryCredentials, logtailToken } = options
18
- const logDestinations = createLogDestinations(logtailToken)
19
-
20
- return serviceConfigs.map((config) => {
21
- // Remove custom properties that shouldn't be passed to DO API
22
- const {
23
- ingressPrefix: _ingressPrefix,
24
- skip: _skip,
25
- image: configImage,
26
- internalUrl: _internalUrl,
27
- ...serviceConfig
28
- } = config
29
-
30
- // Use provided image or default to GHCR
31
- const image = configImage ?? getImage(`platform/${config.name}`, registryCredentials)
32
-
33
- return {
34
- healthCheck: defaultHealthCheck,
35
- alerts: defaultAlerts,
36
- logDestinations,
37
- ...serviceConfig,
38
- image,
39
- }
40
- })
41
- }
42
-
43
- /**
44
- * Builds the ingress rules from service configs.
45
- * Only includes services that have both an ingressPrefix AND httpPort defined.
46
- * Services with only internalPorts cannot have ingress rules.
47
- */
48
- export function buildIngressRules(serviceConfigs: ServiceConfig[]) {
49
- return serviceConfigs
50
- .filter((config): config is ServiceConfig & { ingressPrefix: string; httpPort: number } =>
51
- config.ingressPrefix !== undefined && config.httpPort !== undefined
52
- )
53
- .map((config) => ({
54
- component: { name: config.name },
55
- match: {
56
- path: {
57
- prefix: config.ingressPrefix,
58
- },
59
- },
60
- }))
61
- }
@@ -1,62 +0,0 @@
1
- /**
2
- * Runtime helpers for services to access their configuration.
3
- * These functions read from environment variables set by generate-env (local)
4
- * or injected by DO App Platform (production).
5
- */
6
-
7
- import type { ServiceName } from '../types'
8
-
9
- /**
10
- * Convert service name to valid env var key (e.g., api-gateway -> API_GATEWAY)
11
- */
12
- const toEnvKey = (name: string) => name.toUpperCase().replace(/-/g, '_')
13
-
14
- /**
15
- * Get the port for a service from environment variables.
16
- * Reads from <SERVICE_NAME>_PORT (e.g., ORDERS_PORT, API_GATEWAY_PORT)
17
- *
18
- * @param serviceName - The service name (e.g., 'orders', 'api-gateway')
19
- * @param defaultPort - Fallback port if env var is not set (default: 8080)
20
- * @returns The configured port number
21
- *
22
- * @example
23
- * ```typescript
24
- * import { getServicePort } from '@crossdelta/infrastructure/env'
25
- *
26
- * const port = getServicePort('orders', 3001)
27
- * // Reads process.env.ORDERS_PORT, falls back to 3001
28
- * ```
29
- */
30
- export function getServicePort(serviceName: ServiceName, defaultPort = 8080): number {
31
- const envKey = `${toEnvKey(serviceName)}_PORT`
32
- const envValue = process.env[envKey]
33
-
34
- if (envValue) {
35
- const parsed = Number(envValue)
36
- if (!Number.isNaN(parsed)) {
37
- return parsed
38
- }
39
- }
40
-
41
- return defaultPort
42
- }
43
-
44
- /**
45
- * Get the URL for a service from environment variables.
46
- * Reads from <SERVICE_NAME>_URL (e.g., ORDERS_URL, API_GATEWAY_URL)
47
- *
48
- * @param serviceName - The service name (e.g., 'orders', 'api-gateway')
49
- * @returns The service URL or undefined if not set
50
- *
51
- * @example
52
- * ```typescript
53
- * import { getServiceUrl } from '@crossdelta/infrastructure/env'
54
- *
55
- * const ordersUrl = getServiceUrl('orders')
56
- * // Returns process.env.ORDERS_URL
57
- * ```
58
- */
59
- export function getServiceUrl(serviceName: ServiceName): string | undefined {
60
- const envKey = `${toEnvKey(serviceName)}_URL`
61
- return process.env[envKey]
62
- }
@@ -1,82 +0,0 @@
1
- import type { AppSpecEnv } from '@pulumi/digitalocean/types/input'
2
- import type { ServiceConfig } from '../types'
3
-
4
- /**
5
- * Get the primary port for a service.
6
- * Prefers httpPort, falls back to first internalPort, then 8080.
7
- */
8
- function getServicePort(config: ServiceConfig): number {
9
- if (config.httpPort) return config.httpPort as number
10
- // internalPorts is Input<Input<number>[]> at type level, but plain number[] at runtime
11
- const internalPorts = config.internalPorts as number[] | undefined
12
- if (internalPorts?.[0]) return internalPorts[0]
13
- return 8080
14
- }
15
-
16
- /**
17
- * Generate internal service URLs for service-to-service communication.
18
- * Uses DigitalOcean App Platform's internal DNS (e.g., http://orders:3001)
19
- * Uses internalUrl if defined, otherwise defaults to http://{name}:{port}
20
- */
21
- export function buildInternalUrls(serviceConfigs: ServiceConfig[]): Record<string, string> {
22
- return Object.fromEntries(
23
- serviceConfigs.map((config) => {
24
- const url = config.internalUrl ?? `http://${config.name}:${getServicePort(config)}`
25
- return [config.name, url]
26
- }),
27
- )
28
- }
29
-
30
- /**
31
- * Generate external service URLs based on primary domain and ingress prefix.
32
- */
33
- export function buildExternalUrls(
34
- serviceConfigs: ServiceConfig[],
35
- baseUrl: string,
36
- ): Record<string, string> {
37
- return Object.fromEntries(
38
- serviceConfigs
39
- .filter((config) => config.ingressPrefix !== undefined)
40
- .map((config) => {
41
- const path = config.ingressPrefix === '/' ? '' : config.ingressPrefix
42
- return [config.name, `${baseUrl}${path}`]
43
- }),
44
- )
45
- }
46
-
47
- /**
48
- * Generate local development URLs (localhost with configured ports).
49
- */
50
- export function buildLocalUrls(serviceConfigs: ServiceConfig[]): Record<string, string> {
51
- return Object.fromEntries(
52
- serviceConfigs.map((config) => {
53
- const port = getServicePort(config)
54
- return [config.name, `http://localhost:${port}`]
55
- }),
56
- )
57
- }
58
-
59
- /**
60
- * Generate environment variables for service URLs.
61
- * Used to inject service URLs into DO App Platform.
62
- * Uses internalUrl if defined, otherwise defaults to http://{name}:{port}
63
- */
64
- export function buildServiceUrlEnvs(serviceConfigs: ServiceConfig[]): AppSpecEnv[] {
65
- return serviceConfigs.map((config) => ({
66
- key: `${config.name.toUpperCase().replace(/-/g, '_')}_URL`,
67
- scope: 'RUN_TIME' as const,
68
- value: config.internalUrl ?? `http://${config.name}:${getServicePort(config)}`,
69
- }))
70
- }
71
-
72
- /**
73
- * Generate environment variables for service ports.
74
- * Used to inject service ports into DO App Platform.
75
- */
76
- export function buildServicePortEnvs(serviceConfigs: ServiceConfig[]): AppSpecEnv[] {
77
- return serviceConfigs.map((config) => ({
78
- key: `${config.name.toUpperCase().replace(/-/g, '_')}_PORT`,
79
- scope: 'RUN_TIME' as const,
80
- value: String(getServicePort(config)),
81
- }))
82
- }
package/lib/index.ts DELETED
@@ -1,2 +0,0 @@
1
- export * from './helpers'
2
- export * from './types'
@@ -1,87 +0,0 @@
1
- import type { AppSpecService } from '@pulumi/digitalocean/types/input'
2
-
3
- /**
4
- * Supported container registry types for DigitalOcean App Platform.
5
- */
6
- export const RegistryType = {
7
- /** Docker Hub - public or private images */
8
- DOCKER_HUB: 'DOCKER_HUB',
9
- /** GitHub Container Registry */
10
- GHCR: 'GHCR',
11
- /** DigitalOcean Container Registry */
12
- DOCR: 'DOCR',
13
- } as const
14
-
15
- export type RegistryType = (typeof RegistryType)[keyof typeof RegistryType]
16
-
17
- /**
18
- * Image configuration for a service.
19
- */
20
- export interface ImageConfig {
21
- /** Registry type */
22
- registryType: RegistryType
23
- /** Registry name (e.g., 'library' for official Docker Hub images, 'orderboss' for GHCR) */
24
- registry: string
25
- /** Repository name (e.g., 'nats', 'platform/storefront') */
26
- repository: string
27
- /** Image tag (e.g., '2.10-alpine', 'latest') */
28
- tag: string
29
- }
30
-
31
- /**
32
- * Configuration for a service that will be deployed to DigitalOcean App Platform.
33
- * Each service in infra/services/*.ts should export a default object of this type.
34
- *
35
- * ## Port Configuration
36
- *
37
- * Services can expose ports in two ways:
38
- *
39
- * - `httpPort`: Public HTTP port with ingress routing (requires `ingressPrefix`)
40
- * - `internalPorts`: Internal-only ports, not publicly accessible
41
- *
42
- * The **first port** (httpPort or internalPorts[0]) is used as the primary port for:
43
- * - `<SERVICE>_PORT` environment variable
44
- * - `<SERVICE>_URL` environment variable (unless `internalUrl` is set)
45
- *
46
- * ## Examples
47
- *
48
- * ```ts
49
- * // Public service (API Gateway)
50
- * { httpPort: 4000, ingressPrefix: '/api' }
51
- *
52
- * // Internal-only service (Orders)
53
- * { internalPorts: [4001] }
54
- *
55
- * // Internal service with custom protocol (NATS)
56
- * { internalPorts: [4222, 8222], internalUrl: 'nats://nats:4222' }
57
- * ```
58
- *
59
- * @see https://docs.digitalocean.com/reference/api/digitalocean/#tag/Apps
60
- */
61
- export type ServiceConfig = Partial<Omit<AppSpecService, 'image'>> & {
62
- /** Unique name of the service (required) */
63
- name: string
64
- /**
65
- * Ingress path prefix for public routing (e.g., '/api').
66
- * Only used when `httpPort` is set. Services with only `internalPorts` cannot have ingress.
67
- */
68
- ingressPrefix?: string
69
- /** Set to true to exclude this service from deployment */
70
- skip?: boolean
71
- /** Custom image configuration (defaults to GHCR image based on service name) */
72
- image?: ImageConfig
73
- /**
74
- * Override the internal URL for service-to-service communication.
75
- * Use this for non-HTTP protocols (e.g., 'nats://nats:4222').
76
- * If not set, defaults to `http://{name}:{primaryPort}`.
77
- */
78
- internalUrl?: string
79
- }
80
-
81
- /**
82
- * Full service spec after merging with common config.
83
- * This is what gets passed to the DigitalOcean App spec.
84
- */
85
- export type FullServiceSpec = AppSpecService
86
-
87
- export * from './service-names'
@@ -1,10 +0,0 @@
1
- /**
2
- * Auto-generated file - do not edit manually!
3
- * Generated by: bun run generate-env
4
- */
5
-
6
- /**
7
- * Known service names in the platform.
8
- * This type is auto-generated from infra/services/*.ts
9
- */
10
- export type ServiceName = 'nats' | 'orders' | 'storefront' | 'api-gateway'