iec-builder 0.1.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/.claude/settings.local.json +111 -0
- package/.iec.yaml +5 -0
- package/CLAUDE.md +174 -0
- package/Dockerfile +34 -0
- package/README.md +84 -0
- package/catalog-info.yaml +11 -0
- package/dist/config/env.d.ts +219 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +89 -0
- package/dist/config/env.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +148 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +43 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +217 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/org-access.d.ts +28 -0
- package/dist/middleware/org-access.d.ts.map +1 -0
- package/dist/middleware/org-access.js +102 -0
- package/dist/middleware/org-access.js.map +1 -0
- package/dist/models/types.d.ts +254 -0
- package/dist/models/types.d.ts.map +1 -0
- package/dist/models/types.js +2 -0
- package/dist/models/types.js.map +1 -0
- package/dist/routes/ai.d.ts +2 -0
- package/dist/routes/ai.d.ts.map +1 -0
- package/dist/routes/ai.js +77 -0
- package/dist/routes/ai.js.map +1 -0
- package/dist/routes/audit.d.ts +2 -0
- package/dist/routes/audit.d.ts.map +1 -0
- package/dist/routes/audit.js +102 -0
- package/dist/routes/audit.js.map +1 -0
- package/dist/routes/builds.d.ts +2 -0
- package/dist/routes/builds.d.ts.map +1 -0
- package/dist/routes/builds.js +262 -0
- package/dist/routes/builds.js.map +1 -0
- package/dist/routes/cluster.d.ts +2 -0
- package/dist/routes/cluster.d.ts.map +1 -0
- package/dist/routes/cluster.js +181 -0
- package/dist/routes/cluster.js.map +1 -0
- package/dist/routes/config.d.ts +2 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +291 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/databases.d.ts +2 -0
- package/dist/routes/databases.d.ts.map +1 -0
- package/dist/routes/databases.js +161 -0
- package/dist/routes/databases.js.map +1 -0
- package/dist/routes/db-whitelist.d.ts +2 -0
- package/dist/routes/db-whitelist.d.ts.map +1 -0
- package/dist/routes/db-whitelist.js +148 -0
- package/dist/routes/db-whitelist.js.map +1 -0
- package/dist/routes/domains.d.ts +2 -0
- package/dist/routes/domains.d.ts.map +1 -0
- package/dist/routes/domains.js +449 -0
- package/dist/routes/domains.js.map +1 -0
- package/dist/routes/oauth.d.ts +2 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +180 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/routes/observability.d.ts +2 -0
- package/dist/routes/observability.d.ts.map +1 -0
- package/dist/routes/observability.js +167 -0
- package/dist/routes/observability.js.map +1 -0
- package/dist/routes/orgs.d.ts +2 -0
- package/dist/routes/orgs.d.ts.map +1 -0
- package/dist/routes/orgs.js +270 -0
- package/dist/routes/orgs.js.map +1 -0
- package/dist/routes/platform.d.ts +2 -0
- package/dist/routes/platform.d.ts.map +1 -0
- package/dist/routes/platform.js +107 -0
- package/dist/routes/platform.js.map +1 -0
- package/dist/routes/push.d.ts +2 -0
- package/dist/routes/push.d.ts.map +1 -0
- package/dist/routes/push.js +233 -0
- package/dist/routes/push.js.map +1 -0
- package/dist/routes/rotation.d.ts +3 -0
- package/dist/routes/rotation.d.ts.map +1 -0
- package/dist/routes/rotation.js +154 -0
- package/dist/routes/rotation.js.map +1 -0
- package/dist/routes/services.d.ts +2 -0
- package/dist/routes/services.d.ts.map +1 -0
- package/dist/routes/services.js +246 -0
- package/dist/routes/services.js.map +1 -0
- package/dist/routes/storage.d.ts +2 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +118 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/users.d.ts +2 -0
- package/dist/routes/users.d.ts.map +1 -0
- package/dist/routes/users.js +183 -0
- package/dist/routes/users.js.map +1 -0
- package/dist/routes/versions.d.ts +2 -0
- package/dist/routes/versions.d.ts.map +1 -0
- package/dist/routes/versions.js +195 -0
- package/dist/routes/versions.js.map +1 -0
- package/dist/routes/webhooks.d.ts +2 -0
- package/dist/routes/webhooks.d.ts.map +1 -0
- package/dist/routes/webhooks.js +334 -0
- package/dist/routes/webhooks.js.map +1 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts +2 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.d.ts.map +1 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.js +482 -0
- package/dist/services/__tests__/deploy-pipeline.integration.test.js.map +1 -0
- package/dist/services/bio-client.d.ts +68 -0
- package/dist/services/bio-client.d.ts.map +1 -0
- package/dist/services/bio-client.js +110 -0
- package/dist/services/bio-client.js.map +1 -0
- package/dist/services/build-queue.d.ts +7 -0
- package/dist/services/build-queue.d.ts.map +1 -0
- package/dist/services/build-queue.js +114 -0
- package/dist/services/build-queue.js.map +1 -0
- package/dist/services/builder.d.ts +7 -0
- package/dist/services/builder.d.ts.map +1 -0
- package/dist/services/builder.js +1384 -0
- package/dist/services/builder.js.map +1 -0
- package/dist/services/catalog.d.ts +177 -0
- package/dist/services/catalog.d.ts.map +1 -0
- package/dist/services/catalog.js +805 -0
- package/dist/services/catalog.js.map +1 -0
- package/dist/services/catalog.test.d.ts +2 -0
- package/dist/services/catalog.test.d.ts.map +1 -0
- package/dist/services/catalog.test.js +467 -0
- package/dist/services/catalog.test.js.map +1 -0
- package/dist/services/cloudflare.d.ts +43 -0
- package/dist/services/cloudflare.d.ts.map +1 -0
- package/dist/services/cloudflare.js +182 -0
- package/dist/services/cloudflare.js.map +1 -0
- package/dist/services/config-validator.d.ts +28 -0
- package/dist/services/config-validator.d.ts.map +1 -0
- package/dist/services/config-validator.js +68 -0
- package/dist/services/config-validator.js.map +1 -0
- package/dist/services/config-validator.test.d.ts +2 -0
- package/dist/services/config-validator.test.d.ts.map +1 -0
- package/dist/services/config-validator.test.js +151 -0
- package/dist/services/config-validator.test.js.map +1 -0
- package/dist/services/crypto.d.ts +19 -0
- package/dist/services/crypto.d.ts.map +1 -0
- package/dist/services/crypto.js +63 -0
- package/dist/services/crypto.js.map +1 -0
- package/dist/services/database.d.ts +26 -0
- package/dist/services/database.d.ts.map +1 -0
- package/dist/services/database.js +100 -0
- package/dist/services/database.js.map +1 -0
- package/dist/services/db-credential-manager.d.ts +73 -0
- package/dist/services/db-credential-manager.d.ts.map +1 -0
- package/dist/services/db-credential-manager.js +342 -0
- package/dist/services/db-credential-manager.js.map +1 -0
- package/dist/services/db-provisioner.d.ts +57 -0
- package/dist/services/db-provisioner.d.ts.map +1 -0
- package/dist/services/db-provisioner.js +400 -0
- package/dist/services/db-provisioner.js.map +1 -0
- package/dist/services/db-provisioner.test.d.ts +2 -0
- package/dist/services/db-provisioner.test.d.ts.map +1 -0
- package/dist/services/db-provisioner.test.js +141 -0
- package/dist/services/db-provisioner.test.js.map +1 -0
- package/dist/services/db-whitelist.d.ts +58 -0
- package/dist/services/db-whitelist.d.ts.map +1 -0
- package/dist/services/db-whitelist.js +379 -0
- package/dist/services/db-whitelist.js.map +1 -0
- package/dist/services/dependency-resolver.d.ts +58 -0
- package/dist/services/dependency-resolver.d.ts.map +1 -0
- package/dist/services/dependency-resolver.js +180 -0
- package/dist/services/dependency-resolver.js.map +1 -0
- package/dist/services/dependency-resolver.test.d.ts +2 -0
- package/dist/services/dependency-resolver.test.d.ts.map +1 -0
- package/dist/services/dependency-resolver.test.js +195 -0
- package/dist/services/dependency-resolver.test.js.map +1 -0
- package/dist/services/deploy-gate.d.ts +19 -0
- package/dist/services/deploy-gate.d.ts.map +1 -0
- package/dist/services/deploy-gate.js +56 -0
- package/dist/services/deploy-gate.js.map +1 -0
- package/dist/services/deploy-gate.test.d.ts +2 -0
- package/dist/services/deploy-gate.test.d.ts.map +1 -0
- package/dist/services/deploy-gate.test.js +199 -0
- package/dist/services/deploy-gate.test.js.map +1 -0
- package/dist/services/dockerfile-generator.d.ts +31 -0
- package/dist/services/dockerfile-generator.d.ts.map +1 -0
- package/dist/services/dockerfile-generator.js +544 -0
- package/dist/services/dockerfile-generator.js.map +1 -0
- package/dist/services/dockerfile-generator.test.d.ts +2 -0
- package/dist/services/dockerfile-generator.test.d.ts.map +1 -0
- package/dist/services/dockerfile-generator.test.js +144 -0
- package/dist/services/dockerfile-generator.test.js.map +1 -0
- package/dist/services/forgejo.d.ts +58 -0
- package/dist/services/forgejo.d.ts.map +1 -0
- package/dist/services/forgejo.js +131 -0
- package/dist/services/forgejo.js.map +1 -0
- package/dist/services/koko.d.ts +153 -0
- package/dist/services/koko.d.ts.map +1 -0
- package/dist/services/koko.js +260 -0
- package/dist/services/koko.js.map +1 -0
- package/dist/services/kubernetes.d.ts +16 -0
- package/dist/services/kubernetes.d.ts.map +1 -0
- package/dist/services/kubernetes.js +102 -0
- package/dist/services/kubernetes.js.map +1 -0
- package/dist/services/oauth-provisioner.d.ts +30 -0
- package/dist/services/oauth-provisioner.d.ts.map +1 -0
- package/dist/services/oauth-provisioner.js +182 -0
- package/dist/services/oauth-provisioner.js.map +1 -0
- package/dist/services/oauth-provisioner.test.d.ts +2 -0
- package/dist/services/oauth-provisioner.test.d.ts.map +1 -0
- package/dist/services/oauth-provisioner.test.js +349 -0
- package/dist/services/oauth-provisioner.test.js.map +1 -0
- package/dist/services/pod-diagnostics.d.ts +11 -0
- package/dist/services/pod-diagnostics.d.ts.map +1 -0
- package/dist/services/pod-diagnostics.js +201 -0
- package/dist/services/pod-diagnostics.js.map +1 -0
- package/dist/services/rotation-scheduler.d.ts +2 -0
- package/dist/services/rotation-scheduler.d.ts.map +1 -0
- package/dist/services/rotation-scheduler.js +215 -0
- package/dist/services/rotation-scheduler.js.map +1 -0
- package/dist/services/storage-credential-manager.d.ts +43 -0
- package/dist/services/storage-credential-manager.d.ts.map +1 -0
- package/dist/services/storage-credential-manager.js +159 -0
- package/dist/services/storage-credential-manager.js.map +1 -0
- package/dist/services/storage-provisioner.d.ts +32 -0
- package/dist/services/storage-provisioner.d.ts.map +1 -0
- package/dist/services/storage-provisioner.js +136 -0
- package/dist/services/storage-provisioner.js.map +1 -0
- package/dist/services/storage.d.ts +65 -0
- package/dist/services/storage.d.ts.map +1 -0
- package/dist/services/storage.js +204 -0
- package/dist/services/storage.js.map +1 -0
- package/dist/services/troubleshooter.d.ts +22 -0
- package/dist/services/troubleshooter.d.ts.map +1 -0
- package/dist/services/troubleshooter.js +168 -0
- package/dist/services/troubleshooter.js.map +1 -0
- package/dist/services/vault-client.d.ts +114 -0
- package/dist/services/vault-client.d.ts.map +1 -0
- package/dist/services/vault-client.js +411 -0
- package/dist/services/vault-client.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +6 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/response.d.ts +13 -0
- package/dist/utils/response.d.ts.map +1 -0
- package/dist/utils/response.js +12 -0
- package/dist/utils/response.js.map +1 -0
- package/docs/registry-migration.md +301 -0
- package/docs/registry-quickstart.md +169 -0
- package/ecosystem.config.cjs +14 -0
- package/findings.md +168 -0
- package/helm/default-service/Chart.yaml +6 -0
- package/helm/default-service/templates/deployment.yaml +97 -0
- package/helm/default-service/templates/ingress.yaml +43 -0
- package/helm/default-service/templates/service.yaml +17 -0
- package/helm/default-service/values.yaml +82 -0
- package/helm/services/iec-builder/Chart.yaml +6 -0
- package/helm/services/iec-builder/templates/_helpers.tpl +61 -0
- package/helm/services/iec-builder/templates/deployment.yaml +73 -0
- package/helm/services/iec-builder/templates/service.yaml +15 -0
- package/helm/services/iec-builder/templates/serviceaccount.yaml +12 -0
- package/helm/services/iec-builder/values.yaml +56 -0
- package/helm/vault-values.yaml +127 -0
- package/package.json +45 -0
- package/progress.md +156 -0
- package/scripts/.vault-init-keys.json +23 -0
- package/scripts/backfill-ownership.ts +113 -0
- package/scripts/finalize-mongo-auth.sh +212 -0
- package/scripts/setup-ipset.sh +107 -0
- package/scripts/setup-mongo-auth.sh +163 -0
- package/scripts/setup-neo4j-auth.sh +62 -0
- package/scripts/setup-redis-auth.sh +55 -0
- package/scripts/setup-registry-secret.sh +71 -0
- package/scripts/setup-vault.sh +308 -0
- package/src/config/env.ts +117 -0
- package/src/index.ts +153 -0
- package/src/middleware/auth.ts +294 -0
- package/src/middleware/org-access.ts +126 -0
- package/src/models/types.ts +288 -0
- package/src/routes/ai.ts +115 -0
- package/src/routes/audit.ts +121 -0
- package/src/routes/builds.ts +320 -0
- package/src/routes/cluster.ts +235 -0
- package/src/routes/config.ts +369 -0
- package/src/routes/databases.ts +201 -0
- package/src/routes/db-whitelist.ts +204 -0
- package/src/routes/domains.ts +547 -0
- package/src/routes/oauth.ts +195 -0
- package/src/routes/observability.ts +205 -0
- package/src/routes/orgs.ts +330 -0
- package/src/routes/platform.ts +134 -0
- package/src/routes/rotation.ts +191 -0
- package/src/routes/services.ts +290 -0
- package/src/routes/storage.ts +153 -0
- package/src/routes/users.ts +235 -0
- package/src/routes/webhooks.ts +384 -0
- package/src/services/__tests__/catalog-storage.test.ts +186 -0
- package/src/services/__tests__/deploy-pipeline.integration.test.ts +624 -0
- package/src/services/__tests__/pod-diagnostics.test.ts +332 -0
- package/src/services/__tests__/storage-credential-manager.test.ts +129 -0
- package/src/services/__tests__/storage-provisioner.test.ts +166 -0
- package/src/services/__tests__/troubleshooter.test.ts +329 -0
- package/src/services/bio-client.ts +189 -0
- package/src/services/build-queue.ts +137 -0
- package/src/services/builder.ts +1800 -0
- package/src/services/catalog.test.ts +1389 -0
- package/src/services/catalog.ts +1187 -0
- package/src/services/cloudflare.ts +259 -0
- package/src/services/config-validator.test.ts +190 -0
- package/src/services/config-validator.ts +108 -0
- package/src/services/crypto.ts +78 -0
- package/src/services/database.ts +122 -0
- package/src/services/db-credential-manager.test.ts +101 -0
- package/src/services/db-credential-manager.ts +447 -0
- package/src/services/db-provisioner.test.ts +602 -0
- package/src/services/db-provisioner.ts +589 -0
- package/src/services/db-whitelist.test.ts +671 -0
- package/src/services/db-whitelist.ts +496 -0
- package/src/services/dependency-resolver.test.ts +677 -0
- package/src/services/dependency-resolver.ts +319 -0
- package/src/services/deploy-gate.test.ts +247 -0
- package/src/services/deploy-gate.ts +75 -0
- package/src/services/dockerfile-generator.test.ts +401 -0
- package/src/services/dockerfile-generator.ts +606 -0
- package/src/services/forgejo.ts +212 -0
- package/src/services/koko.ts +492 -0
- package/src/services/kubernetes.ts +141 -0
- package/src/services/oauth-provisioner.test.ts +477 -0
- package/src/services/oauth-provisioner.ts +286 -0
- package/src/services/pod-diagnostics.ts +261 -0
- package/src/services/rotation-scheduler.ts +293 -0
- package/src/services/storage-credential-manager.ts +223 -0
- package/src/services/storage-provisioner.ts +216 -0
- package/src/services/storage.ts +274 -0
- package/src/services/troubleshooter.ts +208 -0
- package/src/services/vault-client.test.ts +272 -0
- package/src/services/vault-client.ts +587 -0
- package/src/utils/logger.ts +6 -0
- package/src/utils/response.ts +23 -0
- package/task_plan.md +171 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { config } from 'dotenv'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
config({ path: resolve(__dirname, '../../.env'), override: true })
|
|
8
|
+
|
|
9
|
+
const envSchema = z.object({
|
|
10
|
+
NODE_ENV: z.enum(['development', 'staging', 'production', 'test']).default('development'),
|
|
11
|
+
PORT: z.string().transform(Number).default('3002'),
|
|
12
|
+
LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace']).default('info'),
|
|
13
|
+
|
|
14
|
+
// Database
|
|
15
|
+
MONGODB_URI: z.string().default('mongodb://localhost:27017/builder'),
|
|
16
|
+
BIO_MONGODB_URI: z.string().default('mongodb://localhost:27017/bio'),
|
|
17
|
+
REDIS_URL: z.string().default('redis://localhost:6379'),
|
|
18
|
+
|
|
19
|
+
// Database provisioning hosts (used by db-provisioner for tenant DBs)
|
|
20
|
+
DB_MONGODB_ADMIN_URI: z.string().default(''), // Admin connection for per-service credential management
|
|
21
|
+
DB_MONGODB_HOST: z.string().default('localhost'),
|
|
22
|
+
DB_MONGODB_PUBLIC_HOST: z.string().default(''), // Public IP for external DB connections (whitelist feature)
|
|
23
|
+
DB_MONGODB_PORT: z.string().transform(Number).default('27017'),
|
|
24
|
+
DB_REDIS_ADMIN_URL: z.string().default(''), // Admin connection for per-service Redis ACL management
|
|
25
|
+
DB_REDIS_HOST: z.string().default('localhost'),
|
|
26
|
+
DB_REDIS_PORT: z.string().transform(Number).default('6379'),
|
|
27
|
+
DB_NEO4J_ADMIN_URI: z.string().default(''), // Admin connection for per-service Neo4j auth management
|
|
28
|
+
DB_NEO4J_ADMIN_USER: z.string().default(''),
|
|
29
|
+
DB_NEO4J_ADMIN_PASSWORD: z.string().default(''),
|
|
30
|
+
DB_NEO4J_HOST: z.string().default('localhost'),
|
|
31
|
+
DB_NEO4J_PORT: z.string().transform(Number).default('7687'),
|
|
32
|
+
|
|
33
|
+
// MinIO object storage (Vault-managed — admin creds live in Vault KV only)
|
|
34
|
+
MINIO_HOST: z.string().default('64.23.181.20'),
|
|
35
|
+
MINIO_PORT: z.string().transform(Number).default('9000'),
|
|
36
|
+
|
|
37
|
+
// Koko integration
|
|
38
|
+
KOKO_URL: z.string().default('https://koko.tawa.insureco.io'),
|
|
39
|
+
|
|
40
|
+
// Bio-ID OAuth provider
|
|
41
|
+
BIO_ID_URL: z.string().default('https://bio.tawa.insureco.io'),
|
|
42
|
+
BIO_INTERNAL_KEY: z.string().default(''),
|
|
43
|
+
|
|
44
|
+
// Build configuration
|
|
45
|
+
WORKSPACE_DIR: z.string().default('/tmp/iec-builds'),
|
|
46
|
+
DOCKER_REGISTRY: z.string().default('registry.insureco.io/insureco'),
|
|
47
|
+
DOCKER_REGISTRY_USER: z.string().default(''), // Username for Docker registry auth
|
|
48
|
+
DOCKER_REGISTRY_TOKEN: z.string().default(''), // Token/password for Docker registry auth
|
|
49
|
+
|
|
50
|
+
// Tarball upload limits
|
|
51
|
+
MAX_UPLOAD_SIZE_MB: z.string().transform(Number).default('500'),
|
|
52
|
+
|
|
53
|
+
// Framework detection and Dockerfile generation
|
|
54
|
+
ENABLE_AUTO_DOCKERFILE: z.string().default('true'),
|
|
55
|
+
|
|
56
|
+
// Catalog version enforcement (reject deploys below minimum catalog schema version)
|
|
57
|
+
ENFORCE_MINIMUM_CATALOG_VERSION: z.string().default('false'),
|
|
58
|
+
|
|
59
|
+
// Kubernetes
|
|
60
|
+
KUBECONFIG: z.string().default(''),
|
|
61
|
+
K8S_NAMESPACE: z.string().default('iec-platform'),
|
|
62
|
+
ENABLE_K8S_DEPLOY: z.string().default('false'),
|
|
63
|
+
|
|
64
|
+
// Cloudflare DNS
|
|
65
|
+
CLOUDFLARE_API_TOKEN: z.string().default(''),
|
|
66
|
+
CLOUDFLARE_ZONE_ID: z.string().default('0945f68a0f28faa9d93721b572586737'),
|
|
67
|
+
INGRESS_TARGET: z.string().default(''),
|
|
68
|
+
ENABLE_DNS_MANAGEMENT: z.string().default('false'),
|
|
69
|
+
|
|
70
|
+
// Forgejo (git.insureco.io) access for cloning private repos
|
|
71
|
+
FORGEJO_URL: z.string().default('https://git.insureco.io'),
|
|
72
|
+
FORGEJO_TOKEN: z.string().default(''),
|
|
73
|
+
FORGEJO_ADMIN_USER: z.string().default(''),
|
|
74
|
+
FORGEJO_ADMIN_PASSWORD: z.string().default(''),
|
|
75
|
+
|
|
76
|
+
// GitHub access for cloning private repos (PAT with repo scope)
|
|
77
|
+
GITHUB_TOKEN: z.string().default(''),
|
|
78
|
+
|
|
79
|
+
// Webhook secrets (for verifying GitHub/GitLab signatures)
|
|
80
|
+
GITHUB_WEBHOOK_SECRET: z.string().default(''),
|
|
81
|
+
GITLAB_WEBHOOK_SECRET: z.string().default(''),
|
|
82
|
+
|
|
83
|
+
// Wallet (deploy gate)
|
|
84
|
+
WALLET_URL: z.string().default(''),
|
|
85
|
+
INTERNAL_SERVICE_KEY: z.string().default(''),
|
|
86
|
+
|
|
87
|
+
// Build worker concurrency (max parallel builds)
|
|
88
|
+
BUILD_CONCURRENCY: z.string().transform(Number).default('3'),
|
|
89
|
+
|
|
90
|
+
// HashiCorp Vault (Phase 2: dynamic credentials)
|
|
91
|
+
VAULT_ADDR: z.string().default(''), // Vault API address (e.g., http://vault.vault.svc.cluster.local:8200)
|
|
92
|
+
VAULT_TOKEN: z.string().default(''), // Builder's Vault token for role management
|
|
93
|
+
VAULT_ENABLED: z.string().default('false'), // Feature flag: 'true' to use Vault dynamic credentials
|
|
94
|
+
|
|
95
|
+
// Credential rotation
|
|
96
|
+
ROTATION_TTL_DAYS: z.string().transform(Number).default('30'),
|
|
97
|
+
RELAY_DIRECT_URL: z.string().default(''), // InsureRelay URL for rotation notifications
|
|
98
|
+
RELAY_INTERNAL_KEY: z.string().default(''), // Auth key for relay service
|
|
99
|
+
|
|
100
|
+
// AI proxy
|
|
101
|
+
ANTHROPIC_API_KEY: z.string().default(''),
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
export type Env = z.infer<typeof envSchema>
|
|
105
|
+
|
|
106
|
+
export function loadEnv(): Env {
|
|
107
|
+
const result = envSchema.safeParse(process.env)
|
|
108
|
+
|
|
109
|
+
if (!result.success) {
|
|
110
|
+
const formatted = result.error.format()
|
|
111
|
+
throw new Error(`Invalid environment configuration: ${JSON.stringify(formatted, null, 2)}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result.data
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const env = loadEnv()
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import { pinoHttp } from 'pino-http'
|
|
3
|
+
import { env } from './config/env.js'
|
|
4
|
+
import { logger } from './utils/logger.js'
|
|
5
|
+
import { apiError } from './utils/response.js'
|
|
6
|
+
import { authenticate } from './middleware/auth.js'
|
|
7
|
+
import { connectDatabase, connectRedis, checkConnections, closeConnections } from './services/database.js'
|
|
8
|
+
import { recoverStaleBuilds } from './services/builder.js'
|
|
9
|
+
import { initBuildQueue, initBuildWorker, closeBuildQueue } from './services/build-queue.js'
|
|
10
|
+
import { webhooksRouter } from './routes/webhooks.js'
|
|
11
|
+
import { buildsRouter } from './routes/builds.js'
|
|
12
|
+
import { servicesRouter } from './routes/services.js'
|
|
13
|
+
import { oauthRouter } from './routes/oauth.js'
|
|
14
|
+
import { configRouter } from './routes/config.js'
|
|
15
|
+
import { aiRouter } from './routes/ai.js'
|
|
16
|
+
import { usersRouter } from './routes/users.js'
|
|
17
|
+
import { domainsRouter } from './routes/domains.js'
|
|
18
|
+
import { databasesRouter } from './routes/databases.js'
|
|
19
|
+
import { clusterRouter } from './routes/cluster.js'
|
|
20
|
+
import { orgsRouter } from './routes/orgs.js'
|
|
21
|
+
import { dbWhitelistRouter } from './routes/db-whitelist.js'
|
|
22
|
+
import { storageRouter } from './routes/storage.js'
|
|
23
|
+
import { rotationRouter, rotationDashboardRouter } from './routes/rotation.js'
|
|
24
|
+
import { observabilityRouter } from './routes/observability.js'
|
|
25
|
+
import { auditRouter } from './routes/audit.js'
|
|
26
|
+
import { platformRouter } from './routes/platform.js'
|
|
27
|
+
import { reconcileOnStartup } from './services/db-whitelist.js'
|
|
28
|
+
import { startRotationScheduler } from './services/rotation-scheduler.js'
|
|
29
|
+
|
|
30
|
+
const app = express()
|
|
31
|
+
|
|
32
|
+
// Middleware
|
|
33
|
+
app.use(express.json({ limit: `${env.MAX_UPLOAD_SIZE_MB}mb` }))
|
|
34
|
+
app.use(pinoHttp({ logger }))
|
|
35
|
+
|
|
36
|
+
// Health check
|
|
37
|
+
app.get('/health', (_req, res) => {
|
|
38
|
+
res.json({ status: 'ok', service: 'iec-builder' })
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
// Readiness check
|
|
42
|
+
app.get('/ready', async (_req, res) => {
|
|
43
|
+
const connections = await checkConnections()
|
|
44
|
+
if (connections.mongodb && connections.redis) {
|
|
45
|
+
res.json({ status: 'ready', connections })
|
|
46
|
+
} else {
|
|
47
|
+
res.status(503).json({ status: 'not ready', connections })
|
|
48
|
+
}
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
// Routes
|
|
52
|
+
app.use('/webhooks', webhooksRouter)
|
|
53
|
+
app.use('/builds', authenticate, buildsRouter)
|
|
54
|
+
app.use('/services', authenticate, servicesRouter)
|
|
55
|
+
app.use('/oauth', authenticate, oauthRouter)
|
|
56
|
+
app.use('/services', authenticate, configRouter) // Config sub-routes: /services/:name/config
|
|
57
|
+
app.use('/ai', authenticate, aiRouter)
|
|
58
|
+
app.use('/users', authenticate, usersRouter)
|
|
59
|
+
app.use('/domains', authenticate, domainsRouter)
|
|
60
|
+
app.use('/services', authenticate, databasesRouter) // Database credential sub-routes: /services/:name/databases
|
|
61
|
+
app.use('/cluster', authenticate, clusterRouter)
|
|
62
|
+
app.use('/orgs', authenticate, orgsRouter)
|
|
63
|
+
app.use('/services', authenticate, dbWhitelistRouter) // DB whitelist sub-routes: /services/:name/databases/whitelist
|
|
64
|
+
app.use('/databases', authenticate, dbWhitelistRouter) // Global whitelist: /databases/whitelist
|
|
65
|
+
app.use('/services', authenticate, storageRouter) // Storage sub-routes: /services/:name/storage
|
|
66
|
+
app.use('/services', authenticate, rotationRouter) // Rotation sub-routes: /services/:name/rotation
|
|
67
|
+
app.use('/rotation', authenticate, rotationDashboardRouter) // Platform-wide: /rotation/upcoming
|
|
68
|
+
app.use('/services', authenticate, observabilityRouter) // Observability sub-routes: /services/:name/pods, /pods/logs, /troubleshoot
|
|
69
|
+
app.use('/audit', authenticate, auditRouter) // Audit log: /audit/events
|
|
70
|
+
app.use('/platform', authenticate, platformRouter) // Platform admin: /platform/dashboard
|
|
71
|
+
|
|
72
|
+
// Error handler
|
|
73
|
+
app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
74
|
+
logger.error({ err }, 'Unhandled error')
|
|
75
|
+
res.status(500).json(apiError('INTERNAL_ERROR', 'Internal server error'))
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Start server
|
|
79
|
+
async function start(): Promise<void> {
|
|
80
|
+
try {
|
|
81
|
+
logger.info('Connecting to MongoDB...')
|
|
82
|
+
await connectDatabase()
|
|
83
|
+
logger.info('MongoDB connected ✓')
|
|
84
|
+
logger.info('GridFS initialized for tarball storage ✓')
|
|
85
|
+
|
|
86
|
+
logger.info('Connecting to Redis...')
|
|
87
|
+
await connectRedis()
|
|
88
|
+
logger.info('Redis connected ✓')
|
|
89
|
+
|
|
90
|
+
// Recover builds left in active states from previous crash/restart
|
|
91
|
+
const recovered = await recoverStaleBuilds(0)
|
|
92
|
+
if (recovered > 0) {
|
|
93
|
+
logger.info({ recovered }, 'Recovered stale builds on startup')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Initialize BullMQ build queue and worker
|
|
97
|
+
await initBuildQueue()
|
|
98
|
+
initBuildWorker()
|
|
99
|
+
|
|
100
|
+
// Graceful shutdown: drain active builds before exiting
|
|
101
|
+
const shutdown = async () => {
|
|
102
|
+
logger.info('Shutdown signal received — draining builds...')
|
|
103
|
+
await closeBuildQueue()
|
|
104
|
+
await closeConnections()
|
|
105
|
+
process.exit(0)
|
|
106
|
+
}
|
|
107
|
+
process.once('SIGTERM', shutdown)
|
|
108
|
+
process.once('SIGINT', shutdown)
|
|
109
|
+
|
|
110
|
+
// Reconcile ipset whitelist entries with DB records
|
|
111
|
+
reconcileOnStartup().catch(err => {
|
|
112
|
+
logger.error({ err }, 'Whitelist reconciliation failed')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
// Start credential rotation scheduler
|
|
116
|
+
startRotationScheduler().catch(err => {
|
|
117
|
+
logger.error({ err }, 'Rotation scheduler crashed')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
// Validate critical env vars at startup
|
|
121
|
+
const criticalWarnings: string[] = []
|
|
122
|
+
if (!process.env.CONFIG_ENCRYPTION_KEY) criticalWarnings.push('CONFIG_ENCRYPTION_KEY not set — managed secrets will fail')
|
|
123
|
+
if (!env.BIO_INTERNAL_KEY) criticalWarnings.push('BIO_INTERNAL_KEY not set — OAuth provisioning will fail')
|
|
124
|
+
if (!env.CLOUDFLARE_API_TOKEN) criticalWarnings.push('CLOUDFLARE_API_TOKEN not set — DNS management will fail')
|
|
125
|
+
if (!env.KUBECONFIG) criticalWarnings.push('KUBECONFIG not set — K8s deployments may fail')
|
|
126
|
+
if (!env.DB_MONGODB_ADMIN_URI) criticalWarnings.push('DB_MONGODB_ADMIN_URI not set — MongoDB credentials will not be provisioned')
|
|
127
|
+
if (!env.DB_REDIS_ADMIN_URL) criticalWarnings.push('DB_REDIS_ADMIN_URL not set — Redis credentials will not be provisioned')
|
|
128
|
+
if (!env.DB_NEO4J_ADMIN_URI) criticalWarnings.push('DB_NEO4J_ADMIN_URI not set — Neo4j credentials will not be provisioned')
|
|
129
|
+
if (env.VAULT_ENABLED !== 'true') criticalWarnings.push('VAULT_ENABLED not set — storage provisioning (MinIO) will not be available')
|
|
130
|
+
for (const warning of criticalWarnings) {
|
|
131
|
+
logger.warn(`STARTUP: ${warning}`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
app.listen(env.PORT, () => {
|
|
135
|
+
logger.info(`iec-builder running on port ${env.PORT}`)
|
|
136
|
+
logger.info(`Environment: ${env.NODE_ENV}`)
|
|
137
|
+
logger.info(`Workspace: ${env.WORKSPACE_DIR}`)
|
|
138
|
+
logger.info(`Registry: ${env.DOCKER_REGISTRY}`)
|
|
139
|
+
logger.info(`K8s deploy: ${env.ENABLE_K8S_DEPLOY}`)
|
|
140
|
+
logger.info(`DNS management: ${env.ENABLE_DNS_MANAGEMENT}`)
|
|
141
|
+
logger.info(`Auto-Dockerfile: ${env.ENABLE_AUTO_DOCKERFILE}`)
|
|
142
|
+
logger.info(`Max upload size: ${env.MAX_UPLOAD_SIZE_MB}MB`)
|
|
143
|
+
if (criticalWarnings.length > 0) {
|
|
144
|
+
logger.warn(`${criticalWarnings.length} critical warning(s) — check logs above`)
|
|
145
|
+
}
|
|
146
|
+
})
|
|
147
|
+
} catch (error) {
|
|
148
|
+
logger.error({ error }, 'Failed to start server')
|
|
149
|
+
process.exit(1)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
start()
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express'
|
|
2
|
+
import { env } from '../config/env.js'
|
|
3
|
+
import { logger } from '../utils/logger.js'
|
|
4
|
+
import { apiError } from '../utils/response.js'
|
|
5
|
+
|
|
6
|
+
export interface AuthenticatedUser {
|
|
7
|
+
id: string
|
|
8
|
+
email: string
|
|
9
|
+
name?: string
|
|
10
|
+
roles: string[]
|
|
11
|
+
org?: string // Org slug (for service ownership, wallet derivation)
|
|
12
|
+
orgId?: string // Canonical ORG-xxx identifier (for Bio-ID admin calls)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type OrgRole = 'viewer' | 'member' | 'admin'
|
|
16
|
+
|
|
17
|
+
export const ORG_ROLE_HIERARCHY: Record<OrgRole, number> = {
|
|
18
|
+
viewer: 1,
|
|
19
|
+
member: 2,
|
|
20
|
+
admin: 3,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Derive the effective org role from a user's roles array.
|
|
25
|
+
* Explicit org:admin > admin > org:member > member > default viewer
|
|
26
|
+
*/
|
|
27
|
+
export function deriveOrgRole(roles: string[]): OrgRole {
|
|
28
|
+
if (roles.includes('org:admin') || roles.includes('admin')) return 'admin'
|
|
29
|
+
if (roles.includes('org:member') || roles.includes('member')) return 'member'
|
|
30
|
+
if (roles.includes('org:viewer') || roles.includes('viewer')) return 'viewer'
|
|
31
|
+
return 'viewer'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AuthenticatedRequest extends Request {
|
|
35
|
+
user?: AuthenticatedUser
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate Bearer token from Authorization header
|
|
40
|
+
* Tries Koko CLI token verification first, then bio-id JWT introspection
|
|
41
|
+
*/
|
|
42
|
+
export async function validateToken(
|
|
43
|
+
req: AuthenticatedRequest,
|
|
44
|
+
res: Response,
|
|
45
|
+
next: NextFunction
|
|
46
|
+
): Promise<void> {
|
|
47
|
+
const authHeader = req.headers.authorization
|
|
48
|
+
|
|
49
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
50
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Missing or invalid authorization header'))
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const token = authHeader.substring(7)
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
// Try Koko CLI token verification first (iec CLI uses these)
|
|
58
|
+
const kokoUrl = env.KOKO_URL || 'https://koko.tawa.insureco.io'
|
|
59
|
+
const kokoResponse = await fetch(`${kokoUrl}/api/auth/verify`, {
|
|
60
|
+
headers: {
|
|
61
|
+
Authorization: `Bearer ${token}`,
|
|
62
|
+
},
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
if (kokoResponse.ok) {
|
|
66
|
+
const kokoResult = await kokoResponse.json() as {
|
|
67
|
+
success: boolean
|
|
68
|
+
data?: { id: string; email: string; name: string; org?: string; orgId?: string; roles?: string[] }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (kokoResult.success && kokoResult.data) {
|
|
72
|
+
req.user = {
|
|
73
|
+
id: kokoResult.data.id,
|
|
74
|
+
email: kokoResult.data.email,
|
|
75
|
+
name: kokoResult.data.name,
|
|
76
|
+
roles: kokoResult.data.roles || ['admin'],
|
|
77
|
+
org: kokoResult.data.org,
|
|
78
|
+
orgId: kokoResult.data.orgId,
|
|
79
|
+
}
|
|
80
|
+
next()
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fall back to bio-id JWT introspection
|
|
86
|
+
const bioIdUrl = env.BIO_ID_URL || 'https://bio.tawa.insureco.io'
|
|
87
|
+
const response = await fetch(`${bioIdUrl}/api/auth/introspect`, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: JSON.stringify({ token }),
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
logger.warn({ status: response.status }, 'Token validation failed (both Koko and bio-id)')
|
|
97
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Invalid or expired token'))
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = await response.json() as {
|
|
102
|
+
active: boolean
|
|
103
|
+
user?: AuthenticatedUser
|
|
104
|
+
org_id?: string // ORG-xxx (canonical)
|
|
105
|
+
org_slug?: string // URL-friendly slug
|
|
106
|
+
organization_name?: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!result.active || !result.user) {
|
|
110
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Token is not active'))
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Populate org (slug) and orgId (ORG-xxx) from introspection response.
|
|
115
|
+
const org = result.user.org
|
|
116
|
+
|| result.org_slug
|
|
117
|
+
const orgId = result.org_id
|
|
118
|
+
|
|
119
|
+
req.user = { ...result.user, org, orgId }
|
|
120
|
+
next()
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error({ error }, 'Token validation error')
|
|
123
|
+
res.status(500).json(apiError('AUTH_ERROR', 'Failed to validate token'))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Simple API key authentication for CLI tools
|
|
129
|
+
* API keys are scoped to services and stored in the service record
|
|
130
|
+
*/
|
|
131
|
+
export async function validateApiKey(
|
|
132
|
+
req: AuthenticatedRequest,
|
|
133
|
+
res: Response,
|
|
134
|
+
next: NextFunction
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const apiKey = req.headers['x-api-key'] as string
|
|
137
|
+
|
|
138
|
+
if (!apiKey) {
|
|
139
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Missing API key'))
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
// Validate API key format: iec_<service-id>_<random-bytes>
|
|
145
|
+
if (!apiKey.startsWith('iec_')) {
|
|
146
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Invalid API key format'))
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Extract service ID from API key
|
|
151
|
+
const parts = apiKey.split('_')
|
|
152
|
+
if (parts.length < 3) {
|
|
153
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Invalid API key format'))
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const serviceId = parts[1]
|
|
158
|
+
|
|
159
|
+
// Validate with koko service registry
|
|
160
|
+
const kokoUrl = env.KOKO_URL || 'https://koko.tawa.insureco.io'
|
|
161
|
+
const response = await fetch(`${kokoUrl}/api/services/${serviceId}/validate-key`, {
|
|
162
|
+
method: 'POST',
|
|
163
|
+
headers: {
|
|
164
|
+
'Content-Type': 'application/json',
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({ apiKey }),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
if (!response.ok) {
|
|
170
|
+
logger.warn({ serviceId, status: response.status }, 'API key validation failed')
|
|
171
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Invalid API key'))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const result = await response.json() as { valid: boolean; serviceId: string; serviceName: string }
|
|
176
|
+
|
|
177
|
+
if (!result.valid) {
|
|
178
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'API key is not valid'))
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Set user info from API key validation
|
|
183
|
+
const user: AuthenticatedUser = {
|
|
184
|
+
id: `service:${result.serviceId}`,
|
|
185
|
+
email: `${result.serviceName}@services.iec`,
|
|
186
|
+
name: result.serviceName,
|
|
187
|
+
roles: ['service'],
|
|
188
|
+
}
|
|
189
|
+
req.user = user
|
|
190
|
+
|
|
191
|
+
// Store service info for downstream use
|
|
192
|
+
;(req as Request & { serviceId: string }).serviceId = result.serviceId
|
|
193
|
+
|
|
194
|
+
next()
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error({ error }, 'API key validation error')
|
|
197
|
+
res.status(500).json(apiError('AUTH_ERROR', 'Failed to validate API key'))
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Combined auth middleware - accepts internal key, Bearer token, or API key.
|
|
203
|
+
* Internal key is used for trusted platform services (tawa-web, etc).
|
|
204
|
+
*/
|
|
205
|
+
export async function authenticate(
|
|
206
|
+
req: AuthenticatedRequest,
|
|
207
|
+
res: Response,
|
|
208
|
+
next: NextFunction
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
// Platform service-to-service auth via shared internal key
|
|
211
|
+
const internalKey = req.headers['x-internal-key'] as string
|
|
212
|
+
if (internalKey && env.INTERNAL_SERVICE_KEY && internalKey === env.INTERNAL_SERVICE_KEY) {
|
|
213
|
+
req.user = {
|
|
214
|
+
id: 'service:platform',
|
|
215
|
+
email: 'platform@internal.iec',
|
|
216
|
+
name: 'Platform Service',
|
|
217
|
+
roles: ['admin', 'service'],
|
|
218
|
+
}
|
|
219
|
+
next()
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const authHeader = req.headers.authorization
|
|
224
|
+
const apiKey = req.headers['x-api-key']
|
|
225
|
+
|
|
226
|
+
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
227
|
+
return validateToken(req, res, next)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (apiKey) {
|
|
231
|
+
return validateApiKey(req, res, next)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'))
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Require specific roles
|
|
239
|
+
*/
|
|
240
|
+
export function requireRoles(...roles: string[]) {
|
|
241
|
+
return (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
|
|
242
|
+
if (!req.user) {
|
|
243
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'))
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const hasRole = roles.some(role => req.user!.roles.includes(role))
|
|
248
|
+
if (!hasRole) {
|
|
249
|
+
res.status(403).json(apiError('FORBIDDEN', `Required roles: ${roles.join(', ')}`))
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
next()
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Validate webhook signature from GitHub
|
|
259
|
+
*/
|
|
260
|
+
export function validateGitHubWebhook(
|
|
261
|
+
req: Request,
|
|
262
|
+
res: Response,
|
|
263
|
+
next: NextFunction
|
|
264
|
+
): void {
|
|
265
|
+
const signature = req.headers['x-hub-signature-256'] as string
|
|
266
|
+
const secret = env.GITHUB_WEBHOOK_SECRET
|
|
267
|
+
|
|
268
|
+
if (!secret) {
|
|
269
|
+
logger.warn('GitHub webhook secret not configured')
|
|
270
|
+
next()
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!signature) {
|
|
275
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Missing webhook signature'))
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const payload = JSON.stringify(req.body)
|
|
280
|
+
const expectedSignature = `sha256=${createHmacSignature(payload, secret)}`
|
|
281
|
+
|
|
282
|
+
if (signature !== expectedSignature) {
|
|
283
|
+
logger.warn('Invalid GitHub webhook signature')
|
|
284
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Invalid webhook signature'))
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
next()
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function createHmacSignature(payload: string, secret: string): string {
|
|
292
|
+
const { createHmac } = require('crypto')
|
|
293
|
+
return createHmac('sha256', secret).update(payload).digest('hex')
|
|
294
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Response, NextFunction } from 'express'
|
|
2
|
+
import { getServicesCollection } from '../services/database.js'
|
|
3
|
+
import { apiError } from '../utils/response.js'
|
|
4
|
+
import { logger } from '../utils/logger.js'
|
|
5
|
+
import type { AuthenticatedRequest, OrgRole } from './auth.js'
|
|
6
|
+
import { deriveOrgRole, ORG_ROLE_HIERARCHY } from './auth.js'
|
|
7
|
+
import type { Service } from '../models/types.js'
|
|
8
|
+
|
|
9
|
+
export interface ServiceScopedRequest extends AuthenticatedRequest {
|
|
10
|
+
service?: Service
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Extract a service identifier from the request.
|
|
15
|
+
* Checks route params first, then request body.
|
|
16
|
+
*/
|
|
17
|
+
function extractServiceIdentifier(req: ServiceScopedRequest): string | undefined {
|
|
18
|
+
return (
|
|
19
|
+
req.params.serviceId ||
|
|
20
|
+
req.params.serviceName ||
|
|
21
|
+
req.body?.serviceId ||
|
|
22
|
+
req.body?.serviceName
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Middleware: Verify the authenticated user belongs to the same org as the service.
|
|
28
|
+
*
|
|
29
|
+
* - Loads the service from DB and attaches it to `req.service`
|
|
30
|
+
* - Super admins and platform services bypass the check
|
|
31
|
+
* - Legacy services without `org` allow access (will be backfilled)
|
|
32
|
+
*/
|
|
33
|
+
export function requireOrgAccess() {
|
|
34
|
+
return async (req: ServiceScopedRequest, res: Response, next: NextFunction): Promise<void> => {
|
|
35
|
+
if (!req.user) {
|
|
36
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'))
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Platform service and super_admin bypass
|
|
41
|
+
if (req.user.roles.includes('super_admin') || req.user.roles.includes('service')) {
|
|
42
|
+
next()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const serviceId = extractServiceIdentifier(req)
|
|
47
|
+
if (!serviceId) {
|
|
48
|
+
// Let the route handler deal with missing params
|
|
49
|
+
next()
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
const services = getServicesCollection()
|
|
55
|
+
const service = await services.findOne({
|
|
56
|
+
$or: [{ id: serviceId }, { name: serviceId }],
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (!service) {
|
|
60
|
+
res.status(404).json(apiError('NOT_FOUND', 'Service not found'))
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Legacy services without org: allow access, log for visibility
|
|
65
|
+
if (!service.org) {
|
|
66
|
+
logger.debug({ serviceName: service.name }, 'Service has no org set — allowing access (legacy)')
|
|
67
|
+
req.service = service
|
|
68
|
+
next()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Org match check
|
|
73
|
+
if (req.user.org !== service.org) {
|
|
74
|
+
res.status(403).json(apiError('FORBIDDEN', 'Service belongs to a different organization'))
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
req.service = service
|
|
79
|
+
next()
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.error({ error }, 'Error in requireOrgAccess middleware')
|
|
82
|
+
next(error)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Middleware: Require a minimum org role level.
|
|
89
|
+
*
|
|
90
|
+
* Role hierarchy: viewer (1) < member (2) < admin (3)
|
|
91
|
+
* Roles are derived from the user's JWT/token claims:
|
|
92
|
+
* - org:admin or admin → admin
|
|
93
|
+
* - org:member or member → member
|
|
94
|
+
* - org:viewer or viewer → viewer
|
|
95
|
+
* - default → viewer
|
|
96
|
+
*
|
|
97
|
+
* Super admins and platform services always pass.
|
|
98
|
+
*/
|
|
99
|
+
export function requireOrgRole(minimumRole: OrgRole) {
|
|
100
|
+
const minimumLevel = ORG_ROLE_HIERARCHY[minimumRole]
|
|
101
|
+
|
|
102
|
+
return (req: ServiceScopedRequest, res: Response, next: NextFunction): void => {
|
|
103
|
+
if (!req.user) {
|
|
104
|
+
res.status(401).json(apiError('UNAUTHORIZED', 'Authentication required'))
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Super admin and platform service bypass
|
|
109
|
+
if (req.user.roles.includes('super_admin') || req.user.roles.includes('service')) {
|
|
110
|
+
next()
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const effectiveRole = deriveOrgRole(req.user.roles)
|
|
115
|
+
const effectiveLevel = ORG_ROLE_HIERARCHY[effectiveRole]
|
|
116
|
+
|
|
117
|
+
if (effectiveLevel < minimumLevel) {
|
|
118
|
+
res.status(403).json(
|
|
119
|
+
apiError('FORBIDDEN', `Requires org role: ${minimumRole} (you have: ${effectiveRole})`)
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
next()
|
|
125
|
+
}
|
|
126
|
+
}
|