@vibesdotdev/infra-core 0.0.2 → 1.0.0-rc.4

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 (319) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +27 -1
  3. package/dist/bootstrap/seed.descriptor.d.ts +1 -1
  4. package/dist/bootstrap/seed.descriptor.d.ts.map +1 -1
  5. package/dist/bootstrap/seed.descriptor.js +1 -1
  6. package/dist/bootstrap/seed.descriptor.js.map +1 -1
  7. package/dist/bootstrap/seed.impl-shape.d.ts +1 -1
  8. package/dist/bootstrap/seed.impl-shape.d.ts.map +1 -1
  9. package/dist/bootstrap/seed.kind.d.ts +1 -3
  10. package/dist/bootstrap/seed.kind.d.ts.map +1 -1
  11. package/dist/bootstrap/seed.kind.js +17 -4
  12. package/dist/bootstrap/seed.kind.js.map +1 -1
  13. package/dist/bootstrap/seed.runner.d.ts +9 -2
  14. package/dist/bootstrap/seed.runner.d.ts.map +1 -1
  15. package/dist/bootstrap/seed.runner.js +37 -18
  16. package/dist/bootstrap/seed.runner.js.map +1 -1
  17. package/dist/capped-id.d.ts +19 -0
  18. package/dist/capped-id.d.ts.map +1 -0
  19. package/dist/capped-id.js +64 -0
  20. package/dist/capped-id.js.map +1 -0
  21. package/dist/cli/generate/artifact/artifact.cli-command.descriptor.d.ts +23 -0
  22. package/dist/cli/generate/artifact/artifact.cli-command.descriptor.d.ts.map +1 -0
  23. package/dist/cli/generate/artifact/artifact.cli-command.descriptor.js +33 -0
  24. package/dist/cli/generate/artifact/artifact.cli-command.descriptor.js.map +1 -0
  25. package/dist/cli/generate/artifact/artifact.cli-command.impl.d.ts +24 -0
  26. package/dist/cli/generate/artifact/artifact.cli-command.impl.d.ts.map +1 -0
  27. package/dist/cli/generate/artifact/artifact.cli-command.impl.js +73 -0
  28. package/dist/cli/generate/artifact/artifact.cli-command.impl.js.map +1 -0
  29. package/dist/cli/generate/context.d.ts +17 -0
  30. package/dist/cli/generate/context.d.ts.map +1 -0
  31. package/dist/cli/generate/context.js +24 -0
  32. package/dist/cli/generate/context.js.map +1 -0
  33. package/dist/cli/generate/generate/generate.cli-command.descriptor.d.ts +30 -0
  34. package/dist/cli/generate/generate/generate.cli-command.descriptor.d.ts.map +1 -0
  35. package/dist/cli/generate/generate/generate.cli-command.descriptor.js +25 -0
  36. package/dist/cli/generate/generate/generate.cli-command.descriptor.js.map +1 -0
  37. package/dist/cli/generate/generate/generate.cli-command.impl.d.ts +25 -0
  38. package/dist/cli/generate/generate/generate.cli-command.impl.d.ts.map +1 -0
  39. package/dist/cli/generate/generate/generate.cli-command.impl.js +64 -0
  40. package/dist/cli/generate/generate/generate.cli-command.impl.js.map +1 -0
  41. package/dist/cli/generate/generate-core.d.ts +51 -0
  42. package/dist/cli/generate/generate-core.d.ts.map +1 -0
  43. package/dist/cli/generate/generate-core.js +172 -0
  44. package/dist/cli/generate/generate-core.js.map +1 -0
  45. package/dist/cli/generate/index.d.ts +5 -0
  46. package/dist/cli/generate/index.d.ts.map +1 -0
  47. package/dist/cli/generate/index.js +5 -0
  48. package/dist/cli/generate/index.js.map +1 -0
  49. package/dist/credentials/resolve.d.ts +82 -22
  50. package/dist/credentials/resolve.d.ts.map +1 -1
  51. package/dist/credentials/resolve.js +251 -77
  52. package/dist/credentials/resolve.js.map +1 -1
  53. package/dist/deployment-environment.d.ts +30 -0
  54. package/dist/deployment-environment.d.ts.map +1 -0
  55. package/dist/deployment-environment.js +53 -0
  56. package/dist/deployment-environment.js.map +1 -0
  57. package/dist/deployment.d.ts +8 -4
  58. package/dist/deployment.d.ts.map +1 -1
  59. package/dist/deployment.js +30 -0
  60. package/dist/deployment.js.map +1 -1
  61. package/dist/doks/cloud/base.d.ts +49 -0
  62. package/dist/doks/cloud/base.d.ts.map +1 -0
  63. package/dist/doks/cloud/base.js +138 -0
  64. package/dist/doks/cloud/base.js.map +1 -0
  65. package/dist/doks/cloud/factory.d.ts +18 -0
  66. package/dist/doks/cloud/factory.d.ts.map +1 -0
  67. package/dist/doks/cloud/factory.js +22 -0
  68. package/dist/doks/cloud/factory.js.map +1 -0
  69. package/dist/doks/cloud/index.d.ts +5 -0
  70. package/dist/doks/cloud/index.d.ts.map +1 -0
  71. package/dist/doks/cloud/index.js +4 -0
  72. package/dist/doks/cloud/index.js.map +1 -0
  73. package/dist/doks/types.d.ts +95 -0
  74. package/dist/doks/types.d.ts.map +1 -0
  75. package/dist/doks/types.js +14 -0
  76. package/dist/doks/types.js.map +1 -0
  77. package/dist/generate/index.d.ts +29 -0
  78. package/dist/generate/index.d.ts.map +1 -0
  79. package/dist/generate/index.js +28 -0
  80. package/dist/generate/index.js.map +1 -0
  81. package/dist/generate/infra-generate.plugin.d.ts +3 -0
  82. package/dist/generate/infra-generate.plugin.d.ts.map +1 -0
  83. package/dist/generate/infra-generate.plugin.js +23 -0
  84. package/dist/generate/infra-generate.plugin.js.map +1 -0
  85. package/dist/generate/plugin.d.ts +2 -0
  86. package/dist/generate/plugin.d.ts.map +1 -0
  87. package/dist/generate/plugin.js +2 -0
  88. package/dist/generate/plugin.js.map +1 -0
  89. package/dist/heartbeat/cron-heartbeat.d.ts +97 -0
  90. package/dist/heartbeat/cron-heartbeat.d.ts.map +1 -0
  91. package/dist/heartbeat/cron-heartbeat.js +200 -0
  92. package/dist/heartbeat/cron-heartbeat.js.map +1 -0
  93. package/dist/index.d.ts +34 -20
  94. package/dist/index.d.ts.map +1 -1
  95. package/dist/index.js +28 -14
  96. package/dist/index.js.map +1 -1
  97. package/dist/infra.plugin.d.ts +1 -1
  98. package/dist/infra.plugin.d.ts.map +1 -1
  99. package/dist/infra.plugin.js +62 -12
  100. package/dist/infra.plugin.js.map +1 -1
  101. package/dist/kinds/alerts.kind.d.ts +64 -25
  102. package/dist/kinds/alerts.kind.d.ts.map +1 -1
  103. package/dist/kinds/alerts.kind.js +43 -16
  104. package/dist/kinds/alerts.kind.js.map +1 -1
  105. package/dist/kinds/artifact.kind.d.ts +58 -10
  106. package/dist/kinds/artifact.kind.d.ts.map +1 -1
  107. package/dist/kinds/artifact.kind.js +26 -9
  108. package/dist/kinds/artifact.kind.js.map +1 -1
  109. package/dist/kinds/billing.kind.d.ts +88 -0
  110. package/dist/kinds/billing.kind.d.ts.map +1 -0
  111. package/dist/kinds/billing.kind.js +48 -0
  112. package/dist/kinds/billing.kind.js.map +1 -0
  113. package/dist/kinds/build-job.kind.d.ts +381 -0
  114. package/dist/kinds/build-job.kind.d.ts.map +1 -0
  115. package/dist/kinds/build-job.kind.js +216 -0
  116. package/dist/kinds/build-job.kind.js.map +1 -0
  117. package/dist/kinds/cache.kind.d.ts +39 -92
  118. package/dist/kinds/cache.kind.d.ts.map +1 -1
  119. package/dist/kinds/cache.kind.js +26 -51
  120. package/dist/kinds/cache.kind.js.map +1 -1
  121. package/dist/kinds/cluster.kind.d.ts +154 -0
  122. package/dist/kinds/cluster.kind.d.ts.map +1 -0
  123. package/dist/kinds/cluster.kind.js +99 -0
  124. package/dist/kinds/cluster.kind.js.map +1 -0
  125. package/dist/kinds/compute-source.kind.d.ts +138 -0
  126. package/dist/kinds/compute-source.kind.d.ts.map +1 -0
  127. package/dist/kinds/compute-source.kind.js +145 -0
  128. package/dist/kinds/compute-source.kind.js.map +1 -0
  129. package/dist/kinds/compute-sources.descriptors.d.ts +45 -0
  130. package/dist/kinds/compute-sources.descriptors.d.ts.map +1 -0
  131. package/dist/kinds/compute-sources.descriptors.js +93 -0
  132. package/dist/kinds/compute-sources.descriptors.js.map +1 -0
  133. package/dist/kinds/container-registry.kind.d.ts +62 -0
  134. package/dist/kinds/container-registry.kind.d.ts.map +1 -0
  135. package/dist/kinds/container-registry.kind.js +47 -0
  136. package/dist/kinds/container-registry.kind.js.map +1 -0
  137. package/dist/kinds/database-migrator.kind.d.ts +6 -0
  138. package/dist/kinds/database-migrator.kind.d.ts.map +1 -0
  139. package/dist/kinds/database-migrator.kind.js +28 -0
  140. package/dist/kinds/database-migrator.kind.js.map +1 -0
  141. package/dist/kinds/database.kind.d.ts +114 -93
  142. package/dist/kinds/database.kind.d.ts.map +1 -1
  143. package/dist/kinds/database.kind.js +93 -54
  144. package/dist/kinds/database.kind.js.map +1 -1
  145. package/dist/kinds/deployment-provider.kind.d.ts +5 -0
  146. package/dist/kinds/deployment-provider.kind.d.ts.map +1 -0
  147. package/dist/kinds/deployment-provider.kind.js +11 -0
  148. package/dist/kinds/deployment-provider.kind.js.map +1 -0
  149. package/dist/kinds/discovery.d.ts +5 -12
  150. package/dist/kinds/discovery.d.ts.map +1 -1
  151. package/dist/kinds/discovery.js +5 -17
  152. package/dist/kinds/discovery.js.map +1 -1
  153. package/dist/kinds/dns.kind.d.ts +162 -0
  154. package/dist/kinds/dns.kind.d.ts.map +1 -0
  155. package/dist/kinds/dns.kind.js +54 -0
  156. package/dist/kinds/dns.kind.js.map +1 -0
  157. package/dist/kinds/drift.kind.d.ts +115 -0
  158. package/dist/kinds/drift.kind.d.ts.map +1 -0
  159. package/dist/kinds/drift.kind.js +60 -0
  160. package/dist/kinds/drift.kind.js.map +1 -0
  161. package/dist/kinds/edge-tls.kind.d.ts +194 -0
  162. package/dist/kinds/edge-tls.kind.d.ts.map +1 -0
  163. package/dist/kinds/edge-tls.kind.js +58 -0
  164. package/dist/kinds/edge-tls.kind.js.map +1 -0
  165. package/dist/kinds/firewall.kind.d.ts +92 -0
  166. package/dist/kinds/firewall.kind.d.ts.map +1 -0
  167. package/dist/kinds/firewall.kind.js +59 -0
  168. package/dist/kinds/firewall.kind.js.map +1 -0
  169. package/dist/kinds/git-hosting.kind.d.ts +36 -38
  170. package/dist/kinds/git-hosting.kind.d.ts.map +1 -1
  171. package/dist/kinds/git-hosting.kind.js +21 -32
  172. package/dist/kinds/git-hosting.kind.js.map +1 -1
  173. package/dist/kinds/index.d.ts +40 -15
  174. package/dist/kinds/index.d.ts.map +1 -1
  175. package/dist/kinds/index.js +39 -14
  176. package/dist/kinds/index.js.map +1 -1
  177. package/dist/kinds/inventory.kind.d.ts +326 -0
  178. package/dist/kinds/inventory.kind.d.ts.map +1 -0
  179. package/dist/kinds/inventory.kind.js +188 -0
  180. package/dist/kinds/inventory.kind.js.map +1 -0
  181. package/dist/kinds/load-balancer.kind.d.ts +135 -0
  182. package/dist/kinds/load-balancer.kind.d.ts.map +1 -0
  183. package/dist/kinds/load-balancer.kind.js +71 -0
  184. package/dist/kinds/load-balancer.kind.js.map +1 -0
  185. package/dist/kinds/logs.kind.d.ts +74 -3
  186. package/dist/kinds/logs.kind.d.ts.map +1 -1
  187. package/dist/kinds/logs.kind.js +26 -17
  188. package/dist/kinds/logs.kind.js.map +1 -1
  189. package/dist/kinds/nfs.kind.d.ts +67 -0
  190. package/dist/kinds/nfs.kind.d.ts.map +1 -0
  191. package/dist/kinds/nfs.kind.js +45 -0
  192. package/dist/kinds/nfs.kind.js.map +1 -0
  193. package/dist/kinds/node-pool.kind.d.ts +134 -0
  194. package/dist/kinds/node-pool.kind.d.ts.map +1 -0
  195. package/dist/kinds/node-pool.kind.js +88 -0
  196. package/dist/kinds/node-pool.kind.js.map +1 -0
  197. package/dist/kinds/object-storage.kind.d.ts +50 -24
  198. package/dist/kinds/object-storage.kind.d.ts.map +1 -1
  199. package/dist/kinds/object-storage.kind.js +40 -23
  200. package/dist/kinds/object-storage.kind.js.map +1 -1
  201. package/dist/kinds/observability.kind.d.ts +104 -6
  202. package/dist/kinds/observability.kind.d.ts.map +1 -1
  203. package/dist/kinds/observability.kind.js +67 -9
  204. package/dist/kinds/observability.kind.js.map +1 -1
  205. package/dist/kinds/package-registry.kind.d.ts +25 -29
  206. package/dist/kinds/package-registry.kind.d.ts.map +1 -1
  207. package/dist/kinds/package-registry.kind.js +12 -25
  208. package/dist/kinds/package-registry.kind.js.map +1 -1
  209. package/dist/kinds/queue.kind.d.ts +67 -63
  210. package/dist/kinds/queue.kind.d.ts.map +1 -1
  211. package/dist/kinds/queue.kind.js +55 -33
  212. package/dist/kinds/queue.kind.js.map +1 -1
  213. package/dist/kinds/read-descriptor.d.ts +15 -0
  214. package/dist/kinds/read-descriptor.d.ts.map +1 -0
  215. package/dist/kinds/read-descriptor.js +11 -0
  216. package/dist/kinds/read-descriptor.js.map +1 -0
  217. package/dist/kinds/rum.kind.d.ts +36 -6
  218. package/dist/kinds/rum.kind.d.ts.map +1 -1
  219. package/dist/kinds/rum.kind.js +6 -7
  220. package/dist/kinds/rum.kind.js.map +1 -1
  221. package/dist/kinds/sandbox.kind.d.ts +136 -36
  222. package/dist/kinds/sandbox.kind.d.ts.map +1 -1
  223. package/dist/kinds/sandbox.kind.js +80 -34
  224. package/dist/kinds/sandbox.kind.js.map +1 -1
  225. package/dist/kinds/select-compute-source.d.ts +72 -0
  226. package/dist/kinds/select-compute-source.d.ts.map +1 -0
  227. package/dist/kinds/select-compute-source.js +170 -0
  228. package/dist/kinds/select-compute-source.js.map +1 -0
  229. package/dist/kinds/service-release.kind.d.ts +771 -0
  230. package/dist/kinds/service-release.kind.d.ts.map +1 -0
  231. package/dist/kinds/service-release.kind.js +676 -0
  232. package/dist/kinds/service-release.kind.js.map +1 -0
  233. package/dist/kinds/spend-guard.kind.d.ts +40 -0
  234. package/dist/kinds/spend-guard.kind.d.ts.map +1 -0
  235. package/dist/kinds/spend-guard.kind.js +133 -0
  236. package/dist/kinds/spend-guard.kind.js.map +1 -0
  237. package/dist/kinds/utilization.kind.d.ts +88 -0
  238. package/dist/kinds/utilization.kind.d.ts.map +1 -0
  239. package/dist/kinds/utilization.kind.js +62 -0
  240. package/dist/kinds/utilization.kind.js.map +1 -0
  241. package/dist/kinds/volume.kind.d.ts +54 -0
  242. package/dist/kinds/volume.kind.d.ts.map +1 -0
  243. package/dist/kinds/volume.kind.js +39 -0
  244. package/dist/kinds/volume.kind.js.map +1 -0
  245. package/dist/kinds/vpc.kind.d.ts +61 -0
  246. package/dist/kinds/vpc.kind.d.ts.map +1 -0
  247. package/dist/kinds/vpc.kind.js +51 -0
  248. package/dist/kinds/vpc.kind.js.map +1 -0
  249. package/dist/kinds/web-app.kind.d.ts +115 -45
  250. package/dist/kinds/web-app.kind.d.ts.map +1 -1
  251. package/dist/kinds/web-app.kind.js +69 -42
  252. package/dist/kinds/web-app.kind.js.map +1 -1
  253. package/dist/kinds/worker.kind.d.ts +291 -188
  254. package/dist/kinds/worker.kind.d.ts.map +1 -1
  255. package/dist/kinds/worker.kind.js +45 -123
  256. package/dist/kinds/worker.kind.js.map +1 -1
  257. package/dist/schemas/database-migration.d.ts +173 -0
  258. package/dist/schemas/database-migration.d.ts.map +1 -0
  259. package/dist/schemas/database-migration.js +88 -0
  260. package/dist/schemas/database-migration.js.map +1 -0
  261. package/dist/schemas/deployment-environment.d.ts +11 -0
  262. package/dist/schemas/deployment-environment.d.ts.map +1 -0
  263. package/dist/schemas/deployment-environment.js +9 -0
  264. package/dist/schemas/deployment-environment.js.map +1 -0
  265. package/dist/schemas/deployment-provider.d.ts +336 -0
  266. package/dist/schemas/deployment-provider.d.ts.map +1 -0
  267. package/dist/schemas/deployment-provider.js +198 -0
  268. package/dist/schemas/deployment-provider.js.map +1 -0
  269. package/dist/schemas/log-stream.d.ts +27 -0
  270. package/dist/schemas/log-stream.d.ts.map +1 -0
  271. package/dist/schemas/log-stream.js +31 -0
  272. package/dist/schemas/log-stream.js.map +1 -0
  273. package/dist/schemas/spend-guard.d.ts +155 -0
  274. package/dist/schemas/spend-guard.d.ts.map +1 -0
  275. package/dist/schemas/spend-guard.js +61 -0
  276. package/dist/schemas/spend-guard.js.map +1 -0
  277. package/dist/schemas.d.ts +190 -20
  278. package/dist/schemas.d.ts.map +1 -1
  279. package/dist/schemas.js +137 -31
  280. package/dist/schemas.js.map +1 -1
  281. package/dist/service.d.ts +87 -0
  282. package/dist/service.d.ts.map +1 -0
  283. package/dist/service.js +199 -0
  284. package/dist/service.js.map +1 -0
  285. package/dist/settle-work.d.ts +20 -0
  286. package/dist/settle-work.d.ts.map +1 -0
  287. package/dist/settle-work.js +22 -0
  288. package/dist/settle-work.js.map +1 -0
  289. package/dist/web-app-projection.d.ts +132 -0
  290. package/dist/web-app-projection.d.ts.map +1 -0
  291. package/dist/web-app-projection.js +93 -0
  292. package/dist/web-app-projection.js.map +1 -0
  293. package/package.json +87 -36
  294. package/src/bootstrap/index.ts +0 -15
  295. package/src/bootstrap/seed.descriptor.ts +0 -24
  296. package/src/bootstrap/seed.impl-shape.ts +0 -5
  297. package/src/bootstrap/seed.kind.ts +0 -26
  298. package/src/bootstrap/seed.runner.ts +0 -87
  299. package/src/credentials/resolve.ts +0 -205
  300. package/src/deployment.ts +0 -57
  301. package/src/index.ts +0 -149
  302. package/src/infra.plugin.ts +0 -54
  303. package/src/kinds/alerts.kind.ts +0 -164
  304. package/src/kinds/artifact.kind.ts +0 -96
  305. package/src/kinds/cache.kind.ts +0 -104
  306. package/src/kinds/database.kind.ts +0 -120
  307. package/src/kinds/discovery.ts +0 -26
  308. package/src/kinds/git-hosting.kind.ts +0 -100
  309. package/src/kinds/index.ts +0 -139
  310. package/src/kinds/logs.kind.ts +0 -104
  311. package/src/kinds/object-storage.kind.ts +0 -101
  312. package/src/kinds/observability.kind.ts +0 -178
  313. package/src/kinds/package-registry.kind.ts +0 -95
  314. package/src/kinds/queue.kind.ts +0 -102
  315. package/src/kinds/rum.kind.ts +0 -135
  316. package/src/kinds/sandbox.kind.ts +0 -115
  317. package/src/kinds/web-app.kind.ts +0 -103
  318. package/src/kinds/worker.kind.ts +0 -203
  319. package/src/schemas.ts +0 -127
