@monster-orange/den 0.0.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. package/src/ui/server/workspace-routes.ts +2185 -0
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Validate a repo's manifest layer by layer: the base (repo or held), the
3
+ * override, then the merged result. Each layer's errors name its file.
4
+ *
5
+ * The override is partial, so it is not checked against the whole schema
6
+ * (it need not repeat `name`, say). Instead each value it sets is checked
7
+ * against the schema at that path, and every key it names must exist in the
8
+ * base (see overrideKeyErrors).
9
+ *
10
+ * The schema and the single-file validator come in as `SchemaTools`, so this
11
+ * file stays under the typecheck gate: schema.ts and validate-manifest.ts are
12
+ * still baseline-excluded (zod v4 drift), and importing them here would pull
13
+ * them back into the checked program. validate-manifest.ts wires them in and
14
+ * exports validateManifestLayers(repoPath).
15
+ */
16
+
17
+ import * as fs from "fs";
18
+ import { z } from "zod";
19
+ import type { ValidationError, ValidationResult } from "../types/index.js";
20
+ import {
21
+ isPlainMap,
22
+ mergeOverride,
23
+ overrideKeyErrors,
24
+ type ManifestMap,
25
+ } from "./merge-override.js";
26
+ import {
27
+ loadManifest,
28
+ orphanOverrideMessage,
29
+ parseManifestText,
30
+ type ManifestLayer,
31
+ } from "./read-manifest.js";
32
+
33
+ /** What layered validation needs from the schema module. */
34
+ export interface SchemaTools {
35
+ /** The current schema version and its schema. */
36
+ version: number;
37
+ schema: z.ZodType;
38
+ /** Validate one whole manifest (validateManifest). */
39
+ validate: (input: unknown) => ValidationResult;
40
+ /** Map Zod issues to ValidationErrors, as validateManifest does. */
41
+ issuesToErrors: (issues: z.core.$ZodIssue[]) => ValidationError[];
42
+ }
43
+
44
+ export interface LayerValidation {
45
+ layer: ManifestLayer | "merged";
46
+ /** The layer's file; null for the merged result. */
47
+ file: string | null;
48
+ result: ValidationResult;
49
+ }
50
+
51
+ export interface LayeredValidation {
52
+ valid: boolean;
53
+ /**
54
+ * Base first, then override, then merged (only when there is an override
55
+ * and both layers pass on their own). Empty when no manifest file exists.
56
+ */
57
+ layers: LayerValidation[];
58
+ }
59
+
60
+ /** Strip optional/nullable/default wrappers to the schema underneath. */
61
+ function unwrap(schema: z.ZodType): z.ZodType {
62
+ let s = schema;
63
+ for (;;) {
64
+ if (s instanceof z.ZodOptional || s instanceof z.ZodNullable)
65
+ s = s.unwrap() as z.ZodType;
66
+ else if (s instanceof z.ZodDefault) s = s.def.innerType as z.ZodType;
67
+ else return s;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Schema errors in `value`, an override's value at `at`, merging into
73
+ * `base` (the base's value there). Maps are walked key by key where the
74
+ * schema is an object or a record; anything else is checked whole (a map
75
+ * merged onto its base first, so a partial union member still passes).
76
+ */
77
+ function overrideSchemaErrors(
78
+ tools: SchemaTools,
79
+ schema: z.ZodType,
80
+ value: unknown,
81
+ base: unknown,
82
+ at: string[],
83
+ ): ValidationError[] {
84
+ if (value === null) return []; // a delete: overrideKeyErrors checks the key
85
+ const node = unwrap(schema);
86
+ const baseAt = (key: string): unknown =>
87
+ isPlainMap(base) ? base[key] : undefined;
88
+ if (isPlainMap(value) && node instanceof z.ZodObject) {
89
+ const shape = node.shape as Record<string, z.ZodType>;
90
+ return Object.entries(value).flatMap(([key, v]) =>
91
+ key in shape
92
+ ? overrideSchemaErrors(tools, shape[key], v, baseAt(key), [...at, key])
93
+ : [],
94
+ );
95
+ }
96
+ if (isPlainMap(value) && node instanceof z.ZodRecord) {
97
+ const inner = node.valueType as z.ZodType;
98
+ return Object.entries(value).flatMap(([key, v]) =>
99
+ overrideSchemaErrors(tools, inner, v, baseAt(key), [...at, key]),
100
+ );
101
+ }
102
+ const candidate =
103
+ isPlainMap(value) && isPlainMap(base) ? mergeOverride(base, value) : value;
104
+ const result = node.safeParse(candidate);
105
+ return result.success
106
+ ? []
107
+ : tools.issuesToErrors(
108
+ result.error.issues.map((i) => ({ ...i, path: [...at, ...i.path] })),
109
+ );
110
+ }
111
+
112
+ /** Validate an override against the schema and against its base's keys. */
113
+ export function validateOverride(
114
+ tools: SchemaTools,
115
+ override: unknown,
116
+ base: ManifestMap,
117
+ ): ValidationResult {
118
+ const { version } = tools;
119
+ if (!isPlainMap(override))
120
+ return {
121
+ valid: false,
122
+ version,
123
+ errors: [{ path: "", message: "an override must be a map" }],
124
+ warnings: [],
125
+ };
126
+ const errors = [
127
+ ...overrideKeyErrors(base, override),
128
+ ...overrideSchemaErrors(tools, tools.schema, override, base, []),
129
+ ];
130
+ return { valid: errors.length === 0, version, errors, warnings: [] };
131
+ }
132
+
133
+ function withFile(result: ValidationResult, file: string): ValidationResult {
134
+ const tag = (e: ValidationError): ValidationError => ({ ...e, file });
135
+ return {
136
+ ...result,
137
+ errors: result.errors.map(tag),
138
+ warnings: result.warnings.map(tag),
139
+ };
140
+ }
141
+
142
+ function failure(
143
+ version: number,
144
+ file: string,
145
+ message: string,
146
+ ): ValidationResult {
147
+ return {
148
+ valid: false,
149
+ version,
150
+ errors: [{ path: "", message, file }],
151
+ warnings: [],
152
+ };
153
+ }
154
+
155
+ /** Read and parse a layer file, or the reason it cannot be used. */
156
+ function readLayer(file: string): { value: unknown } | { error: string } {
157
+ try {
158
+ const raw = fs.readFileSync(file, "utf-8");
159
+ // A non-map (scalar, list, empty file) is passed on as is, so the
160
+ // validator reports it the way it reports any other bad manifest.
161
+ let value: unknown;
162
+ try {
163
+ value = parseManifestText(file, raw);
164
+ } catch (e) {
165
+ return { error: `could not parse: ${(e as Error).message}` };
166
+ }
167
+ return { value: value ?? (raw.trim() ? raw : null) };
168
+ } catch (e) {
169
+ return { error: `could not read: ${(e as Error).message}` };
170
+ }
171
+ }
172
+
173
+ /** Validate every manifest layer of `repoPath`, then the merged result. */
174
+ export function validateLayers(
175
+ tools: SchemaTools,
176
+ repoPath: string,
177
+ ): LayeredValidation {
178
+ const { baseFile, baseLayer, overrideFile } = loadManifest(repoPath);
179
+ const layers: LayerValidation[] = [];
180
+
181
+ if (!baseFile || !baseLayer) {
182
+ if (overrideFile)
183
+ layers.push({
184
+ layer: "override",
185
+ file: overrideFile,
186
+ result: failure(tools.version, overrideFile, orphanOverrideMessage()),
187
+ });
188
+ return { valid: false, layers };
189
+ }
190
+
191
+ const base = readLayer(baseFile);
192
+ const baseResult =
193
+ "error" in base
194
+ ? failure(tools.version, baseFile, base.error)
195
+ : withFile(tools.validate(base.value), baseFile);
196
+ layers.push({ layer: baseLayer, file: baseFile, result: baseResult });
197
+
198
+ if (overrideFile) {
199
+ const override = readLayer(overrideFile);
200
+ const baseMap = "value" in base && isPlainMap(base.value) ? base.value : {};
201
+ const overrideResult =
202
+ "error" in override
203
+ ? failure(tools.version, overrideFile, override.error)
204
+ : withFile(
205
+ validateOverride(tools, override.value, baseMap),
206
+ overrideFile,
207
+ );
208
+ layers.push({
209
+ layer: "override",
210
+ file: overrideFile,
211
+ result: overrideResult,
212
+ });
213
+ // The merged result is checked once both layers pass on their own, so a
214
+ // layer's error is reported once, against its own file.
215
+ if (
216
+ baseResult.valid &&
217
+ overrideResult.valid &&
218
+ "value" in override &&
219
+ isPlainMap(override.value)
220
+ )
221
+ layers.push({
222
+ layer: "merged",
223
+ file: null,
224
+ result: tools.validate(mergeOverride(baseMap, override.value)),
225
+ });
226
+ }
227
+
228
+ return { valid: layers.every((l) => l.result.valid), layers };
229
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * .den.yml manifest validator.
3
+ *
4
+ * Accepts a parsed object or a raw YAML string.
5
+ * Returns a ValidationResult describing any errors or warnings.
6
+ */
7
+
8
+ import * as yaml from "yaml";
9
+ import { z } from "zod";
10
+ import {
11
+ CURRENT_SCHEMA_VERSION,
12
+ KNOWN_VERSIONS,
13
+ schemaByVersion,
14
+ } from "./schema.js";
15
+ import type { ValidationError, ValidationResult } from "../types/index.js";
16
+ import { placementErrors } from "./placement.js";
17
+ import { builtinEnvironments, orgEnvironments } from "../den-config.js";
18
+ import {
19
+ validateLayers,
20
+ type LayeredValidation,
21
+ type SchemaTools,
22
+ } from "./validate-layers.js";
23
+
24
+ // ── Helpers ───────────────────────────────────────────────────────────────────
25
+
26
+ /** Convert a Zod issue path to a dot-separated string. */
27
+ function zodPathToString(path: (string | number)[]): string {
28
+ return path
29
+ .map((p, i) => (typeof p === "number" ? `[${p}]` : i === 0 ? p : `.${p}`))
30
+ .join("");
31
+ }
32
+
33
+ /** Friendly message for common Zod issue codes. */
34
+ function zodIssueMessage(issue: z.ZodIssue): string {
35
+ switch (issue.code) {
36
+ case z.ZodIssueCode.invalid_type:
37
+ if (issue.received === "undefined")
38
+ return `"${issue.path.at(-1)}" is required`;
39
+ return `expected ${issue.expected}, received ${issue.received}`;
40
+ case z.ZodIssueCode.invalid_enum_value:
41
+ return `invalid value "${issue.received}" — must be one of: ${issue.options.join(", ")}`;
42
+ case z.ZodIssueCode.invalid_union:
43
+ return "value does not match any valid shape";
44
+ default:
45
+ return issue.message;
46
+ }
47
+ }
48
+
49
+ /** Suggestion text for specific paths / issues. */
50
+ function suggestionFor(issue: z.ZodIssue): string | undefined {
51
+ const pathStr = zodPathToString(issue.path);
52
+
53
+ // auth arrays in smoke / synthetics
54
+ if (pathStr.includes("auth")) {
55
+ return 'Valid roles are: "admin", "user", "beta"';
56
+ }
57
+
58
+ // health with missing url/cmd
59
+ if (pathStr.includes("health")) {
60
+ return 'HTTP checks need "url"; shell checks need "type: shell" and "cmd"';
61
+ }
62
+
63
+ return undefined;
64
+ }
65
+
66
+ /** Map Zod issues to ValidationErrors. */
67
+ export function issuesToErrors(
68
+ issues: readonly z.ZodIssue[],
69
+ ): ValidationError[] {
70
+ return issues.map((issue) => ({
71
+ path: zodPathToString(issue.path as (string | number)[]),
72
+ message: zodIssueMessage(issue),
73
+ suggestion: suggestionFor(issue),
74
+ }));
75
+ }
76
+
77
+ // ── Main export ───────────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Validate a .den.yml manifest.
81
+ *
82
+ * @param input - A parsed object or a raw YAML string.
83
+ * @returns A ValidationResult with errors, warnings, and the detected version.
84
+ */
85
+ export interface ValidateOptions {
86
+ /** The org's environments; default: the org config's (see orgEnvironments). */
87
+ environments?: readonly string[];
88
+ }
89
+
90
+ function environmentsFor(opts: ValidateOptions): readonly string[] {
91
+ if (opts.environments) return opts.environments;
92
+ try {
93
+ return orgEnvironments();
94
+ } catch {
95
+ // An unreadable org config is reported where it is read; validate the
96
+ // manifest against Den's built-in environment list meanwhile.
97
+ return builtinEnvironments();
98
+ }
99
+ }
100
+
101
+ export function validateManifest(
102
+ input: unknown,
103
+ opts: ValidateOptions = {},
104
+ ): ValidationResult {
105
+ // 1. Parse YAML string if needed
106
+ let parsed: unknown = input;
107
+ if (typeof input === "string") {
108
+ try {
109
+ parsed = yaml.parse(input);
110
+ } catch (err: unknown) {
111
+ return {
112
+ valid: false,
113
+ version: 1,
114
+ errors: [
115
+ {
116
+ path: "",
117
+ message: `Could not parse YAML: ${(err as Error).message}`,
118
+ },
119
+ ],
120
+ warnings: [],
121
+ };
122
+ }
123
+ }
124
+
125
+ // 2. Detect version
126
+ const rawVersion =
127
+ parsed !== null &&
128
+ typeof parsed === "object" &&
129
+ "version" in (parsed as object)
130
+ ? (parsed as Record<string, unknown>).version
131
+ : undefined;
132
+
133
+ const detectedVersion = rawVersion === undefined ? 1 : Number(rawVersion);
134
+
135
+ const warnings: ValidationError[] = [];
136
+
137
+ // 3. Warn on unknown future versions but still validate with the latest schema
138
+ const isKnown = KNOWN_VERSIONS.includes(
139
+ detectedVersion as (typeof KNOWN_VERSIONS)[number],
140
+ );
141
+ if (rawVersion !== undefined && !isKnown) {
142
+ warnings.push({
143
+ path: "version",
144
+ message: `Unknown schema version ${detectedVersion}. Validating against v${CURRENT_SCHEMA_VERSION} (latest known).`,
145
+ });
146
+ }
147
+
148
+ // 4. Pick schema — fall back to latest for unknown future versions
149
+ const schema =
150
+ schemaByVersion[detectedVersion] ?? schemaByVersion[CURRENT_SCHEMA_VERSION];
151
+
152
+ // 5. Run Zod validation
153
+ const result = schema.safeParse(parsed);
154
+
155
+ if (result.success) {
156
+ // Env names in `runs` / `target.env` must be the org's (R13).
157
+ const placement = placementErrors(parsed, environmentsFor(opts));
158
+ if (placement.length > 0)
159
+ return {
160
+ valid: false,
161
+ version: detectedVersion,
162
+ errors: placement,
163
+ warnings,
164
+ };
165
+ return {
166
+ valid: true,
167
+ version: detectedVersion,
168
+ errors: [],
169
+ warnings,
170
+ };
171
+ }
172
+
173
+ // 6. Map Zod issues → ValidationError[]
174
+ const errors = issuesToErrors(result.error.issues);
175
+
176
+ return {
177
+ valid: false,
178
+ version: detectedVersion,
179
+ errors,
180
+ warnings,
181
+ };
182
+ }
183
+
184
+ // ── Layers ────────────────────────────────────────────────────────────────────
185
+
186
+ const schemaTools: SchemaTools = {
187
+ version: CURRENT_SCHEMA_VERSION,
188
+ schema: schemaByVersion[CURRENT_SCHEMA_VERSION],
189
+ validate: (input) => validateManifest(input),
190
+ issuesToErrors: (issues) => issuesToErrors(issues as z.ZodIssue[]),
191
+ };
192
+
193
+ /**
194
+ * Validate each manifest layer of the repo at `repoPath` (its own or the
195
+ * held manifest, then manifests/<repo>.override.yml), then the merge.
196
+ * See validate-layers.ts.
197
+ */
198
+ export function validateManifestLayers(repoPath: string): LayeredValidation {
199
+ return validateLayers(schemaTools, repoPath);
200
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Every service of the active workspaces, with its repo's manifest: the one
3
+ * list the security scan, the repo suites and the release check walk.
4
+ *
5
+ * Mirrors the server's health walk: the focused workspace first, then the
6
+ * other active ones; an address listed twice keeps its first owner.
7
+ */
8
+
9
+ import { readCatalog } from "../environment-manager/catalog.js";
10
+ import {
11
+ getActiveWorkspaces,
12
+ getFocusedWorkspaceName,
13
+ getWorkspaceByName,
14
+ listWorkspaceServices,
15
+ readManifestAnyBranch,
16
+ } from "../environment-manager/workspaces.js";
17
+ import type {
18
+ RawManifest,
19
+ ServiceCatalog,
20
+ ServiceManagementConfig,
21
+ } from "../types/index.js";
22
+ import { orgOf } from "./service-links.js";
23
+
24
+ export interface WorkspaceService {
25
+ address: string;
26
+ repo: string;
27
+ service: string;
28
+ repoPath: string;
29
+ org: string;
30
+ manifest: RawManifest;
31
+ config: ServiceManagementConfig | undefined;
32
+ isEntrypoint: boolean;
33
+ }
34
+
35
+ export function workspaceNames(workspaceName?: string | null): string[] {
36
+ if (workspaceName)
37
+ return getWorkspaceByName(workspaceName) ? [workspaceName] : [];
38
+ const focus = getFocusedWorkspaceName();
39
+ return [
40
+ ...(focus ? [focus] : []),
41
+ ...getActiveWorkspaces().filter((n) => n !== focus),
42
+ ];
43
+ }
44
+
45
+ export function listServicesWithManifests(
46
+ workspaceName?: string | null,
47
+ catalog: ServiceCatalog = readCatalog(),
48
+ ): WorkspaceService[] {
49
+ const seen = new Set<string>();
50
+ const out: WorkspaceService[] = [];
51
+ for (const name of workspaceNames(workspaceName)) {
52
+ const ws = getWorkspaceByName(name);
53
+ if (!ws) continue;
54
+ for (const svc of listWorkspaceServices(ws.config, catalog, name)) {
55
+ if (seen.has(svc.address)) continue;
56
+ seen.add(svc.address);
57
+ const manifest = readManifestAnyBranch(svc.repoName, svc.repoPath);
58
+ if (!manifest) continue;
59
+ out.push({
60
+ address: svc.address,
61
+ repo: svc.repoName,
62
+ service: svc.subsystem,
63
+ repoPath: svc.repoPath,
64
+ org: orgOf(svc.repoName, catalog),
65
+ manifest,
66
+ config: manifest.services?.[svc.subsystem],
67
+ isEntrypoint: manifest.entrypoint === svc.subsystem,
68
+ });
69
+ }
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** One entry per repo (its first service carries the manifest). */
75
+ export function listReposWithManifests(
76
+ workspaceName?: string | null,
77
+ catalog: ServiceCatalog = readCatalog(),
78
+ ): Array<Pick<WorkspaceService, "repo" | "repoPath" | "org" | "manifest">> {
79
+ const byRepo = new Map<
80
+ string,
81
+ Pick<WorkspaceService, "repo" | "repoPath" | "org" | "manifest">
82
+ >();
83
+ for (const s of listServicesWithManifests(workspaceName, catalog))
84
+ if (!byRepo.has(s.repo))
85
+ byRepo.set(s.repo, {
86
+ repo: s.repo,
87
+ repoPath: s.repoPath,
88
+ org: s.org,
89
+ manifest: s.manifest,
90
+ });
91
+ return [...byRepo.values()];
92
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Where a service's app opens in a browser, per environment, for the UI's
3
+ * open-in-new-tab links. Health URLs point at /healthz and friends, never the
4
+ * app, so they only ever contribute their origin.
5
+ *
6
+ * Order, first answer wins:
7
+ * 1. manifest `services.<svc>.urls.<env>`
8
+ * 2. local only: the edge route for the service's compose container,
9
+ * `https://<route>.<stack>.<edge domain>`; the stack-wide service (the
10
+ * entrypoint, or one named after the stack) takes the `app` route, else
11
+ * the first route
12
+ * 3. the origin of the env's HTTP health URL
13
+ * Only http(s) URLs are returned.
14
+ */
15
+ import { manifestStackName } from "./stacks/config.js";
16
+ import { parseTarget } from "./stacks/render.js";
17
+ import type { RawManifest as DenManifest } from "./types/index.js";
18
+
19
+ export interface OpenLink {
20
+ label: string;
21
+ url: string;
22
+ }
23
+
24
+ /** The URL if it is absolute http(s), else null. */
25
+ export function httpUrl(raw: unknown): string | null {
26
+ if (typeof raw !== "string" || !raw) return null;
27
+ try {
28
+ const u = new URL(raw);
29
+ return u.protocol === "http:" || u.protocol === "https:" ? u.href : null;
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ function edgeRouteUrl(
36
+ manifest: DenManifest,
37
+ subsystem: string,
38
+ repoName: string,
39
+ edgeDomain: string,
40
+ ): string | null {
41
+ const routes = manifest.compose?.routes;
42
+ if (!routes || !Object.keys(routes).length) return null;
43
+ const stack = manifestStackName(manifest, repoName);
44
+ const names = Object.keys(routes).sort();
45
+ let route = names.find((r) => {
46
+ try {
47
+ return parseTarget(routes[r]).service === subsystem;
48
+ } catch {
49
+ return false;
50
+ }
51
+ });
52
+ if (!route && (manifest.entrypoint === subsystem || subsystem === stack))
53
+ route = routes.app ? "app" : names[0];
54
+ return route ? `https://${route}.${stack}.${edgeDomain}/` : null;
55
+ }
56
+
57
+ export function openUrlFor(opts: {
58
+ manifest: DenManifest;
59
+ subsystem: string;
60
+ repoName: string;
61
+ env: string;
62
+ /** The env's HTTP health URL, when the check is HTTP. */
63
+ healthUrl?: string | null;
64
+ /** Primary edge domain; local edge routes are skipped without it. */
65
+ edgeDomain?: string | null;
66
+ }): string | null {
67
+ const svc = opts.manifest.services?.[opts.subsystem];
68
+ const declared = httpUrl(
69
+ (svc?.urls as Record<string, string> | undefined)?.[opts.env],
70
+ );
71
+ if (declared) return declared;
72
+ if (opts.env === "local" && opts.edgeDomain) {
73
+ const routed = edgeRouteUrl(
74
+ opts.manifest,
75
+ opts.subsystem,
76
+ opts.repoName,
77
+ opts.edgeDomain,
78
+ );
79
+ if (routed) return routed;
80
+ }
81
+ const health = httpUrl(opts.healthUrl);
82
+ return health ? `${new URL(health).origin}/` : null;
83
+ }
84
+
85
+ /** Extra named links (`services.<svc>.links`) that have a URL for the env. */
86
+ export function linksFor(
87
+ manifest: DenManifest,
88
+ subsystem: string,
89
+ env: string,
90
+ ): OpenLink[] {
91
+ const out: OpenLink[] = [];
92
+ for (const l of manifest.services?.[subsystem]?.links ?? []) {
93
+ const url = httpUrl((l?.urls as Record<string, string> | undefined)?.[env]);
94
+ if (url && typeof l.label === "string") out.push({ label: l.label, url });
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /**
100
+ * Swap the primary edge domain for the alias the viewer is on (e.g. the UI was
101
+ * opened as den.<alias> from another machine), so links resolve there too.
102
+ */
103
+ export function rehost(
104
+ url: string,
105
+ primary: string,
106
+ host: string | undefined,
107
+ aliases: string[],
108
+ ): string {
109
+ const h = (host ?? "").split(":")[0].toLowerCase();
110
+ const alias = aliases.find((a) => h === a || h.endsWith(`.${a}`));
111
+ if (!alias) return url;
112
+ try {
113
+ const u = new URL(url);
114
+ if (u.hostname === primary || u.hostname.endsWith(`.${primary}`)) {
115
+ u.hostname = u.hostname.slice(0, -primary.length) + alias;
116
+ return u.href;
117
+ }
118
+ } catch {
119
+ /* not a URL: leave it */
120
+ }
121
+ return url;
122
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The org this den instance serves: the root of
3
+ * Org > Application > Repo > Services. One instance = one org.
4
+ *
5
+ * org:
6
+ * id: globex # defaults to edge.org
7
+ * title: Globex # defaults to the id
8
+ */
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ import * as yaml from "yaml";
12
+ import { getDistRoot } from "./paths.js";
13
+
14
+ import { product } from "./product.js";
15
+ export interface OrgConfig {
16
+ id?: string;
17
+ title?: string;
18
+ }
19
+
20
+ export interface Org {
21
+ id: string;
22
+ title: string;
23
+ }
24
+
25
+ /** Pure: the org from a parsed config/den.yml, or undefined if unnamed. */
26
+ export function resolveOrg(
27
+ parsed: { org?: OrgConfig; edge?: { org?: string } } | null | undefined,
28
+ ): Org | undefined {
29
+ const id = parsed?.org?.id?.trim() || parsed?.edge?.org?.trim();
30
+ if (!id) return undefined;
31
+ return { id, title: parsed?.org?.title?.trim() || id };
32
+ }
33
+
34
+ export function readOrg(root: string = getDistRoot()): Org | undefined {
35
+ try {
36
+ return resolveOrg(
37
+ yaml.parse(
38
+ fs.readFileSync(path.join(root, product().configFile), "utf-8"),
39
+ ),
40
+ );
41
+ } catch {
42
+ return undefined;
43
+ }
44
+ }