@underpostnet/cyberia 3.3.73 → 3.3.77

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 (269) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +10 -0
  3. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  4. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  5. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  8. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  9. package/.github/workflows/engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/publish.cyberia.ci.yml +44 -0
  11. package/AGENTS.md +61 -12
  12. package/CHANGELOG.md +159 -161
  13. package/CLI-HELP.md +20 -13
  14. package/Dockerfile +1 -1
  15. package/Dockerfile.dev +1 -1
  16. package/Dockerfile.test +2 -2
  17. package/README.md +28 -28
  18. package/bin/build.js +96 -21
  19. package/bin/cyberia.js +1247 -1373
  20. package/bin/index.js +1247 -1373
  21. package/compose.env +17 -5
  22. package/conf.js +7 -4
  23. package/deploy/cyberia-client/deploy.sh +10 -11
  24. package/deploy/cyberia-client/package.sh +0 -1
  25. package/deploy/cyberia-client/state.sh +3 -2
  26. package/deploy/cyberia-server/deploy.sh +10 -11
  27. package/deploy/cyberia-server/package.sh +0 -1
  28. package/deploy/cyberia-server/state.sh +3 -2
  29. package/deploy/dd-cyberia/deploy.sh +5 -5
  30. package/deploy/dd-cyberia/init.sh +15 -14
  31. package/deploy/dd-cyberia/package.sh +0 -1
  32. package/deploy/dd-cyberia/state.sh +2 -2
  33. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  34. package/deploy/lib/config.sh +29 -0
  35. package/deploy/lib/github-actions-logging.sh +92 -76
  36. package/deploy/lib/host.sh +28 -19
  37. package/deploy/lib/state.sh +4 -1
  38. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  39. package/deploy/release/deploy.sh +0 -2
  40. package/deployment.yaml +1 -1
  41. package/docker-compose.yml +23 -17
  42. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  43. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  44. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  45. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  46. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  47. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  48. package/docs/coverage/cyberia/base.css +362 -0
  49. package/docs/coverage/cyberia/block-navigation.js +82 -0
  50. package/docs/coverage/cyberia/favicon.png +0 -0
  51. package/docs/coverage/cyberia/index.html +161 -0
  52. package/docs/coverage/cyberia/prettify.css +101 -0
  53. package/docs/coverage/cyberia/prettify.js +937 -0
  54. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  55. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  56. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  57. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  58. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  59. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  60. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  61. package/docs/coverage/cyberia/sorter.js +205 -0
  62. package/hardhat/package-lock.json +2 -2
  63. package/hardhat/package.json +1 -1
  64. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  65. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  66. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  67. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  68. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  69. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  70. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  71. package/manifests/deployment/mongo-express/service.yaml +16 -0
  72. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  73. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  74. package/package.json +19 -17
  75. package/scripts/test-monitor.sh +3 -3
  76. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  77. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  78. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  79. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  80. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  81. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  82. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  83. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  84. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  85. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  86. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  87. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  88. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  89. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  90. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  91. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  92. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  93. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  94. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  95. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  96. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  97. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  98. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  99. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  100. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  101. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  102. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  103. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  104. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  105. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  106. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  107. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  108. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  109. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  110. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  111. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  112. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  113. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  114. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  115. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  116. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  117. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  118. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  119. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  120. package/src/api/file/file.ref.js +153 -0
  121. package/src/api/file/file.ref.json +11 -2
  122. package/src/api/object-layer/object-layer.model.js +47 -12
  123. package/src/api/object-layer/object-layer.router.js +10 -3
  124. package/src/api/object-layer/object-layer.service.js +113 -120
  125. package/src/cli/app.js +12 -1
  126. package/src/cli/client.js +2 -2
  127. package/src/cli/cluster.js +80 -18
  128. package/src/cli/deploy.js +215 -34
  129. package/src/cli/docker-compose.js +1 -1
  130. package/src/cli/fs-selection.js +115 -0
  131. package/src/cli/fs.js +158 -342
  132. package/src/cli/index.js +22 -10
  133. package/src/cli/release.js +3 -1
  134. package/src/cli/repository.js +35 -9
  135. package/src/cli/run.js +80 -199
  136. package/src/cli/test.js +4 -0
  137. package/src/cli/wireguard.js +32 -31
  138. package/src/client/components/core/Badge.js +0 -1
  139. package/src/client/components/core/CssCore.js +8 -1
  140. package/src/client/components/core/Docs.js +77 -100
  141. package/src/client/components/core/Modal.js +77 -23
  142. package/src/client/components/core/ToolTip.js +15 -0
  143. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  144. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  145. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  146. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  147. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  148. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  149. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  150. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  151. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  152. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  153. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  154. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  155. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  156. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  157. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  158. package/src/client/components/default/AppShellDefault.js +1 -4
  159. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  160. package/src/client/components/underpost/AppShellUnderpost.js +1 -1
  161. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  162. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  163. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  164. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  165. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  166. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  167. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  168. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  169. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  170. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  171. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  172. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  173. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  174. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  175. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  176. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  177. package/src/client/services/object-layer/object-layer.management.js +6 -10
  178. package/src/client/services/object-layer/object-layer.service.js +3 -1
  179. package/src/client/ssr/views/Cyberia404.js +3 -3
  180. package/src/client-builder/client-build-docs.js +52 -52
  181. package/src/client-builder/client-build.js +67 -44
  182. package/src/client-builder/client-bundle.js +347 -0
  183. package/src/db/mongo/MongoExpress.js +187 -0
  184. package/src/grpc/cyberia/grpc-server.js +5 -7
  185. package/src/index.js +1 -1
  186. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  187. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  188. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  189. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  190. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  191. package/src/projects/cyberia/gemini-client.js +3 -3
  192. package/src/projects/cyberia/generate-saga.js +4 -11
  193. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  194. package/src/projects/cyberia/instance-backup.js +210 -0
  195. package/src/projects/cyberia/instance-data.js +177 -257
  196. package/src/projects/cyberia/map-preview-generator.js +32 -68
  197. package/src/projects/cyberia/object-layer.js +152 -319
  198. package/src/projects/cyberia/seed-audio.js +139 -0
  199. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  200. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  201. package/src/projects/cyberia/stat-balance.js +81 -0
  202. package/src/projects/cyberia/stat-commands.js +23 -0
  203. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  204. package/src/runtime/cyberia-client/Dockerfile +8 -4
  205. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  206. package/src/runtime/cyberia-server/Dockerfile +9 -2
  207. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  208. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  209. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  210. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  211. package/src/runtime/engine-cyberia/compose.env +17 -5
  212. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  213. package/src/server/build/coverage.js +113 -42
  214. package/src/server/build/package.js +55 -12
  215. package/src/server/build/testing.js +59 -9
  216. package/src/server/network/middlewares.js +8 -2
  217. package/src/server/network/underpost-gateway.js +113 -14
  218. package/src/server/ops/logger.js +70 -35
  219. package/src/server/runtime/conf.js +40 -9
  220. package/src/server/runtime/start.js +11 -2
  221. package/src/server/storage/data-query.js +16 -0
  222. package/src/server/storage/downloader.js +62 -14
  223. package/src/server/storage/zip.js +153 -0
  224. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  225. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  226. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  227. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  228. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  229. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  230. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  231. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  232. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  233. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  234. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  235. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  236. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  237. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  238. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  239. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  240. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  241. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  242. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  243. package/test/unit/catalog.test.js +13 -2
  244. package/test/unit/client-build-docs.test.js +97 -70
  245. package/test/unit/client-bundle.test.js +313 -0
  246. package/test/unit/conf-resolution.test.js +58 -7
  247. package/test/unit/coverage-artifact.test.js +120 -29
  248. package/test/unit/cyberia/instance-backup.test.js +183 -0
  249. package/test/unit/cyberia/instance-data.test.js +314 -0
  250. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  251. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  252. package/test/unit/cyberia/stat-balance.test.js +69 -0
  253. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  254. package/test/unit/deploy-log-table.test.js +76 -5
  255. package/test/unit/downloader-integrity.test.js +98 -0
  256. package/test/unit/file-reference-registry.test.js +141 -0
  257. package/test/unit/fs-storage-paths.test.js +596 -0
  258. package/test/unit/gateway-pod-fetch.test.js +95 -0
  259. package/test/unit/letsencrypt-issuer.test.js +101 -0
  260. package/test/unit/logger-redaction.test.js +29 -0
  261. package/test/unit/package.test.js +70 -16
  262. package/test/unit/prepare-host.test.js +84 -1
  263. package/test/unit/release-bump.test.js +3 -1
  264. package/test/unit/start-options.test.js +12 -0
  265. package/test/unit/test-tiers.test.js +27 -0
  266. package/test/unit/zip-archive.test.js +142 -0
  267. package/vitest.config.js +11 -3
  268. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  269. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
