@tdk-landscape/tdk-cli-core 1.3.90 → 1.3.92

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 (36) hide show
  1. package/README.md +9 -0
  2. package/cli/dist/backend-frameworks/h3.js +38 -0
  3. package/cli/dist/backend-frameworks/koa.js +48 -0
  4. package/cli/dist/backend-frameworks/mcp.js +87 -0
  5. package/cli/dist/backend-frameworks/registry.js +20 -0
  6. package/cli/dist/commands/completion.js +3 -3
  7. package/cli/dist/commands/doctor.js +8 -2
  8. package/cli/dist/commands/networks.js +2 -1
  9. package/cli/dist/commands/project.js +15 -2
  10. package/cli/dist/commands/resource.js +20 -14
  11. package/cli/dist/commands/up.js +37 -1
  12. package/cli/dist/types/index.js +1 -0
  13. package/cli/dist/utils/constants.js +4 -0
  14. package/cli/dist/utils/doctor-runtime.js +3 -2
  15. package/cli/dist/utils/doctor-wiring.js +2 -1
  16. package/cli/dist/utils/env-validator.js +118 -30
  17. package/cli/dist/utils/resource-kind.js +8 -0
  18. package/cli/dist/utils/service-manifest.js +23 -3
  19. package/cli/dist/utils/service-urls.js +4 -3
  20. package/cli/dist/utils/services.js +2 -1
  21. package/cli/dist/utils/smoke.js +225 -0
  22. package/cli/package.json +1 -1
  23. package/engine/schemas/service-schema.json +73 -25
  24. package/engine/topologies/platform/docker/auth/auth.star +66 -28
  25. package/engine/topologies/platform/docker/compose/compose.star +8 -26
  26. package/engine/topologies/platform/docker/layers/l3_builder_layers.star +3 -0
  27. package/engine/topologies/platform/docker/layers/l4_runtime_layers.star +3 -0
  28. package/engine/topologies/platform/docker/networking/api_path_constants.star +1 -57
  29. package/engine/topologies/platform/docker/secrets/env_secrets.star +72 -0
  30. package/engine/topologies/tilt/common/utils.star +0 -2
  31. package/engine/topologies/tilt/common/utils_env.star +72 -25
  32. package/engine/topologies/tilt/manifest/constants.star +2 -0
  33. package/engine/topologies/tilt/manifest/json_to_yaml.py +4 -0
  34. package/engine/topologies/tilt/manifest/loader.star +7 -0
  35. package/engine/topologies/tilt/resources/orchestrator/generators/env.star +43 -96
  36. package/package.json +1 -1
@@ -107,7 +107,7 @@
107
107
  "type": "string"
108
108
  },
109
109
  "appType": {
110
- "description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React, Vue, Svelte, Preact, Lit, Solid, Qwik or Vanilla TypeScript, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library\n• bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
110
+ "description": "Resource type determining build strategy:\n• backend - Hono/Express API server\n• frontend - Vite single-page application (React, Vue, Svelte, Preact, Lit, Solid, Qwik or Vanilla TypeScript, see framework)\n• sdk - Shared library package\n• worker - Background job processor\n• migrator - Database migration runner\n• library - Shared code library\n• mcp - Model Context Protocol server (an HTTP service on Bun, run like a backend)\n• bring-your-own - Wrap an existing service. No app scaffold. Needs Dockerfile or --image.",
111
111
  "enum": [
112
112
  "backend",
113
113
  "frontend",
@@ -115,6 +115,7 @@
115
115
  "worker",
116
116
  "migrator",
117
117
  "library",
118
+ "mcp",
118
119
  "bring-your-own"
119
120
  ],
120
121
  "type": "string"
@@ -179,10 +180,10 @@
179
180
  "type": "object"
180
181
  },