@@ -0,0 +1,676 @@
1
+ /**
2
+ * Kind: infra/service-release
3
+ *
4
+ * A provider-neutral release unit for one public stateless gateway backed by
5
+ * one private, durable service. Provider adapters project this contract into
6
+ * their native workload, service, storage, and network-policy resources.
7
+ */
8
+ import * as z from 'zod/v4';
9
+ import { createRuntimeKind } from '@vibesdotdev/kernel/factory/kind';
10
+ import { RuntimeDescriptorSchema } from '@vibesdotdev/kernel/schemas/descriptor';
11
+ import { readKindDescriptor } from "./read-descriptor.js";
12
+ import { CredentialReferenceSchema } from "./object-storage.kind.js";
13
+ import { INFRA_DISCOVERY_PATTERNS } from "./discovery.js";
14
+ const DnsLabelSchema = z
15
+ .string()
16
+ .min(1)
17
+ .max(63)
18
+ .regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'must be a DNS label');
19
+ const ImageReferenceSchema = z
20
+ .string()
21
+ .min(1)
22
+ .regex(/@sha256:[a-f0-9]{64}$/, 'image must be pinned by a 64-character sha256 digest');
23
+ const RevisionSchema = z
24
+ .string()
25
+ .min(1)
26
+ .max(63)
27
+ .regex(/^[A-Za-z0-9](?:[A-Za-z0-9_.-]*[A-Za-z0-9])?$/, 'must be a Kubernetes label value');
28
+ const CronScheduleSchema = z
29
+ .string()
30
+ .regex(/^(?:\S+\s+){4}\S+$/, 'must be a five-field cron schedule');
31
+ const StorageQuantitySchema = z.string().regex(/^\d+(?:Mi|Gi|Ti)$/);
32
+ /**
33
+ * A storage quantity in MiB, or `undefined` for one its own field already refused:
34
+ * a refinement runs on fields that failed their checks, so it never throws.
35
+ */
36
+ function storageQuantityMi(value) {
37
+ const match = value.match(/^(\d+)(Mi|Gi|Ti)$/);
38
+ if (!match)
39
+ return undefined;
40
+ const amount = Number(match[1]);
41
+ return amount * (match[2] === 'Ti' ? 1024 * 1024 : match[2] === 'Gi' ? 1024 : 1);
42
+ }
43
+ /**
44
+ * The parts of a URL, or `undefined` for text that is not one. Zod runs a
45
+ * refinement even when the field's own `url()` check failed, so every URL read
46
+ * inside a refinement goes through here and never throws out of `safeParse`.
47
+ */
48
+ function urlOf(value) {
49
+ return URL.canParse(value) ? new URL(value) : undefined;
50
+ }
51
+ const BoundedIpv4CidrSchema = z.string().refine((value) => {
52
+ const match = value.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d|[12]\d|3[0-2])$/);
53
+ if (!match)
54
+ return false;
55
+ const octets = match.slice(1, 5).map(Number);
56
+ const prefix = Number(match[5]);
57
+ if (octets.some((octet) => octet > 255) || prefix < 8)
58
+ return false;
59
+ const address = octets.reduce((result, octet) => result * 256 + octet, 0);
60
+ return address % 2 ** (32 - prefix) === 0;
61
+ }, 'must be a canonical bounded IPv4 CIDR; world-scale prefixes /0 through /7 are forbidden');
62
+ const BoundedIpv4CidrListSchema = z
63
+ .array(BoundedIpv4CidrSchema)
64
+ .min(1)
65
+ .max(64)
66
+ .refine((cidrs) => new Set(cidrs).size === cidrs.length, 'CIDRs must be unique');
67
+ const EgressHostnameListSchema = z
68
+ .array(z.string().regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'must be an exact lowercase DNS hostname'))
69
+ .min(1)
70
+ .max(8)
71
+ .refine((hostnames) => new Set(hostnames).size === hostnames.length, 'hostnames must be unique');
72
+ const DnsZoneListSchema = z
73
+ .array(z.string().regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'must be an exact lowercase DNS zone'))
74
+ .min(1)
75
+ .max(8)
76
+ .refine((zones) => new Set(zones).size === zones.length, 'DNS zones must be unique');
77
+ const SecretStoreKeySchema = z
78
+ .string()
79
+ .regex(/^[A-Z][A-Z0-9_]+$/, 'secret-store keys must use the canonical uppercase key form');
80
+ export const ServiceReleaseSecretRefSchema = z.object({
81
+ name: DnsLabelSchema,
82
+ key: z.string().min(1)
83
+ });
84
+ export const ServiceReleaseSecretBindingSchema = z.object({
85
+ source: z.discriminatedUnion('kind', [
86
+ z.object({ kind: z.literal('secret-store'), key: SecretStoreKeySchema }),
87
+ z.object({
88
+ kind: z.literal('repository-file'),
89
+ path: z
90
+ .string()
91
+ .min(1)
92
+ .refine((path) => !path.startsWith('/') && !path.split('/').includes('..'), 'must be a repository-relative path')
93
+ })
94
+ ]),
95
+ target: ServiceReleaseSecretRefSchema,
96
+ description: z.string().min(1)
97
+ });
98
+ export const ServiceReleaseSecretBindingsSchema = z
99
+ .array(ServiceReleaseSecretBindingSchema)
100
+ .min(1)
101
+ .refine((bindings) => new Set(bindings.map((binding) => `${binding.target.name}/${binding.target.key}`)).size ===
102
+ bindings.length, 'each Kubernetes Secret key must have exactly one source binding');
103
+ export const ServiceReleaseEnvSchema = z.discriminatedUnion('source', [
104
+ z.object({ source: z.literal('literal'), name: z.string().regex(/^[A-Z_][A-Z0-9_]*$/), value: z.string() }),
105
+ z.object({
106
+ source: z.literal('secret'),
107
+ name: z.string().regex(/^[A-Z_][A-Z0-9_]*$/),
108
+ secretRef: ServiceReleaseSecretRefSchema
109
+ })
110
+ ]).superRefine((entry, ctx) => {
111
+ if (entry.source === 'literal' &&
112
+ /(SECRET|TOKEN|PASSWORD|PRIVATE_KEY|API_KEY)/.test(entry.name) &&
113
+ !/(DIR|PATH|FILE|URL|SCHEME)$/.test(entry.name)) {
114
+ ctx.addIssue({ code: 'custom', message: 'secret-shaped environment variables must use a secret reference' });
115
+ }
116
+ });
117
+ export const ServiceReleaseSecretMountSchema = z.object({
118
+ secretName: DnsLabelSchema,
119
+ mountPath: z.string().startsWith('/'),
120
+ /** Secret keys projected as files (each file named by its key). */
121
+ keys: z.array(z.string().min(1)).optional(),
122
+ readOnly: z.literal(true).default(true)
123
+ });
124
+ const ContainerResourcesSchema = z.object({
125
+ requests: z.object({ cpu: z.string().min(1), memory: z.string().min(1) }),
126
+ limits: z.object({ cpu: z.string().min(1), memory: z.string().min(1) })
127
+ });
128
+ /**
129
+ * Environment variables owned by the managed gateway topology.
130
+ * These names are reserved because the gateway container sets them
131
+ * from its own configuration and host-key authority.
132
+ */
133
+ const GatewayReservedEnv = new Set([
134
+ 'GITD_ROOT',
135
+ 'GITD_SESSION_URL',
136
+ 'GIT_SSH_GATEWAY_PORT',
137
+ 'VIBES_SSH_HOST_KEY',
138
+ 'VIBES_SSH_HOST_KEY_FILE'
139
+ ]);
140
+ export const ServiceReleaseGatewaySchema = z.object({
141
+ name: DnsLabelSchema,
142
+ image: ImageReferenceSchema,
143
+ revision: RevisionSchema,
144
+ replicas: z.number().int().min(2),
145
+ port: z.literal(22).default(22),
146
+ listenPort: z.number().int().min(1024).max(65535).default(2222),
147
+ /** Public gateway hostname. Must be a valid DNS name / FQDN. */
148
+ hostname: z.string().regex(/^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/, 'must be a valid DNS hostname'),
149
+ hostKeyAuthority: z.object({
150
+ publicKey: z.string().regex(/^ssh-ed25519 [A-Za-z0-9+/]+={0,2}(?: [^\r\n]+)?$/),
151
+ fingerprint: z.string().regex(/^SHA256:[A-Za-z0-9+/]{43}$/),
152
+ source: z.object({
153
+ repository: z.string().regex(/^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9-]*$/),
154
+ path: z.string().min(1).refine((path) => !path.startsWith('/') && !path.split('/').includes('..'), 'must be a repository-relative path')
155
+ }),
156
+ secretRef: z.object({
157
+ name: DnsLabelSchema,
158
+ privateKeyKey: z.string().min(1),
159
+ publicKeyKey: z.string().min(1),
160
+ mountPath: z.string().startsWith('/')
161
+ }),
162
+ provisioning: z.object({
163
+ owner: z.literal('secret-management'),
164
+ mode: z.literal('external-preprovisioned')
165
+ }),
166
+ rotation: z.object({
167
+ generation: z.number().int().positive(),
168
+ strategy: z.literal('preprovisioned-atomic'),
169
+ previousFingerprint: z.string().regex(/^SHA256:[A-Za-z0-9+/]{43}$/).optional(),
170
+ knownHostsOverlapUntil: z.string().datetime().optional()
171
+ }).superRefine((rotation, ctx) => {
172
+ if (rotation.generation > 1 && (!rotation.previousFingerprint || !rotation.knownHostsOverlapUntil)) {
173
+ ctx.addIssue({ code: 'custom', message: 'rotations after generation 1 require the prior fingerprint and a known-hosts overlap deadline' });
174
+ }
175
+ })
176
+ }),
177
+ env: z.array(ServiceReleaseEnvSchema).default([]),
178
+ secretMounts: z.array(ServiceReleaseSecretMountSchema).default([]),
179
+ resources: ContainerResourcesSchema,
180
+ maxUnavailable: z.literal(0).default(0),
181
+ maxSurge: z.number().int().positive().default(1)
182
+ }).superRefine((gateway, ctx) => {
183
+ for (const [index, env] of gateway.env.entries()) {
184
+ if (GatewayReservedEnv.has(env.name)) {
185
+ ctx.addIssue({ code: 'custom', path: ['env', index, 'name'], message: `${env.name} is owned by the managed gateway topology` });
186
+ }
187
+ }
188
+ });
189
+ export const ServiceReleaseControlPlaneRouterSchema = z.object({
190
+ image: ImageReferenceSchema,
191
+ revision: RevisionSchema,
192
+ publicPort: z.literal(443),
193
+ listenPort: z.number().int().min(1024).max(65535).default(8443),
194
+ tlsSecretRef: z.object({
195
+ name: DnsLabelSchema,
196
+ certificateKey: z.string().min(1).default('tls.crt'),
197
+ privateKeyKey: z.string().min(1).default('tls.key')
198
+ }),
199
+ smartGit: z.object({
200
+ enabled: z.literal(true),
201
+ authorizationOwner: z.literal('stateful'),
202
+ pathContract: z.literal('owner/repo.git')
203
+ }).default({ enabled: true, authorizationOwner: 'stateful', pathContract: 'owner/repo.git' }),
204
+ allowedPathPrefixes: z
205
+ .array(z
206
+ .string()
207
+ .regex(/^\/(?:[^/]+\/)*$/, 'must start and end with / and contain no .. segments')
208
+ .refine((prefix) => prefix !== '/', 'admin path prefixes must name a bounded subtree, never the root')
209
+ .refine((prefix) => !prefix.split('/').some((segment) => segment === '.' || segment === '..'), 'admin path prefixes must not contain . or .. segments'))
210
+ .min(1)
211
+ .max(8)
212
+ .refine((prefixes) => new Set(prefixes).size === prefixes.length, 'path prefixes must be unique'),
213
+ /**
214
+ * When true, the router proxies repo-scoped browse requests to the stateful
215
+ * service. The caller must declare this explicitly if the router plane offers
216
+ * it; the default is true because a service release normally serves browse.
217
+ */
218
+ proxyRepoBrowse: z.boolean().default(true),
219
+ /**
220
+ * When true, the router proxies repo-scoped merge-control requests to the
221
+ * stateful service. The caller must declare this explicitly if the router
222
+ * plane offers it; the default is true because a service release normally
223
+ * serves merge control.
224
+ */
225
+ proxyMergeControl: z.boolean().default(true),
226
+ /**
227
+ * Destination URL for requests that match no declared route. When set, the
228
+ * router redirects unmatched requests here (e.g. a human-facing UI). Omit to
229
+ * respond 404. Must be a valid, credential-free URL.
230
+ */
231
+ fallbackRedirect: z.url().optional(),
232
+ resources: ContainerResourcesSchema
233
+ });
234
+ export const ServiceReleaseRepositoryMigrationSchema = z.object({
235
+ mode: z.literal('mirror-ssh'),
236
+ migrationId: z.string().min(1).max(63).regex(/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/),
237
+ source: z.object({
238
+ origin: z.url().refine((value) => {
239
+ const parsed = urlOf(value);
240
+ return parsed !== undefined && parsed.protocol === 'ssh:' && parsed.username === 'git' && !parsed.password &&
241
+ (!parsed.port || parsed.port === '22') &&
242
+ parsed.pathname === '/' && !parsed.search && !parsed.hash;
243
+ }, 'migration source must be a credential-free SSH origin with the git user and no port or port 22'),
244
+ inventoryUrl: z.url().refine((value) => {
245
+ const parsed = urlOf(value);
246
+ return parsed !== undefined && parsed.protocol === 'https:' && !parsed.port &&
247
+ parsed.pathname === '/api/internal/repositories/migration-inventory' &&
248
+ !parsed.username && !parsed.password && !parsed.search && !parsed.hash;
249
+ }, 'migration inventory must be a credential-free HTTPS URL with the exact path contract'),
250
+ sshLeaseUrl: z.url().refine((value) => {
251
+ const parsed = urlOf(value);
252
+ return parsed !== undefined && parsed.protocol === 'https:' && !parsed.port &&
253
+ !parsed.username && !parsed.password &&
254
+ parsed.pathname === '/api/internal/forge-migrations/ssh-leases' &&
255
+ !parsed.search && !parsed.hash;
256
+ }, 'migration SSH lease URL must be a credential-free HTTPS URL with the exact path contract'),
257
+ inventoryAuthoritySecretRef: ServiceReleaseSecretRefSchema
258
+ }).superRefine((source, ctx) => {
259
+ const inventory = urlOf(source.inventoryUrl);
260
+ const lease = urlOf(source.sshLeaseUrl);
261
+ if (inventory !== undefined && lease !== undefined && inventory.origin !== lease.origin) {
262
+ ctx.addIssue({
263
+ code: 'custom',
264
+ path: ['sshLeaseUrl'],
265
+ message: 'migration inventory and SSH lease must share one credential-free authority origin'
266
+ });
267
+ }
268
+ }),
269
+ expected: z.object({
270
+ repositoryCount: z.number().int().positive().max(10_000),
271
+ owners: z.array(z.object({
272
+ ownerType: z.enum(['user', 'organization']),
273
+ ownerId: z.string().min(1).max(128),
274
+ repositoryCount: z.number().int().positive()
275
+ })).min(1).max(128)
276
+ }).superRefine((expected, ctx) => {
277
+ const ownerKeys = expected.owners.map((owner) => `${owner.ownerType}:${owner.ownerId}`);
278
+ if (new Set(ownerKeys).size !== ownerKeys.length) {
279
+ ctx.addIssue({ code: 'custom', path: ['owners'], message: 'migration owner scopes must be unique' });
280
+ }
281
+ if (expected.owners.reduce((sum, owner) => sum + owner.repositoryCount, 0) !== expected.repositoryCount) {
282
+ ctx.addIssue({ code: 'custom', path: ['owners'], message: 'migration owner counts must sum to the exact repository count' });
283
+ }
284
+ }),
285
+ receipt: z.object({ configMapName: DnsLabelSchema }),
286
+ convergence: z.object({ stablePasses: z.literal(2) }),
287
+ rollback: z.object({ mode: z.literal('source-authoritative-until-public-cutover') })
288
+ });
289
+ export const ServiceReleaseStatefulSchema = z.object({
290
+ name: DnsLabelSchema,
291
+ exposure: z.literal('internal').default('internal'),
292
+ adminExposure: z.literal('internal').default('internal'),
293
+ image: ImageReferenceSchema,
294
+ revision: RevisionSchema,
295
+ replicas: z.literal(1),
296
+ port: z.number().int().min(1).max(65535),
297
+ storage: z.object({
298
+ mountPath: z.string().startsWith('/'),
299
+ size: z.string().regex(/^\d+(?:Mi|Gi|Ti)$/),
300
+ accessMode: z.literal('ReadWriteOnce'),
301
+ storageClassName: z.string().min(1).optional(),
302
+ schemaVersion: z.number().int().positive(),
303
+ releaseCompatibility: z.object({
304
+ readFrom: z.number().int().positive(),
305
+ readThrough: z.number().int().positive(),
306
+ writeVersion: z.number().int().positive()
307
+ })
308
+ }),
309
+ backup: z.discriminatedUnion('mode', [z.object({
310
+ mode: z.literal('restic-s3'),
311
+ toolImage: ImageReferenceSchema,
312
+ repository: z.string().startsWith('s3:https://').refine((value) => {
313
+ const url = urlOf(value.slice(3));
314
+ return url !== undefined && !url.username && !url.password && url.pathname.length > 1;
315
+ }, 'must be a credential-free S3-compatible restic repository URL'),
316
+ credentials: z.object({
317
+ accessKeyId: CredentialReferenceSchema,
318
+ secretAccessKey: CredentialReferenceSchema,
319
+ encryptionKey: CredentialReferenceSchema
320
+ }),
321
+ schedule: CronScheduleSchema,
322
+ retention: z.object({
323
+ keepDaily: z.number().int().positive(),
324
+ keepWeekly: z.number().int().positive(),
325
+ keepMonthly: z.number().int().positive()
326
+ }),
327
+ restoreVerify: z.object({
328
+ schedule: CronScheduleSchema,
329
+ scratchSize: z.string().regex(/^\d+(?:Mi|Gi|Ti)$/),
330
+ timeoutSeconds: z.number().int().positive().max(86400)
331
+ })
332
+ }).superRefine((backup, ctx) => {
333
+ for (const [name, ref] of Object.entries(backup.credentials)) {
334
+ if (!ref.store)
335
+ ctx.addIssue({ code: 'custom', path: ['credentials', name], message: 'DOKS backup CredentialReferences require a preprovisioned Kubernetes Secret store name' });
336
+ }
337
+ }), z.object({
338
+ mode: z.literal('csi-volume-snapshot'),
339
+ toolImage: ImageReferenceSchema,
340
+ snapshotClassName: DnsLabelSchema,
341
+ deletionPolicy: z.literal('Delete'),
342
+ schedule: CronScheduleSchema,
343
+ retention: z.object({
344
+ verifiedGenerations: z.number().int().min(2)
345
+ }),
346
+ restoreVerify: z.object({
347
+ schedule: CronScheduleSchema,
348
+ scratchSize: z.string().regex(/^\d+(?:Mi|Gi|Ti)$/),
349
+ timeoutSeconds: z.number().int().positive().max(86400)
350
+ })
351
+ })]),
352
+ repositoryMigration: ServiceReleaseRepositoryMigrationSchema.optional(),
353
+ env: z.array(ServiceReleaseEnvSchema).default([]),
354
+ secretMounts: z.array(ServiceReleaseSecretMountSchema).default([]),
355
+ resources: ContainerResourcesSchema
356
+ }).superRefine((stateful, ctx) => {
357
+ const reserved = new Set(['GITD_HOST', 'GITD_PORT', 'GITD_ROOT']);
358
+ for (const [index, env] of stateful.env.entries()) {
359
+ if (reserved.has(env.name)) {
360
+ ctx.addIssue({ code: 'custom', path: ['env', index, 'name'], message: `${env.name} is owned by the managed stateful topology` });
361
+ }
362
+ }
363
+ });
364
+ export const ServiceReleaseNetworkPolicySchema = z.object({
365
+ defaultDenyIngress: z.literal(true),
366
+ allowGatewayToStateful: z.literal(true),
367
+ egress: z.object({
368
+ defaultDeny: z.literal(true),
369
+ platformAuthCidrs: BoundedIpv4CidrListSchema,
370
+ objectBackupCidrs: BoundedIpv4CidrListSchema.optional(),
371
+ objectBackupHostnames: EgressHostnameListSchema.optional()
372
+ }).superRefine((egress, ctx) => {
373
+ if (Boolean(egress.objectBackupCidrs) && Boolean(egress.objectBackupHostnames)) {
374
+ ctx.addIssue({
375
+ code: 'custom',
376
+ message: 'object backup egress may declare only one of bounded CIDRs or Cilium FQDN hostnames'
377
+ });
378
+ }
379
+ })
380
+ });
381
+ /**
382
+ * The provider-neutral shape of a release target: an adapter id plus whatever
383
+ * that adapter's own integration package validates once it resolves the target.
384
+ */
385
+ export const ServiceReleaseTargetSchema = z.looseObject({ adapter: z.string().min(1) });
386
+ export const ServiceReleaseNamespaceQuotaSchema = z.object({
387
+ name: DnsLabelSchema,
388
+ hard: z.object({
389
+ 'limits.cpu': z.string().regex(/^\d+(?:m)?$/),
390
+ 'limits.memory': StorageQuantitySchema,
391
+ persistentvolumeclaims: z.string().regex(/^\d+$/),
392
+ pods: z.string().regex(/^\d+$/),
393
+ 'requests.cpu': z.string().regex(/^\d+(?:m)?$/),
394
+ 'requests.memory': StorageQuantitySchema,
395
+ 'requests.storage': StorageQuantitySchema
396
+ })
397
+ });
398
+ export const ServiceReleaseBackupStorageSchema = z.object({
399
+ adapter: z.literal('s3-compatible'),
400
+ authority: z.object({
401
+ provider: z.string().min(1),
402
+ name: z.string().regex(/^[a-z0-9][a-z0-9.-]{1,62}[a-z0-9]$/, 'must be a stable object-storage key name'),
403
+ requiredControlPlaneScopes: z.array(z.string().min(1)).min(1)
404
+ }),
405
+ bucket: z.string()
406
+ .min(3)
407
+ .max(63)
408
+ .regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])$/, 'must be a lowercase S3-compatible bucket name')
409
+ .refine((value) => !/^\d{1,3}(?:\.\d{1,3}){3}$/.test(value), 'must not be formatted as an IP address'),
410
+ region: z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/, 'must be a source-owned S3-compatible region'),
411
+ endpoint: z.url().refine((value) => {
412
+ const parsed = urlOf(value);
413
+ return parsed !== undefined && parsed.protocol === 'https:' &&
414
+ !parsed.username &&
415
+ !parsed.password &&
416
+ parsed.pathname === '/' &&
417
+ !parsed.search &&
418
+ !parsed.hash;
419
+ }, 'must be a credential-free HTTPS origin'),
420
+ forcePathStyle: z.literal(true),
421
+ versioning: z.literal('Enabled'),
422
+ publicAccess: z.literal('private')
423
+ });
424
+ /** The release config: an open adapter object plus the provider-neutral parts this kind checks across fields. */
425
+ export const ServiceReleaseConfigSchema = ServiceReleaseTargetSchema.extend({
426
+ dnsOwnerRef: z.string().regex(/^infra\/dns:[a-z0-9.-]+$/),
427
+ namespaceQuota: ServiceReleaseNamespaceQuotaSchema.optional(),
428
+ backupStorage: ServiceReleaseBackupStorageSchema.optional(),
429
+ certificate: z.object({
430
+ manager: z.literal('cert-manager'),
431
+ issuerRef: z.object({
432
+ kind: z.union([z.literal('ClusterIssuer'), z.literal('Issuer')]),
433
+ name: DnsLabelSchema
434
+ }),
435
+ provisioning: z.object({
436
+ owner: z.literal('service-release'),
437
+ mode: z.literal('namespace-acme-dns01'),
438
+ server: z.url().refine((value) => {
439
+ const parsed = urlOf(value);
440
+ return parsed !== undefined && parsed.protocol === 'https:' && !parsed.username && !parsed.password;
441
+ }, 'ACME server must be a credential-free HTTPS URL'),
442
+ email: z.email(),
443
+ privateKeySecretRef: z.object({ name: DnsLabelSchema }),
444
+ solver: z.object({
445
+ provider: z.string().min(1),
446
+ dnsZones: DnsZoneListSchema,
447
+ apiToken: z.object({
448
+ environment: z.literal('local'),
449
+ sourceKeys: z.array(SecretStoreKeySchema).min(1).max(4)
450
+ .refine((keys) => new Set(keys).size === keys.length, 'certificate solver source keys must be unique'),
451
+ target: z.object({ name: DnsLabelSchema, key: z.string().min(1) })
452
+ })
453
+ })
454
+ }).optional(),
455
+ duration: z.literal('2160h').default('2160h'),
456
+ renewBefore: z.literal('360h').default('360h'),
457
+ privateKey: z.object({
458
+ algorithm: z.literal('ECDSA').default('ECDSA'),
459
+ size: z.literal(256).default(256),
460
+ rotationPolicy: z.literal('Always').default('Always')
461
+ }).default({ algorithm: 'ECDSA', size: 256, rotationPolicy: 'Always' })
462
+ }).optional()
463
+ });
464
+ export const ServiceReleaseDescriptorSchema = RuntimeDescriptorSchema.extend({
465
+ id: DnsLabelSchema,
466
+ kind: z.literal('infra/service-release'),
467
+ namespace: DnsLabelSchema,
468
+ gateway: ServiceReleaseGatewaySchema,
469
+ controlPlaneRouter: ServiceReleaseControlPlaneRouterSchema,
470
+ stateful: ServiceReleaseStatefulSchema,
471
+ secretBindings: ServiceReleaseSecretBindingsSchema,
472
+ networkPolicy: ServiceReleaseNetworkPolicySchema,
473
+ config: ServiceReleaseConfigSchema
474
+ }).superRefine((descriptor, ctx) => {
475
+ if (descriptor.gateway.name === descriptor.stateful.name) {
476
+ ctx.addIssue({ code: 'custom', message: 'gateway and stateful workload names must differ' });
477
+ }
478
+ if (descriptor.gateway.listenPort === descriptor.controlPlaneRouter.listenPort) {
479
+ ctx.addIssue({
480
+ code: 'custom',
481
+ path: ['controlPlaneRouter', 'listenPort'],
482
+ message: 'SSH and HTTPS listeners share one pod network and must use different ports'
483
+ });
484
+ }
485
+ const compatibility = descriptor.stateful.storage.releaseCompatibility;
486
+ const schemaVersion = descriptor.stateful.storage.schemaVersion;
487
+ if (compatibility.readFrom > schemaVersion || compatibility.readThrough < schemaVersion) {
488
+ ctx.addIssue({
489
+ code: 'custom',
490
+ path: ['stateful', 'storage', 'releaseCompatibility'],
491
+ message: 'the release must be able to read the declared PVC schema version'
492
+ });
493
+ }
494
+ if (compatibility.writeVersion !== schemaVersion) {
495
+ ctx.addIssue({ code: 'custom', path: ['stateful', 'storage', 'releaseCompatibility', 'writeVersion'], message: 'service-release does not perform storage migrations; writeVersion must equal the declared PVC schema version' });
496
+ }
497
+ if (descriptor.config.dnsOwnerRef !== `infra/dns:${descriptor.gateway.hostname}`) {
498
+ ctx.addIssue({
499
+ code: 'custom',
500
+ path: ['config', 'dnsOwnerRef'],
501
+ message: 'DNS owner reference must exactly name the public gateway hostname'
502
+ });
503
+ }
504
+ const namespaceQuota = descriptor.config.namespaceQuota;
505
+ if (namespaceQuota) {
506
+ const primaryMi = storageQuantityMi(descriptor.stateful.storage.size);
507
+ const scratchMi = storageQuantityMi(descriptor.stateful.backup.restoreVerify.scratchSize);
508
+ const declaredMi = storageQuantityMi(namespaceQuota.hard['requests.storage']);
509
+ // A quantity its own field refused is already an issue; the budget is checked only over readable ones.
510
+ if (primaryMi !== undefined && scratchMi !== undefined && declaredMi !== undefined && declaredMi < primaryMi * 2 + scratchMi) {
511
+ ctx.addIssue({
512
+ code: 'custom',
513
+ path: ['config', 'namespaceQuota', 'hard', 'requests.storage'],
514
+ message: 'namespace storage quota must fit the primary, verified restore scratch, and one emergency replacement claim'
515
+ });
516
+ }
517
+ }
518
+ const certificate = descriptor.config.certificate;
519
+ if (certificate?.issuerRef.kind === 'Issuer' && !certificate.provisioning) {
520
+ ctx.addIssue({
521
+ code: 'custom',
522
+ path: ['config', 'certificate', 'provisioning'],
523
+ message: 'a namespace Issuer requires source-owned service-release provisioning'
524
+ });
525
+ }
526
+ if (certificate?.issuerRef.kind === 'ClusterIssuer' && certificate.provisioning) {
527
+ ctx.addIssue({
528
+ code: 'custom',
529
+ path: ['config', 'certificate', 'provisioning'],
530
+ message: 'service-release certificate authority provisioning is namespace-scoped and cannot own a ClusterIssuer'
531
+ });
532
+ }
533
+ if (certificate?.provisioning &&
534
+ !certificate.provisioning.solver.dnsZones.some((zone) => descriptor.gateway.hostname === zone || descriptor.gateway.hostname.endsWith(`.${zone}`))) {
535
+ ctx.addIssue({
536
+ code: 'custom',
537
+ path: ['config', 'certificate', 'provisioning', 'solver', 'dnsZones'],
538
+ message: 'certificate solver DNS zones must own the public gateway hostname'
539
+ });
540
+ }
541
+ if (descriptor.config.backupStorage) {
542
+ const storage = descriptor.config.backupStorage;
543
+ if (descriptor.stateful.backup.mode !== 'restic-s3') {
544
+ ctx.addIssue({
545
+ code: 'custom',
546
+ path: ['config', 'backupStorage'],
547
+ message: 'S3 backup storage may be bound only to a restic-s3 backup'
548
+ });
549
+ }
550
+ else {
551
+ const endpoint = urlOf(storage.endpoint);
552
+ const expectedPrefix = endpoint === undefined ? undefined : `s3:${endpoint.origin}/${storage.bucket}/`;
553
+ if (expectedPrefix !== undefined && !descriptor.stateful.backup.repository.startsWith(expectedPrefix)) {
554
+ ctx.addIssue({
555
+ code: 'custom',
556
+ path: ['stateful', 'backup', 'repository'],
557
+ message: `backup repository must live below declared storage ${expectedPrefix}`
558
+ });
559
+ }
560
+ const allowedHostnames = descriptor.networkPolicy.egress.objectBackupHostnames;
561
+ if (endpoint !== undefined && allowedHostnames && !allowedHostnames.includes(endpoint.hostname)) {
562
+ ctx.addIssue({
563
+ code: 'custom',
564
+ path: ['networkPolicy', 'egress', 'objectBackupHostnames'],
565
+ message: `backup storage endpoint hostname ${endpoint.hostname} must be explicitly allowed`
566
+ });
567
+ }
568
+ }
569
+ }
570
+ const hasObjectBackupEgress = Boolean(descriptor.networkPolicy.egress.objectBackupCidrs ||
571
+ descriptor.networkPolicy.egress.objectBackupHostnames);
572
+ if (descriptor.stateful.backup.mode === 'restic-s3' && !hasObjectBackupEgress) {
573
+ ctx.addIssue({
574
+ code: 'custom',
575
+ path: ['networkPolicy', 'egress'],
576
+ message: 'restic-s3 backup requires bounded object-backup egress'
577
+ });
578
+ }
579
+ if (descriptor.stateful.backup.mode === 'csi-volume-snapshot' && hasObjectBackupEgress) {
580
+ ctx.addIssue({
581
+ code: 'custom',
582
+ path: ['networkPolicy', 'egress'],
583
+ message: 'CSI snapshot backup must not retain object-backup egress'
584
+ });
585
+ }
586
+ if (descriptor.stateful.backup.mode === 'csi-volume-snapshot' &&
587
+ !descriptor.stateful.storage.storageClassName) {
588
+ ctx.addIssue({
589
+ code: 'custom',
590
+ path: ['stateful', 'storage', 'storageClassName'],
591
+ message: 'CSI snapshot restore verification requires an explicit storage class'
592
+ });
593
+ }
594
+ const repositoryMigration = descriptor.stateful.repositoryMigration;
595
+ if (repositoryMigration && urlOf(repositoryMigration.source.origin)?.hostname !== descriptor.gateway.hostname) {
596
+ ctx.addIssue({
597
+ code: 'custom',
598
+ path: ['stateful', 'repositoryMigration', 'source', 'origin'],
599
+ message: 'repository migration source must be the declared public gateway hostname'
600
+ });
601
+ }
602
+ const targetKey = (name, key) => `${name}/${key}`;
603
+ const requiredTargets = new Set();
604
+ const publicFileTargets = new Set();
605
+ const hostSecret = descriptor.gateway.hostKeyAuthority.secretRef;
606
+ requiredTargets.add(targetKey(hostSecret.name, hostSecret.privateKeyKey));
607
+ requiredTargets.add(targetKey(hostSecret.name, hostSecret.publicKeyKey));
608
+ publicFileTargets.add(targetKey(hostSecret.name, hostSecret.publicKeyKey));
609
+ const routerSecret = descriptor.controlPlaneRouter.tlsSecretRef;
610
+ if (!descriptor.config.certificate) {
611
+ requiredTargets.add(targetKey(routerSecret.name, routerSecret.certificateKey));
612
+ requiredTargets.add(targetKey(routerSecret.name, routerSecret.privateKeyKey));
613
+ publicFileTargets.add(targetKey(routerSecret.name, routerSecret.certificateKey));
614
+ }
615
+ for (const env of [...descriptor.gateway.env, ...descriptor.stateful.env]) {
616
+ if (env.source === 'secret')
617
+ requiredTargets.add(targetKey(env.secretRef.name, env.secretRef.key));
618
+ }
619
+ for (const mount of [...descriptor.gateway.secretMounts, ...descriptor.stateful.secretMounts]) {
620
+ for (const key of mount.keys ?? [])
621
+ requiredTargets.add(targetKey(mount.secretName, key));
622
+ }
623
+ if (descriptor.stateful.backup.mode === 'restic-s3') {
624
+ for (const credential of Object.values(descriptor.stateful.backup.credentials)) {
625
+ requiredTargets.add(targetKey(credential.store, credential.key));
626
+ }
627
+ }
628
+ if (repositoryMigration) {
629
+ requiredTargets.add(targetKey(repositoryMigration.source.inventoryAuthoritySecretRef.name, repositoryMigration.source.inventoryAuthoritySecretRef.key));
630
+ }
631
+ const boundTargets = new Set(descriptor.secretBindings.map((binding) => targetKey(binding.target.name, binding.target.key)));
632
+ for (const required of requiredTargets) {
633
+ if (!boundTargets.has(required)) {
634
+ ctx.addIssue({
635
+ code: 'custom',
636
+ path: ['secretBindings'],
637
+ message: `missing source binding for Kubernetes Secret key ${required}`
638
+ });
639
+ }
640
+ }
641
+ for (const [index, binding] of descriptor.secretBindings.entries()) {
642
+ const target = targetKey(binding.target.name, binding.target.key);
643
+ if (!requiredTargets.has(target)) {
644
+ ctx.addIssue({
645
+ code: 'custom',
646
+ path: ['secretBindings', index, 'target'],
647
+ message: `binding target ${target} is not consumed by this service release`
648
+ });
649
+ }
650
+ if (binding.source.kind === 'repository-file' && !publicFileTargets.has(target)) {
651
+ ctx.addIssue({
652
+ code: 'custom',
653
+ path: ['secretBindings', index, 'source'],
654
+ message: 'repository files may source only public host-key or TLS-certificate material'
655
+ });
656
+ }
657
+ }
658
+ });
659
+ class DefaultServiceReleaseImplementation {
660
+ descriptor;
661
+ context;
662
+ /** The descriptor as this kind's schema reads it. */
663
+ config;
664
+ constructor(descriptor, context) {
665
+ this.descriptor = descriptor;
666
+ this.context = context;
667
+ this.config = readKindDescriptor(ServiceReleaseDescriptorSchema, descriptor);
668
+ }
669
+ }
670
+ export const serviceReleaseKind = createRuntimeKind({
671
+ id: 'infra/service-release',
672
+ descriptorSchema: ServiceReleaseDescriptorSchema,
673
+ defaultImplementation: DefaultServiceReleaseImplementation,
674
+ discoveryPatterns: INFRA_DISCOVERY_PATTERNS
675
+ });
676
+ //# sourceMappingURL=service-release.kind.js.map