package/src/cli/deploy.js CHANGED
@@ -50,6 +50,8 @@ import nodePath from 'node:path';
50
50
  import dotenv from 'dotenv';
51
51
  import os from 'node:os';
52
52
  import crypto from 'node:crypto';
53
+ import { fileURLToPath } from 'node:url';
54
+ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
53
55
  import { appSecretName } from './app.js';
54
56
  import { domainContextFactory } from './domains.js';
55
57
  import Underpost from '../index.js';
@@ -93,6 +95,23 @@ const UPSTREAM_FAILURE_STATUSES = [502, 503, 504];
93
95
 
94
96
  const CONTAINER_ENGINE_ROOT = '/home/dd/engine';
95
97
 
98
+ /**
99
+ * @constant LETSENCRYPT_ISSUER_NAME
100
+ * @description The ClusterIssuer every generated Certificate references.
101
+ * @memberof UnderpostDeploy
102
+ */
103
+ const LETSENCRYPT_ISSUER_NAME = 'letsencrypt-prod';
104
+
105
+ /**
106
+ * @constant LETSENCRYPT_ISSUER_MANIFEST
107
+ * @description Base ClusterIssuer manifest: account, ACME server and key Secret. Resolved
108
+ * from this module so an installed CLI and a checkout read the same file.
109
+ * @memberof UnderpostDeploy
110
+ */
111
+ const LETSENCRYPT_ISSUER_MANIFEST = fileURLToPath(
112
+ new URL(`../../manifests/${LETSENCRYPT_ISSUER_NAME}.yaml`, import.meta.url),
113
+ );
114
+
96
115
  // The manifest set `deploy --build-manifest` produces for one environment.