181
182
  "secrets": {
182
- "description": "Secret references resolved via Infisical. NO secrets in params/envVars - security violation.",
183
+ "description": "Names of secrets the resource needs. By default each is read from the project .env (gitignored) and injected into the container; with TDK_SECRET_PROVIDER=infisical they are left to Infisical. NO secret values in params/envVars or anywhere in service.json.",
183
184
  "properties": {
184
185
  "required": {
185
- "description": "Required secret names. Resource CRASHES if any are missing.",
186
+ "description": "Required secret names. The resource does not start if any is missing from the project .env.",
186
187
  "items": {
187
188
  "type": "string",
188
189
  "pattern": "^[A-Z_][A-Z0-9_]*$"
@@ -191,7 +192,7 @@
191
192
  "uniqueItems": true
192
193
  },
193
194
  "optional": {
194
- "description": "Optional secret names. Resource starts but warns if missing.",
195
+ "description": "Optional secret names. The resource starts with the variable empty when it is missing from the project .env.",
195
196
  "items": {
196
197
  "type": "string",
197
198
  "pattern": "^[A-Z_][A-Z0-9_]*$"
@@ -226,32 +227,13 @@
226
227
  },
227
228
  "framework": {
228
229
  "description": "Framework provider id. For frontend resources it selects the Vite and TypeScript config (omitted means react, legacy manifests). For backend resources it selects the HTTP framework on the Bun runtime (omitted means hono; not used with a language that owns its runtime, such as python). Valid ids come from the CLI provider registries, which reject unknown ids before writing a resource.",
229
- "examples": [
230
- "react",
231
- "vue",
232
- "svelte",
233
- "preact",
234
- "lit",
235
- "solid",
236
- "qwik",
237
- "vanilla",
238
- "hono",
239
- "express",
240
- "elysia",
241
- "fastify",
242
- "nestjs"
243
- ],
230
+ "examples": ["react", "vue", "solid"],
244
231
  "pattern": "^[a-z][a-z0-9-]*$",
245
232
  "type": "string"
246
233
  },
247
234
  "language": {
248
235
  "description": "Backend language provider id used to scaffold and run the service. Only meaningful for backend resources; omitted means bun (legacy manifests). Valid ids come from the CLI provider registry.",
249
- "examples": [
250
- "bun",
251
- "python",
252
- "go",
253
- "rust"
254
- ],
236
+ "examples": ["bun", "python", "go"],
255
237
  "pattern": "^[a-z][a-z0-9-]*$",
256
238
  "type": "string"
257
239
  },
@@ -358,6 +340,72 @@
358
340
  },
359
341
  "type": "array"
360
342
  },
343
+ "smoke": {
344
+ "description": "Post-start check run by `tdk up` through the public proxy URL (not inside the container): write a record, read it back. `tdk up` fails if a step misses. Absent means no smoke check. Workers and resources with exposeViaProxy false are skipped.",
345
+ "type": "object",
346
+ "required": [
347
+ "via",
348
+ "steps"
349
+ ],
350
+ "additionalProperties": false,
351
+ "properties": {
352
+ "via": {
353
+ "const": "proxy",
354
+ "description": "Only `proxy` is supported: the request goes to the URL `tdk up` prints."
355
+ },
356
+ "timeoutSeconds": {
357
+ "type": "number",
358
+ "exclusiveMinimum": 0,
359
+ "default": 60,
360
+ "description": "How long to wait for the route to come up. Connection errors and 404/502/503/504 are retried until then; any other wrong answer fails at once."
361
+ },
362
+ "steps": {
363
+ "type": "array",
364
+ "minItems": 1,
365
+ "items": {
366
+ "type": "object",
367
+ "required": [
368
+ "path"
369
+ ],
370
+ "additionalProperties": false,
371
+ "properties": {
372
+ "name": {
373
+ "type": "string"
374
+ },
375
+ "method": {
376
+ "type": "string",
377
+ "default": "GET"
378
+ },
379
+ "path": {
380
+ "type": "string",
381
+ "pattern": "^/(?!/)",
382
+ "description": "Appended to the service's public base URL. `{{name}}` is replaced by a value saved by an earlier step."
383
+ },
384
+ "body": {
385
+ "description": "JSON request body."
386
+ },
387
+ "expect": {
388
+ "type": "integer",
389
+ "minimum": 100,
390
+ "maximum": 599,
391
+ "default": 200
392
+ },
393
+ "bodyContains": {
394
+ "type": "string"
395
+ },
396
+ "save": {
397
+ "type": "object",
398
+ "additionalProperties": {
399
+ "type": "string",
400
+ "pattern": "^\\$\\."
401
+ },
402
+ "description": "Saves response fields for later steps, for example {\"id\": \"$.id\"}."
403
+ }
404
+ }
405
+ }
406
+ }
407
+ }
408
+ },
361
409
  "traefik": {
362
410
  "description": "Custom Traefik routing. Auto-generated if not specified.",
363
411
  "properties": {
@@ -12,38 +12,80 @@
12
12
  # them as the auth_config dict.
13
13
  # =============================================================================
14
14
 
15
- # Default values for auth configuration
15
+ # Default values for auth configuration.
16
+ #
17
+ # There is deliberately no default JWT secret: a secret that is the same in every project is not a secret. The JWT secret is
18
+ # generated once per project by the CLI into the project `.env` (JWT_SECRET) and Compose reads it from there.
16
19
  DEFAULT_AUTH_MODE = 'local-jwt'
17
- DEFAULT_JWT_SECRET = 'local-development-secret-min-32-chars-long'
18
20
  DEFAULT_IDENTITY_RESOURCE_URL = 'http://identity-service:3000'
19
21
 
22
+ # Shown by Compose when the project `.env` has no JWT_SECRET.
23
+ JWT_SECRET_MISSING_MESSAGE = 'JWT_SECRET is not set. Run `tdk up` (it adds a generated one to the project .env) or add JWT_SECRET to .env'
24
+
25
+
26
+ def _setting(auth_config, key, default):
27
+ """A configured value, or the default when the key is missing OR present with a None value.
28
+
29
+ Callers build the config dict with every key present and None for anything unset, and `dict.get(key, default)` returns that
30
+ None instead of the default, which used to turn into the literal text `AUTH_MODE=None` in generated files.
31
+ """
32
+ if not auth_config:
33
+ return default
34
+ value = auth_config.get(key)
35
+ if value == None or value == '':
36
+ return default
37
+ return value
20
38
 
21
- def get_auth_mode(auth_config):
22
- """Get auth mode from auth config with default fallback.
39
+
40
+ def _first_set(*values):
41
+ """The first value that is not None or an empty string, else None."""
42
+ for value in values:
43
+ if value != None and value != '':
44
+ return value
45
+ return None
46
+
47
+
48
+ def auth_config_from_manifest(manifest):
49
+ """Build the auth configuration both the env file and the Compose entry use, from one place.
50
+
51
+ The two writers used to build this separately and drifted: the env file read `identityServiceUrl` while Compose read
52
+ `envVars.IDENTITY_RESOURCE_URL`, so setting one field changed only one output.
53
+
54
+ - authMode: the manifest's `authMode`, else `identity-service` when `dependsOn` contains `identity`, else unset (local-jwt).
55
+ - identityServiceUrl: the manifest's `identityServiceUrl`. The deprecated `params` / `envVars` entry named
56
+ `IDENTITY_RESOURCE_URL` is still honoured when the field is absent, so an existing manifest keeps working.
23
57
 
24
58
  Args:
25
- auth_config: Auth configuration dictionary (may be None or empty)
59
+ manifest: The service manifest dict (may be None)
26
60
 
27
61
  Returns:
28
- str: Auth mode ('local-jwt' or 'identity-service')
62
+ dict: {'authMode': str or None, 'identityServiceUrl': str or None}; None means "use the default"
29
63
  """
30
- if not auth_config:
31
- return DEFAULT_AUTH_MODE
32
- return auth_config.get('authMode', DEFAULT_AUTH_MODE)
64
+ if not manifest:
65
+ return {'authMode': None, 'identityServiceUrl': None}
66
+
67
+ auth_mode = _first_set(
68
+ manifest.get('authMode'),
69
+ 'identity-service' if 'identity' in (manifest.get('dependsOn') or []) else None,
70
+ )
71
+ identity_url = _first_set(
72
+ manifest.get('identityServiceUrl'),
73
+ (manifest.get('params') or {}).get('IDENTITY_RESOURCE_URL'),
74
+ (manifest.get('envVars') or {}).get('IDENTITY_RESOURCE_URL'),
75
+ )
76
+ return {'authMode': auth_mode, 'identityServiceUrl': identity_url}
33
77
 
34
78
 
35
- def get_jwt_secret(auth_config):
36
- """Get JWT secret from auth config with default fallback.
79
+ def get_auth_mode(auth_config):
80
+ """Get auth mode from auth config with default fallback.
37
81
 
38
82
  Args:
39
83
  auth_config: Auth configuration dictionary (may be None or empty)
40
84
 
41
85
  Returns:
42
- str: JWT secret for local-jwt mode
86
+ str: Auth mode ('local-jwt' or 'identity-service')
43
87
  """
44
- if not auth_config:
45
- return DEFAULT_JWT_SECRET
46
- return auth_config.get('jwtSecret', DEFAULT_JWT_SECRET)
88
+ return _setting(auth_config, 'authMode', DEFAULT_AUTH_MODE)
47
89
 
48
90
 
49
91
  def get_identity_resource_url(auth_config):
@@ -55,9 +97,7 @@ def get_identity_resource_url(auth_config):
55
97
  Returns:
56
98
  str: URL for identity service
57
99
  """
58
- if not auth_config:
59
- return DEFAULT_IDENTITY_RESOURCE_URL
60
- return auth_config.get('identityServiceUrl', DEFAULT_IDENTITY_RESOURCE_URL)
100
+ return _setting(auth_config, 'identityServiceUrl', DEFAULT_IDENTITY_RESOURCE_URL)
61
101
 
62
102
 
63
103
  def get_auth_config(auth_config):
@@ -67,11 +107,10 @@ def get_auth_config(auth_config):
67
107
  auth_config: Auth configuration dictionary (may be None or empty)
68
108
 
69
109
  Returns:
70
- struct: Auth configuration with mode, jwt_secret, and identity_resource_url
110
+ struct: Auth configuration with mode and identity_resource_url (the JWT secret lives in the project .env, not here)
71
111
  """
72
112
  return struct(
73
113
  mode = get_auth_mode(auth_config),
74
- jwt_secret = get_jwt_secret(auth_config),
75
114
  identity_resource_url = get_identity_resource_url(auth_config),
76
115
  )
77
116
 
@@ -113,12 +152,12 @@ def generate_docker_compose_auth_env(auth_config):
113
152
  str: Formatted auth environment variables for docker-compose
114
153
  """
115
154
  auth_mode = get_auth_mode(auth_config)
116
- jwt_secret = get_jwt_secret(auth_config)
117
155
  identity_resource_url = get_identity_resource_url(auth_config)
118
156
 
119
157
  if auth_mode == 'local-jwt':
158
+ # Required from the project .env (see JWT_SECRET_MISSING_MESSAGE); there is no shared default.
120
159
  return """ - AUTH_MODE=local-jwt
121
- - JWT_SECRET=${JWT_SECRET:-%(jwt_secret)s}""" % {'jwt_secret': jwt_secret}
160
+ - JWT_SECRET=${JWT_SECRET:?%(message)s}""" % {'message': JWT_SECRET_MISSING_MESSAGE}
122
161
  else:
123
162
  return """ - AUTH_MODE=identity-service
124
163
  - IDENTITY_RESOURCE_URL=${IDENTITY_RESOURCE_URL:-%(identity_resource_url)s}""" % {'identity_resource_url': identity_resource_url}
@@ -128,7 +167,9 @@ def generate_env_file_auth_section(auth_config):
128
167
  """Generate .env file auth section content.
129
168
 
130
169
  This function returns the formatted authentication configuration section
131
- for .env.backend.autogenerated files.
170
+ for .env.backend.autogenerated files. These files are generated, checked by
171
+ `tdk config verify` and never carry secrets, so there is no JWT secret here:
172
+ in local-jwt mode Compose takes JWT_SECRET from the project .env.
132
173
 
133
174
  Args:
134
175
  auth_config: Auth configuration dictionary (may be None or empty)
@@ -137,7 +178,6 @@ def generate_env_file_auth_section(auth_config):
137
178
  str: Formatted auth section for .env files
138
179
  """
139
180
  auth_mode = get_auth_mode(auth_config)
140
- jwt_secret = get_jwt_secret(auth_config)
141
181
  identity_resource_url = get_identity_resource_url(auth_config)
142
182
 
143
183
  content = """
@@ -146,9 +186,7 @@ def generate_env_file_auth_section(auth_config):
146
186
  AUTH_MODE={auth_mode}
147
187
  """.format(auth_mode=auth_mode)
148
188
 
149
- if auth_mode == 'local-jwt':
150
- content += "JWT_SECRET={jwt_secret}\n".format(jwt_secret=jwt_secret)
151
- else:
189
+ if auth_mode != 'local-jwt':
152
190
  content += "IDENTITY_RESOURCE_URL={identity_resource_url}\n".format(identity_resource_url=identity_resource_url)
153
191
 
154
192
  return content
@@ -156,8 +194,8 @@ AUTH_MODE={auth_mode}
156
194
 
157
195
  # Export all functions as a struct for easy importing
158
196
  AuthConfig = struct(
197
+ from_manifest = auth_config_from_manifest,
159
198
  get_auth_mode = get_auth_mode,
160
- get_jwt_secret = get_jwt_secret,
161
199
  get_identity_resource_url = get_identity_resource_url,
162
200
  get_auth_config = get_auth_config,
163
201
  is_local_jwt_mode = is_local_jwt_mode,
@@ -20,6 +20,7 @@ load("./compose_helpers.star", "with_runtime_image_tag", "compose_build_config")
20
20
  load("../networking/traefik.star", "get_frontend_traefik_labels", "get_backend_traefik_labels")
21
21
  load("../networking/api_path_constants.star", "get_api_path_for_domain", "get_frontend_base_path")
22
22
  load("../secrets/infisical.star", "get_infisical_environment_vars")
23
+ load("../secrets/env_secrets.star", "get_secret_environment_lines")
23
24
  load("../auth/auth.star", "AuthConfig")
24
25
 
25
26
  load("../constants.star", "PlatformDockerConstants")
@@ -189,32 +190,12 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
189
190
  nats_queue = nats_cfg.get('queueGroup', stack + '_backend_svc')
190
191
  nats_url = PlatformDockerConstants.NATS_URL
191
192
 
192
- # Auth configuration - use centralized auth utilities
193
- # Extract auth config from manifest to decouple auth.star from manifest structure
194
- # Auto-detect identity-service mode if 'identity' is in dependsOn
195
- internal_deps = manifest.get('dependsOn', []) if manifest else []
196
- has_identity_dep = 'identity' in internal_deps
197
-
198
- # If manifest has authMode, use it; otherwise auto-detect based on identity dependency
199
- manifest_auth_mode = manifest.get('authMode') if manifest else None
200
- if manifest_auth_mode:
201
- auth_mode = manifest_auth_mode
202
- elif has_identity_dep:
203
- auth_mode = 'identity-service'
204
- else:
205
- auth_mode = None
206
-
207
- auth_config = {
208
- 'authMode': auth_mode,
209
- 'jwtSecret': manifest.get('jwtSecret') if manifest else None,
210
- 'identityServiceUrl': manifest.get('envVars', {}).get('IDENTITY_RESOURCE_URL') if manifest else None,
211
- }
212
-
213
- # If we auto-detected identity-service mode but no URL is set, use default
214
- if auth_mode == 'identity-service' and not auth_config['identityServiceUrl']:
215
- auth_config['identityServiceUrl'] = AuthConfig.get_identity_resource_url(None)
193
+ # Auth configuration: one shared builder keeps this identical to the generated env file (see AuthConfig.from_manifest).
194
+ auth_config = AuthConfig.from_manifest(manifest)
216
195
 
217
196
  auth_env = AuthConfig.generate_docker_compose_auth_env(auth_config)
197
+ # secrets.required / secrets.optional, read from the project .env by Compose (empty with TDK_SECRET_PROVIDER=infisical)
198
+ secrets_env = get_secret_environment_lines(manifest, res_name)
218
199
 
219
200
  # Use .autogenerated/Dockerfile.app.autogenerated by default for build config (primary only)
220
201
  if manifest and manifest.get('appType') == 'bring-your-own':
@@ -240,7 +221,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
240
221
 
241
222
  # REMOVED: External port exposure - all services accessed via Traefik only
242
223
  # Services are available at {project}.localhost/api/v1/{service}
243
- # Internal port 3000 is accessible within Docker network for service-to-service communication
224
+ # Inside the Docker network a service listens on its own port (manifest.port, passed to it as PORT), for service-to-service calls
244
225
  ports_section = "" # No external ports - Traefik-only access
245
226
 
246
227
  # Determine service name based on instance
@@ -325,7 +306,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
325
306
  # to skip Prisma auto-migration at container boot (e.g. while diagnosing a
326
307
  # migration/CLI issue) without touching the generator or any service code.
327
308
  - AUTO_MIGRATE=${{AUTO_MIGRATE:-true}}
328
- {auth_env}
309
+ {auth_env}{secrets_env}
329
310
  labels:
330
311
  {traefik_labels}
331
312
  networks:
@@ -373,6 +354,7 @@ def _generate_single_backend_entry(resource_path, resource_name, res, manifest,
373
354
  healthcheck_section=healthcheck_section,
374
355
  infisical_env=infisical_env,
375
356
  auth_env=auth_env,
357
+ secrets_env=secrets_env,
376
358
  network_traefik_public=PlatformDockerConstants.NETWORK_TRAEFIK_PUBLIC,
377
359
  network_backend=PlatformDockerConstants.NETWORK_BACKEND,
378
360
  network_database=PlatformDockerConstants.NETWORK_DATABASE,
@@ -28,6 +28,9 @@ load('./prisma/prisma_build.star',
28
28
  'prisma_normalize_output',
29
29
  'L3_generate_migration_engine',
30
30
  )
31
+ # Infisical is opt-in: use_infisical defaults to False and nothing in the free engine passes True, so the generator behind this
32
+ # (tilt/generators/infisical/secrets_generator.star) is not on the default path. It is an older contract with fixed secret names and
33
+ # ports. Do not wire `secrets.required` through it; the default provider is platform/docker/secrets/env_secrets.star.
31
34
  load('./infisical/infisical_docker.star', 'InfisicalDocker')
32
35
 
33
36
  # Load project name for dynamic golden image naming
@@ -29,6 +29,9 @@ load('./prisma/prisma_runtime.star',
29
29
 
30
30
  load('./bun_helpers.star', 'bun_hoisted_packages_symlink_fix')
31
31
  load('../config/healthcheck.star', 'DOCKER_HEALTHCHECK', 'dockerfile_healthcheck_flags')
32
+ # Infisical is opt-in: use_infisical defaults to False and nothing in the free engine passes True, so the generator behind this
33
+ # (tilt/generators/infisical/secrets_generator.star) is not on the default path. It is an older contract with fixed secret names and
34
+ # ports. Do not wire `secrets.required` through it; the default provider is platform/docker/secrets/env_secrets.star.
32
35
  load('./infisical/infisical_docker.star', 'InfisicalDocker')
33
36
 
34
37
 
@@ -33,21 +33,6 @@ def generate_api_path(stack, app_name):
33
33
  # Examples: /api/user-management, /api/order-management
34
34
  return "/api/" + stack + "-management"
35
35
 
36
- # =============================================================================
37
- # SERVICE STACK TO API PATH MAPPING (Dynamic)
38
- # =============================================================================
39
- # This is populated at runtime from discovered manifests
40
- # No hardcoded resource names
41
-
42
- RESOURCE_STACK_TO_API_PATH = {}
43
-
44
- # =============================================================================
45
- # API PATH TO SERVICE MAPPING (Dynamic)
46
- # =============================================================================
47
-
48
- API_PATH_TO_RESOURCE_STACK = {}
49
- API_PATH_TO_RESOURCE_DOMAIN = API_PATH_TO_RESOURCE_STACK
50
-
51
36
  # =============================================================================
52
37
  # HELPER FUNCTIONS
53
38
  # =============================================================================
@@ -110,7 +95,7 @@ def get_api_path_for_stack(stack, manifest=None):
110
95
  return manifest.get("apiPath")
111
96
 
112
97
  # NEW: Use /api/{stack}-management pattern instead of /api/v1/{pluralized}
113
- return RESOURCE_STACK_TO_API_PATH.get(stack, "/api/" + stack + "-management")
98
+ return "/api/" + stack + "-management"
114
99
 
115
100
  # Backwards compatibility alias
116
101
  get_api_path_for_domain = get_api_path_for_stack
@@ -176,20 +161,6 @@ def get_api_path_for_resource(resource_name):
176
161
  # Backwards compatibility alias
177
162
  get_api_path_for_service = get_api_path_for_resource
178
163
 
179
- def get_stack_for_api_path(api_path):
180
- """Returns the service stack for an API path (reverse lookup).
181
-
182
- Args:
183
- api_path: Full API path
184
-
185
- Returns:
186
- Service stack string
187
- """
188
- return API_PATH_TO_RESOURCE_STACK.get(api_path, "")
189
-
190
- # Backwards compatibility alias
191
- get_domain_for_api_path = get_stack_for_api_path
192
-
193
164
  # Load project name for dynamic localhost domain
194
165
  # Use TDK_PROJECT_ROOT env var set by Tilt, fallback to current directory
195
166
  def _load_project_localhost():
@@ -231,30 +202,3 @@ def build_health_endpoint(api_path, health_path="/health"):
231
202
  Full health endpoint path
232
203
  """
233
204
  return api_path + health_path
234
-
235
- def get_all_api_paths():
236
- """Returns a list of all defined API paths from discovered services.
237
-
238
- Returns:
239
- List of API path strings
240
- """
241
- return RESOURCE_DOMAIN_TO_API_PATH.values()
242
-
243
- def is_valid_api_path(api_path):
244
- """Checks if an API path is valid/defined.
245
-
246
- Args:
247
- api_path: API path to check
248
-
249
- Returns:
250
- True if valid, False otherwise
251
- """
252
- return api_path in API_PATH_TO_RESOURCE_DOMAIN
253
-
254
- # =============================================================================
255
- # LEGACY COMPATIBILITY (Deprecated - for migration only)
256
- # =============================================================================
257
- # All legacy paths removed - use dynamic path generation from manifests
258
-
259
- LEGACY_DOMAIN_TO_API_PATH = {}
260
- RESOURCE_DOMAIN_TO_API_PATH = LEGACY_DOMAIN_TO_API_PATH
@@ -0,0 +1,72 @@
1
+ # =============================================================================
2
+ # 🔑 SECRETS FROM THE PROJECT .env
3
+ # =============================================================================
4
+ # Purpose: Make `secrets.required` / `secrets.optional` in service.json real for local development.
5
+ #
6
+ # The default provider is `env-file`: a secret's value lives in the project `.env` (gitignored) and reaches the container through
7
+ # Docker Compose's own `${NAME}` interpolation, so nothing secret is ever written to a generated, committed file. A required secret
8
+ # uses `${NAME:?message}`, so Compose refuses to start the service when the name is missing; an optional one defaults to empty.
9
+ #
10
+ # `TDK_SECRET_PROVIDER=infisical` leaves the names to Infisical instead, and nothing is injected here.
11
+ # =============================================================================
12
+
13
+ DEFAULT_SECRET_PROVIDER = 'env-file'
14
+
15
+ _NAME_FIRST = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'
16
+ _NAME_REST = _NAME_FIRST + '0123456789'
17
+
18
+
19
+ def get_secret_provider():
20
+ """The selected provider: `env-file` (default) or `infisical`."""
21
+ provider = str(os.environ.get('TDK_SECRET_PROVIDER', '')).strip().lower()
22
+ return provider if provider else DEFAULT_SECRET_PROVIDER
23
+
24
+
25
+ def _is_valid_env_name(name):
26
+ if type(name) != 'string' or not name:
27
+ return False
28
+ if name[0] not in _NAME_FIRST:
29
+ return False
30
+ for i in range(len(name)):
31
+ if name[i] not in _NAME_REST:
32
+ return False
33
+ return True
34
+
35
+
36
+ def _names(manifest, resource_name, kind):
37
+ secrets = manifest.get('secrets') or {}
38
+ names = secrets.get(kind) or []
39
+ if type(names) != 'list':
40
+ fail("{}: secrets.{} must be a list of environment variable names".format(resource_name, kind))
41
+ for name in names:
42
+ if not _is_valid_env_name(name):
43
+ fail("{}: invalid secret name '{}' in secrets.{}: use letters, digits and underscores, not starting with a digit".format(resource_name, name, kind))
44
+ return names
45
+
46
+
47
+ def get_secret_environment_lines(manifest, resource_name):
48
+ """Compose `environment:` list lines (each starting with a newline) for a resource's secrets, or '' when there are none.
49
+
50
+ Args:
51
+ manifest: The service manifest (reads secrets.required and secrets.optional)
52
+ resource_name: Used in error messages and in the text Compose shows for a missing required secret
53
+
54
+ Returns:
55
+ str: Lines to append to the Compose environment block
56
+ """
57
+ if not manifest or not manifest.get('secrets'):
58
+ return ''
59
+
60
+ required = _names(manifest, resource_name, 'required')
61
+ optional = _names(manifest, resource_name, 'optional')
62
+
63
+ if get_secret_provider() != DEFAULT_SECRET_PROVIDER:
64
+ return ''
65
+
66
+ lines = ''
67
+ for name in required:
68
+ lines += '\n - {name}=${{{name}:?{name} is required by {resource}. Add it to the project .env}}'.format(name=name, resource=resource_name)
69
+ for name in optional:
70
+ if name not in required:
71
+ lines += '\n - {name}=${{{name}:-}}'.format(name=name)
72
+ return lines
@@ -7,7 +7,6 @@
7
7
 
8
8
  load('./utils_env.star',
9
9
  'load_dotenv',
10
- 'validate_infisical_environment',
11
10
  'should_enable',
12
11
  )
13
12
  load('./utils_debug.star',
@@ -371,7 +370,6 @@ Utils = struct(
371
370
 
372
371
  # Environment
373
372
  load_dotenv = load_dotenv,
374
- validate_infisical_environment = validate_infisical_environment,
375
373
  should_enable = should_enable,
376
374
 
377
375
  # Debug utilities