97
116
  const DEFAULT_MANIFEST_FILES = [
98
117
  'deployment.yaml',
@@ -147,10 +166,10 @@ const GATEWAY_DURATION_UNITS = [
147
166
  ];
148
167
 
149
168
  /**
150
- * Converts an HTTPProxy duration (`300000ms`, `10s`, `infinity`) into a Gateway
169
+ * Converts an HTTPProxy duration (`10000ms`, `10s`, `infinity`) into a Gateway
151
170
  * API Duration. The Gateway API grammar allows at most 5 digits per component,
152
171
  * so a value that overflows in one unit is re-expressed in a coarser one
153
- * (`300000ms` → `5m`); `infinity` maps to `0s`, which disables the timeout.
172
+ * (`10000ms` → `5m`); `infinity` maps to `0s`, which disables the timeout.
154
173
  * @param {string|number} value - Source duration.
155
174
  * @returns {string|null} Gateway API Duration, or null when unset/unparsable.
156
175
  */
@@ -255,12 +274,68 @@ ${buildKindPorts(fromPort, toPort)}`;
255
274
  : Underpost.deploy.trafficServiceYamlFactory({ deployId, env, traffic, namespace, fromPort, toPort });
256
275
  const escaped = `${deployId}-${env}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
257
276
  manifest = manifest.replace(new RegExp(`app: ${escaped}-(?:blue|green)`), `app: ${deployId}-${env}-${traffic}`);
258
- shellExec(`kubectl apply -f - -n ${namespace} <<'EOF'
277
+ logger.info('Applying the traffic Service', { deployId, env, traffic, namespace });
278
+ shellExec(
279
+ `kubectl apply -f - -n ${namespace} <<'EOF'
259
280
  ${manifest}
260
281
  EOF
261
- `);
282
+ `,
283
+ { disableLog: true },
284
+ );
262
285
  return Underpost.deploy.trafficServiceNameFactory({ deployId, env });
263
286
  },
287
+ /**
288
+ * Removes the Gateway API objects an earlier model of this deploy left behind, so
289
+ * they stop competing with the consolidated `<deployId>-<env>` Gateway.
290
+ *
291
+ * The per-host model emitted one Gateway and one `<host>-http3` ClientTrafficPolicy
292
+ * per hostname. Left in place they duplicate that hostname's listeners in the same
293
+ * merged set, and the oldest resource retains traffic. `undefined` / `undefined-http3`
294
+ * is the same problem under a different name: the consolidated objects were briefly
295
+ * emitted with an unresolved host in their metadata, and merged listeners are
296
+ * configured by the oldest policy that targets them.
297
+ *
298
+ * The namespace is listed once and only objects that exist are deleted: a sweep that
299
+ * issued a blind delete per host per kind spent two dozen `kubectl` round-trips per
300
+ * sync on names that were gone after the first migration, and buried the one line
301
+ * that mattered when something was actually removed.
302
+ * @param {object} params
303
+ * @param {string[]} params.hosts - Hostnames the deploy's `conf.server.json` declares.
304
+ * @param {string} params.namespace - Namespace the deploy's Gateway objects live in.
305
+ * @returns {{gateways: string[], policies: string[]}} Names removed, per kind.
306
+ * @memberof UnderpostDeploy
307
+ */
308
+ sweepLegacyGatewayObjects({ hosts = [], namespace = 'default' }) {
309
+ const existing = (kind) =>
310
+ new Set(
311
+ `${
312
+ shellExec(`kubectl get ${kind} -n ${namespace} -o name --ignore-not-found`, {
313
+ stdout: true,
314
+ silent: true,
315
+ silentOnError: true,
316
+ disableLog: true,
317
+ }) ?? ''
318
+ }`
319
+ .split('\n')
320
+ .map((line) => line.trim().split('/').pop())
321
+ .filter(Boolean),
322
+ );
323
+ const legacy = [...new Set(hosts.filter(Boolean)), 'undefined'];
324
+ const gateways = existing('Gateway');
325
+ const policies = existing('ClientTrafficPolicy');
326
+ const removed = {
327
+ gateways: legacy.filter((name) => gateways.has(name)),
328
+ policies: legacy.map((name) => `${name}-http3`).filter((name) => policies.has(name)),
329
+ };
330
+ for (const name of removed.gateways)
331
+ shellExec(`sudo kubectl delete Gateway ${name} -n ${namespace} --ignore-not-found`);
332
+ for (const name of removed.policies)
333
+ shellExec(`sudo kubectl delete ClientTrafficPolicy ${name} -n ${namespace} --ignore-not-found`);
334
+ if (removed.gateways.length > 0 || removed.policies.length > 0)
335
+ logger.warn('Legacy per-host Gateway API objects removed', { namespace, ...removed });
336
+ return removed;
337
+ },
338
+
264
339
  /**
265
340
  * Removes the route kind owned by the inactive ingress stack. The active
266
341
  * route is already published before this runs, so migration never creates a
@@ -617,7 +692,7 @@ spec:
617
692
  * @param {string} options.namespace - Kubernetes namespace for the deployment (defaults to "default").
618
693
  * @param {string} [options.versions] - Comma-separated list of versions to deploy.
619
694
  * @param {string} [options.cmd] - Custom initialization command for deploymentYamlPartsFactory (comma-separated commands).
620
- * @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
695
+ * @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "10000ms", "infinity").
621
696
  * @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
622
697
  * @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
623
698
  * @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
@@ -1112,6 +1187,125 @@ spec:
1112
1187
  kind: ClusterIssuer
1113
1188
  secretName: ${host}`;
1114
1189
  },
1190
+
1191
+ /**
1192
+ * The Gateways an ACME HTTP-01 challenge route must attach to: every Gateway the
1193
+ * namespace holds now, plus the one each routed deploy publishes under, whether or
1194
+ * not it exists yet. A challenge HTTPRoute lists them all as parents; the data plane
1195
+ * attaches it to whichever one carries a listener for the challenged hostname, and
1196
+ * reports the rest as unmatched, which is harmless.
1197
+ *
1198
+ * The union rather than either side alone: the live list covers Gateways a deploy
1199
+ * outside `dd.routes` created, the route table covers a deploy that has not been
1200
+ * applied yet — an issuer that only knew the live list would have to be re-applied
1201
+ * after every first deploy before that deploy's certificate could issue.
1202
+ * @param {object} params
1203
+ * @param {string} [params.namespace] - Namespace the Gateways live in.
1204
+ * @param {string} [params.env] - Environment the routed deploys publish under.
1205
+ * @returns {Array<{name: string, namespace: string}>} Parent references, sorted by name.
1206
+ * @memberof UnderpostDeploy
1207
+ */
1208
+ acmeGatewayParentRefsFactory({ namespace = 'default', env = 'production' } = {}) {
1209
+ const live = `${
1210
+ shellExec(`kubectl get gateway -n ${namespace} -o name --ignore-not-found`, {
1211
+ stdout: true,
1212
+ silent: true,
1213
+ silentOnError: true,
1214
+ disableLog: true,
1215
+ }) ?? ''
1216
+ }`
1217
+ .split('\n')
1218
+ .map((line) => line.trim().split('/').pop())
1219
+ .filter(Boolean);
1220
+ const routed = readDeployRoutes().map((deployId) => Underpost.deploy.gatewayNameFactory({ deployId, env }));
1221
+ return [...new Set([...live, ...routed])].sort().map((name) => ({ name, namespace }));
1222
+ },
1223
+
1224
+ /**
1225
+ * Renders the Let's Encrypt ClusterIssuer for the ingress stack that actually
1226
+ * carries the HTTP-01 challenge.
1227
+ *
1228
+ * The base manifest fixes the account (email, ACME server, key Secret); only the
1229
+ * solver is decided here. With the Gateway API stack, the challenge must be answered
1230
+ * through an HTTPRoute on the deploy's Gateway: an `ingress`-class solver creates an
1231
+ * Ingress nothing on the request path reads, so `/.well-known/acme-challenge/*` is
1232
+ * routed by the application's own HTTPRoute to the workload — a 404 — or, with no
1233
+ * workload behind it, to the maintenance fallback. Every renewal fails that way while
1234
+ * the solver pods sit there ready, and the failure only surfaces when the certificate
1235
+ * expires.
1236
+ * @param {object} params
1237
+ * @param {'gateway'|'ingress'} params.solver - Which stack answers the challenge.
1238
+ * @param {Array<{name: string, namespace: string}>} [params.parentRefs] - Gateways for the `gateway` solver.
1239
+ * @param {string} [params.basePath] - Base ClusterIssuer manifest.
1240
+ * @returns {string} ClusterIssuer YAML.
1241
+ * @throws {Error} When the Gateway solver is requested with no Gateway to attach to.
1242
+ * @memberof UnderpostDeploy
1243
+ */
1244
+ letsEncryptIssuerYamlFactory({ solver, parentRefs = [], basePath = LETSENCRYPT_ISSUER_MANIFEST }) {
1245
+ const issuer = yamlLoad(fs.readFileSync(basePath, 'utf8'));
1246
+ if (solver === 'gateway') {
1247
+ if (parentRefs.length === 0)
1248
+ throw new Error('[cert-manager] the Gateway API HTTP-01 solver needs at least one Gateway parentRef');
1249
+ issuer.spec.acme.solvers = [
1250
+ {
1251
+ http01: {
1252
+ gatewayHTTPRoute: {
1253
+ parentRefs: parentRefs.map(({ name, namespace }) => ({ name, namespace, kind: 'Gateway' })),
1254
+ },
1255
+ },
1256
+ },
1257
+ ];
1258
+ } else issuer.spec.acme.solvers = [{ http01: { ingress: { class: 'contour' } } }];
1259
+ return yamlDump(issuer, { lineWidth: -1, noRefs: true });
1260
+ },
1261
+
1262
+ /**
1263
+ * Whether the Let's Encrypt ClusterIssuer is installed. False both when it was never
1264
+ * applied and when cert-manager's CRDs are absent, which is the same answer here.
1265
+ * @returns {boolean}
1266
+ * @memberof UnderpostDeploy
1267
+ */
1268
+ letsEncryptIssuerExists() {
1269
+ return (
1270
+ `${
1271
+ shellExec(`kubectl get clusterissuer ${LETSENCRYPT_ISSUER_NAME} -o name --ignore-not-found`, {
1272
+ stdout: true,
1273
+ silent: true,
1274
+ silentOnError: true,
1275
+ disableLog: true,
1276
+ }) ?? ''
1277
+ }`.trim().length > 0
1278
+ );
1279
+ },
1280
+
1281
+ /**
1282
+ * Applies the Let's Encrypt ClusterIssuer for the stack in use, so certificates keep
1283
+ * renewing through whichever data plane the deploy's hostnames are served by.
1284
+ * @param {object} params
1285
+ * @param {'gateway'|'ingress'} params.solver - Which stack answers the challenge.
1286
+ * @param {string} [params.namespace] - Namespace the Gateways live in.
1287
+ * @param {string} [params.env] - Environment the routed deploys publish under.
1288
+ * @param {string} [params.basePath] - Base ClusterIssuer manifest.
1289
+ * @returns {{solver: string, parentRefs: Array<{name: string, namespace: string}>}} What was applied.
1290
+ * @memberof UnderpostDeploy
1291
+ */
1292
+ applyLetsEncryptIssuer({ solver, namespace = 'default', env = 'production', basePath } = {}) {
1293
+ const parentRefs = solver === 'gateway' ? Underpost.deploy.acmeGatewayParentRefsFactory({ namespace, env }) : [];
1294
+ const manifest = Underpost.deploy.letsEncryptIssuerYamlFactory({ solver, parentRefs, basePath });
1295
+ const stagePath = `/dev/shm/underpost-${LETSENCRYPT_ISSUER_NAME}.yaml`;
1296
+ fs.writeFileSync(stagePath, manifest, 'utf8');
1297
+ try {
1298
+ shellExec(`sudo kubectl apply -f ${stagePath}`);
1299
+ } finally {
1300
+ fs.removeSync(stagePath);
1301
+ }
1302
+ logger.info("Let's Encrypt ClusterIssuer applied", {
1303
+ issuer: LETSENCRYPT_ISSUER_NAME,
1304
+ solver,
1305
+ gateways: parentRefs.map(({ name }) => name),
1306
+ });
1307
+ return { solver, parentRefs };
1308
+ },
1115
1309
  /**
1116
1310
  * Mirrors each deploy's built development manifests into the project tree, and refreshes the
1117
1311
  * public default configuration they belong to.
@@ -2185,7 +2379,7 @@ ${rules}`;
2185
2379
  * @param {boolean} options.disableUpdateVolume - Whether to disable volume updates.
2186
2380
  * @param {boolean} options.disableUpdateUnderpostConfig - Whether to disable Underpost config updates.
2187
2381
  * @param {string} [options.namespace] - Kubernetes namespace for the deployment (defaults to "default").
2188
- * @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "300000ms", "infinity").
2382
+ * @param {string} [options.timeoutResponse] - HTTPProxy per-route response timeout (e.g. "10000ms", "infinity").
2189
2383
  * @param {string} [options.timeoutIdle] - HTTPProxy per-route idle timeout (e.g. "10s", "infinity").
2190
2384
  * @param {string} [options.retryCount] - HTTPProxy per-route retry count (e.g. 3).
2191
2385
  * @param {string} [options.retryPerTryTimeout] - HTTPProxy per-route per-try timeout (e.g. "150ms").
@@ -2341,34 +2535,15 @@ EOF`);
2341
2535
  });
2342
2536
  }
2343
2537
 
2344
- for (const host of Object.keys(confServer)) {
2345
- if (!options.disableUpdateProxy) {
2346
- // The host's route object is left in place and replaced by the `apply`
2347
- // below. Deleting it first unpublished the hostname for the whole
2348
- // reconciliation window, so every promote dropped live requests before
2349
- // the new colour was ever the question.
2350
- //
2351
- // A deploy that previously ran the per-host model left a Gateway
2352
- // named after each host. Those are superseded by the consolidated
2353
- // one, and leaving them behind duplicates that hostname's listeners
2354
- // in the same merged set. The oldest resource would retain traffic.
2355
- //
2356
- // `undefined-http3` is the same problem under a different name: the
2357
- // consolidated policy was briefly emitted with an unresolved host in
2358
- // its metadata. Merged listeners are configured by the oldest policy
2359
- // that targets them, so that object outranks the correctly named one
2360
- // for as long as it exists.
2361
- if (options.gatewayApi)
2362
- for (const name of [host, 'undefined']) {
2363
- shellExec(`sudo kubectl delete Gateway ${name} -n ${namespace} --ignore-not-found`, { silent: true });
2364
- shellExec(`sudo kubectl delete ClientTrafficPolicy ${name}-http3 -n ${namespace} --ignore-not-found`, {
2365
- silent: true,
2366
- });
2367
- }
2368
- if (Underpost.deploy.isCertManagerContext({ host, env, options }))
2369
- shellExec(`sudo kubectl delete Certificate ${host} -n ${namespace} --ignore-not-found`);
2370
- }
2371
- }
2538
+ // The host's route object is left in place and replaced by the `apply`
2539
+ // below. Deleting it first unpublished the hostname for the whole
2540
+ // reconciliation window, so every promote dropped live requests before
2541
+ // the new colour was ever the question.
2542
+ if (!options.disableUpdateProxy && options.gatewayApi)
2543
+ Underpost.deploy.sweepLegacyGatewayObjects({ hosts: Object.keys(confServer), namespace });
2544
+ for (const host of Object.keys(confServer))
2545
+ if (!options.disableUpdateProxy && Underpost.deploy.isCertManagerContext({ host, env, options }))
2546
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${namespace} --ignore-not-found`);
2372
2547
 
2373
2548
  const manifestsPath =
2374
2549
  env === 'production'
@@ -2480,6 +2655,11 @@ EOF`);
2480
2655
  }
2481
2656
 
2482
2657
  if (Underpost.deploy.isCertManagerContext({ host: Object.keys(confServer)[0], env, options })) {
2658
+ // The issuer's challenge route must list this deploy's Gateway as a parent, or
2659
+ // the certificates applied below can never issue. Re-rendered from the live
2660
+ // Gateway list, so a deploy created after `cluster --cert-manager` is covered.
2661
+ if (options.gatewayApi && Underpost.deploy.letsEncryptIssuerExists())
2662
+ Underpost.deploy.applyLetsEncryptIssuer({ solver: 'gateway', namespace, env });
2483
2663
  const secretPath = `./${manifestsPath}/secret.yaml`;
2484
2664
  if (fs.existsSync(secretPath) && fs.readFileSync(secretPath, 'utf8').trim()) {
2485
2665
  shellExec(`sudo kubectl apply -f ${secretPath} -n ${namespace}`);
@@ -3143,3 +3323,4 @@ spec:
3143
3323
  }
3144
3324
 
3145
3325
  export default UnderpostDeploy;
3326
+ export { LETSENCRYPT_ISSUER_MANIFEST, LETSENCRYPT_ISSUER_NAME };
@@ -192,7 +192,7 @@ services:
192
192
  MONGO_IMAGE=mongo:latest
193
193
  VALKEY_IMAGE=valkey/valkey:latest
194
194
  APP_IMAGE=underpost/underpost-engine
195
- APP_TAG=v3.3.73
195
+ APP_TAG=v3.3.77
196
196
  PROXY_IMAGE=nginx:stable-alpine
197
197
  PROMETHEUS_IMAGE=prom/prometheus:latest
198
198
  GRAFANA_IMAGE=grafana/grafana:latest
@@ -0,0 +1,115 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import UnderpostRepository from './repository.js';
4
+
5
+ const manifestFilterNames = ['key', 'fromKey', 'toKey', 'keyRegex'];
6
+
7
+ const normalizeStoragePath = (value) => {
8
+ if (value === undefined || value === '') return '';
9
+ const absolute = path.resolve(value);
10
+ const relative = path.relative(process.cwd(), absolute);
11
+ return (
12
+ path.isAbsolute(value) && (relative === '..' || relative.startsWith(`..${path.sep}`)) ? absolute : relative || '.'
13
+ )
14
+ .split(path.sep)
15
+ .join('/');
16
+ };
17
+
18
+ const isWithinScope = (key, scope) => {
19
+ if (scope === undefined || scope === '') return true;
20
+ const relative = path.relative(path.resolve(scope), path.resolve(key));
21
+ return relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
22
+ };
23
+
24
+ const validateSelectionOptions = (scope, options) => {
25
+ if (options.git && options.tracked) throw new Error('Use either --git or --tracked.');
26
+ if (!options.tracked && !scope) throw new Error('Provide a filesystem path, or use --tracked.');
27
+ if (options.rm && options.pull) throw new Error('Use either --rm or --pull.');
28
+ if (options.omitUnzip && !options.pull) throw new Error('--omit-unzip requires --pull.');
29
+ };
30
+
31
+ const filterManifestEntries = (entries, options = {}, scope) => {
32
+ const keys = entries.map(([key]) => key);
33
+ const keyIndex = (name) => {
34
+ const index = keys.indexOf(options[name]);
35
+ if (index < 0) throw new Error(`Manifest key not found for ${name}: ${options[name]}`);
36
+ return index;
37
+ };
38
+ const start = options.fromKey === undefined ? 0 : keyIndex('fromKey');
39
+ const end = options.toKey === undefined ? keys.length - 1 : keyIndex('toKey');
40
+ if (start > end && (options.fromKey !== undefined || options.toKey !== undefined))
41
+ throw new Error('--from-key must precede or equal --to-key.');
42
+
43
+ let regex;
44
+ if (options.keyRegex !== undefined) {
45
+ try {
46
+ regex = new RegExp(options.keyRegex);
47
+ } catch {
48
+ throw new Error(`Invalid --key-regex: ${options.keyRegex}`);
49
+ }
50
+ }
51
+ const matches = (key) => isWithinScope(key, scope) && (!regex || regex.test(key));
52
+ if (options.key !== undefined) {
53
+ const index = keyIndex('key');
54
+ if (index < start || index > end || !matches(options.key))
55
+ throw new Error('--key conflicts with the manifest range, path scope, or regex.');
56
+ }
57
+ return entries
58
+ .slice(start, end + 1)
59
+ .filter(([key]) => matches(key) && (options.key === undefined || key === options.key));
60
+ };
61
+
62
+ const resolveFilesystemSelection = (scope) => {
63
+ const files = [];
64
+ const visit = (target) => {
65
+ const stats = fs.statSync(target);
66
+ if (stats.isFile()) files.push(normalizeStoragePath(target));
67
+ else if (stats.isDirectory()) {
68
+ for (const entry of fs
69
+ .readdirSync(target, { withFileTypes: true })
70
+ .sort((a, b) => a.name.localeCompare(b.name))) {
71
+ const child = path.join(target, entry.name);
72
+ // Do not follow directory links during traversal.
73
+ if (entry.isSymbolicLink()) {
74
+ if (fs.statSync(child, { throwIfNoEntry: false })?.isFile()) files.push(normalizeStoragePath(child));
75
+ } else visit(child);
76
+ }
77
+ } else throw new Error(`Storage requires a regular file or directory: ${target}`);
78
+ };
79
+ visit(scope);
80
+ return files;
81
+ };
82
+
83
+ const resolveGitSelection = (files, scope) => {
84
+ const context = fs.statSync(scope).isDirectory() ? scope : path.dirname(scope);
85
+ const tracked = new Set(UnderpostRepository.API.getTrackedFiles(context));
86
+ return files.filter((file) => tracked.has(path.resolve(file)));
87
+ };
88
+
89
+ const resolveTrackedSelection = (entries) => entries.map(([key]) => key);
90
+
91
+ const resolveSelection = (scope, manifest, options = {}) => {
92
+ validateSelectionOptions(scope, options);
93
+ const entries = Object.entries(manifest);
94
+ const filtered = filterManifestEntries(entries, options, scope);
95
+ if (options.tracked) return resolveTrackedSelection(filtered);
96
+
97
+ let files = resolveFilesystemSelection(scope);
98
+ if (options.git) files = resolveGitSelection(files, scope);
99
+ const manifestKeys = new Map(entries.map(([key]) => [normalizeStoragePath(key), key]));
100
+ files = files.map((file) => manifestKeys.get(file) ?? file);
101
+ if (manifestFilterNames.some((name) => options[name] !== undefined)) {
102
+ const selected = new Set(resolveTrackedSelection(filtered));
103
+ files = files.filter((file) => selected.has(file));
104
+ }
105
+ return files;
106
+ };
107
+
108
+ export {
109
+ normalizeStoragePath,
110
+ filterManifestEntries,
111
+ resolveFilesystemSelection,
112
+ resolveGitSelection,
113
+ resolveTrackedSelection,
114
+ resolveSelection,
115
+ };