@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
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ import { expect } from 'chai';
4
+ import shell from 'shelljs';
5
+ import { vi } from 'vitest';
6
+ import { gatewayPodFetch, parseRawHttpResponse } from '../../src/server/network/underpost-gateway.js';
7
+
8
+ describe('gateway pod fetch', () => {
9
+ let commands;
10
+ let answer;
11
+
12
+ beforeEach(() => {
13
+ commands = [];
14
+ answer = (command) =>
15
+ command.includes('| nc ') ? 'HTTP/1.1 502 Bad Gateway\r\nServer: nginx\r\n\r\n<h1>maintenance</h1>\n' : '';
16
+ vi.spyOn(shell, 'exec').mockImplementation((command) => {
17
+ commands.push(command);
18
+ const stdout = answer(command);
19
+ return { code: 0, stdout, stderr: '', toString: () => stdout };
20
+ });
21
+ });
22
+
23
+ afterEach(() => vi.restoreAllMocks());
24
+
25
+ it('reads the status and the body of an error response off the socket', () => {
26
+ // Regression: busybox wget discards the body of any non-2xx answer, so the fallback
27
+ // document — delivered with an upstream-failure status by definition — never
28
+ // matched, and the probe polled every attempt against a correct edge.
29
+ const result = gatewayPodFetch({ namespace: 'default', host: 'www.bymyelectrics.com', path: '/' });
30
+ expect(result).to.deep.equal({ status: '502', body: '<h1>maintenance</h1>\n' });
31
+ expect(commands.length).to.equal(1);
32
+ expect(commands[0]).to.include(
33
+ "printf 'GET / HTTP/1.0\\r\\nHost: www.bymyelectrics.com\\r\\n\\r\\n' | nc -w 10 127.0.0.1 80",
34
+ );
35
+ });
36
+
37
+ it('falls back to the wget pair for the status where nc is absent', () => {
38
+ answer = (command) => (command.includes('wget -S') ? 'HTTP/1.1 503 Service Unavailable\n' : '');
39
+ const result = gatewayPodFetch({ namespace: 'default', host: 'healthcare.nexodev.org' });
40
+ expect(result.status).to.equal('503');
41
+ expect(commands.length).to.equal(3);
42
+ });
43
+
44
+ it('parses a raw response byte for byte after the first blank line', () => {
45
+ expect(parseRawHttpResponse('')).to.deep.equal({ status: '', body: '' });
46
+ expect(parseRawHttpResponse('HTTP/1.0 200 OK\r\nX: y\r\n\r\n')).to.deep.equal({ status: '200', body: '' });
47
+ expect(parseRawHttpResponse('HTTP/1.1 504 Gateway Time-out\n\nline\n\nmore')).to.deep.equal({
48
+ status: '504',
49
+ body: 'line\n\nmore',
50
+ });
51
+ });
52
+ });
53
+
54
+ describe('gateway fetch', () => {
55
+ afterEach(() => vi.restoreAllMocks());
56
+
57
+ const mock = (answer) => {
58
+ const commands = [];
59
+ vi.spyOn(shell, 'exec').mockImplementation((command) => {
60
+ commands.push(command);
61
+ const stdout = answer(command);
62
+ return { code: 0, stdout, stderr: '', toString: () => stdout };
63
+ });
64
+ return commands;
65
+ };
66
+
67
+ it('asks the gateway Service from this host with curl, keeping the error body', async () => {
68
+ const { gatewayFetch } = await import('../../src/server/network/underpost-gateway.js');
69
+ const commands = mock((command) => {
70
+ if (command.includes('kubectl get svc underpost-gateway-service')) return '10.96.151.82\n';
71
+ if (command.startsWith('curl ')) return 'HTTP/1.1 502 Bad Gateway\r\nServer: nginx\r\n\r\n<h1>maintenance</h1>\n';
72
+ return '';
73
+ });
74
+ const result = gatewayFetch({ namespace: 'default', host: 'healthcare.nexodev.org', path: '/' });
75
+ expect(result).to.deep.equal({ status: '502', body: '<h1>maintenance</h1>\n', via: 'host' });
76
+ expect(commands.find((command) => command.startsWith('curl '))).to.include(
77
+ "-H 'Host: healthcare.nexodev.org' http://10.96.151.82:80/",
78
+ );
79
+ expect(commands.some((command) => command.includes('kubectl exec'))).to.equal(false);
80
+ });
81
+
82
+ it('falls back to the pod when the Service cannot be reached from here', async () => {
83
+ const { gatewayFetch } = await import('../../src/server/network/underpost-gateway.js');
84
+ mock((command) => {
85
+ if (command.includes('kubectl get svc underpost-gateway-service')) return '10.96.151.82\n';
86
+ if (command.includes('| nc ')) return 'HTTP/1.1 503 Service Unavailable\r\n\r\nbody';
87
+ return '';
88
+ });
89
+ expect(gatewayFetch({ namespace: 'default', host: 'healthcare.nexodev.org' })).to.deep.equal({
90
+ status: '503',
91
+ body: 'body',
92
+ via: 'pod',
93
+ });
94
+ });
95
+ });
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ import { expect } from 'chai';
4
+ import { load as yamlLoad } from 'js-yaml';
5
+ import shell from 'shelljs';
6
+ import { vi } from 'vitest';
7
+ import UnderpostCluster from '../../src/cli/cluster.js';
8
+ import UnderpostDeploy, { LETSENCRYPT_ISSUER_MANIFEST, LETSENCRYPT_ISSUER_NAME } from '../../src/cli/deploy.js';
9
+
10
+ describe("Let's Encrypt issuer for the ingress stack in use", () => {
11
+ afterEach(() => vi.restoreAllMocks());
12
+
13
+ describe('acmeSolverFactory', () => {
14
+ it('answers through the Gateway API whenever its data plane is installed', () => {
15
+ // The `cluster` runner installs the issuer in an invocation that carries no stack
16
+ // flag, so the decision has to come from the cluster, not from the flags.
17
+ const presence = { contour: false, gateway: true };
18
+ expect(UnderpostCluster.API.acmeSolverFactory({ options: {}, presence })).to.equal('gateway');
19
+ expect(UnderpostCluster.API.acmeSolverFactory({ options: { gatewayApi: true }, presence })).to.equal('gateway');
20
+ expect(
21
+ UnderpostCluster.API.acmeSolverFactory({
22
+ options: { gatewayApi: true },
23
+ presence: { contour: true, gateway: false },
24
+ }),
25
+ ).to.equal('gateway');
26
+ });
27
+
28
+ it('keeps the Contour Ingress solver only where nothing else carries the challenge', () => {
29
+ expect(
30
+ UnderpostCluster.API.acmeSolverFactory({ options: {}, presence: { contour: true, gateway: false } }),
31
+ ).to.equal('ingress');
32
+ expect(
33
+ UnderpostCluster.API.acmeSolverFactory({
34
+ options: { contour: true },
35
+ presence: { contour: false, gateway: true },
36
+ }),
37
+ ).to.equal('ingress');
38
+ });
39
+ });
40
+
41
+ describe('letsEncryptIssuerYamlFactory', () => {
42
+ it('keeps the account from the base manifest and swaps in the Gateway solver', () => {
43
+ const base = yamlLoad(
44
+ UnderpostDeploy.API.letsEncryptIssuerYamlFactory({ solver: 'ingress', basePath: LETSENCRYPT_ISSUER_MANIFEST }),
45
+ );
46
+ const issuer = yamlLoad(
47
+ UnderpostDeploy.API.letsEncryptIssuerYamlFactory({
48
+ solver: 'gateway',
49
+ parentRefs: [
50
+ { name: 'dd-core-production', namespace: 'default' },
51
+ { name: 'dd-prototype-production', namespace: 'default' },
52
+ ],
53
+ }),
54
+ );
55
+ expect(issuer.kind).to.equal('ClusterIssuer');
56
+ expect(issuer.metadata.name).to.equal(LETSENCRYPT_ISSUER_NAME);
57
+ expect(issuer.spec.acme.email).to.equal(base.spec.acme.email);
58
+ expect(issuer.spec.acme.server).to.equal(base.spec.acme.server);
59
+ expect(issuer.spec.acme.privateKeySecretRef).to.deep.equal(base.spec.acme.privateKeySecretRef);
60
+ expect(issuer.spec.acme.solvers).to.deep.equal([
61
+ {
62
+ http01: {
63
+ gatewayHTTPRoute: {
64
+ parentRefs: [
65
+ { name: 'dd-core-production', namespace: 'default', kind: 'Gateway' },
66
+ { name: 'dd-prototype-production', namespace: 'default', kind: 'Gateway' },
67
+ ],
68
+ },
69
+ },
70
+ },
71
+ ]);
72
+ expect(base.spec.acme.solvers).to.deep.equal([{ http01: { ingress: { class: 'contour' } } }]);
73
+ });
74
+
75
+ it('refuses a Gateway solver with nothing to attach the challenge route to', () => {
76
+ expect(() => UnderpostDeploy.API.letsEncryptIssuerYamlFactory({ solver: 'gateway', parentRefs: [] })).to.throw(
77
+ /parentRef/,
78
+ );
79
+ });
80
+ });
81
+
82
+ describe('acmeGatewayParentRefsFactory', () => {
83
+ it('unions the live Gateways with the routed deploys, once each, sorted', () => {
84
+ vi.spyOn(shell, 'exec').mockImplementation((command) => {
85
+ const stdout = command.startsWith('kubectl get gateway')
86
+ ? 'gateway.gateway.networking.k8s.io/dd-core-production\ngateway.gateway.networking.k8s.io/other-production\n'
87
+ : '';
88
+ return { code: 0, stdout, stderr: '', toString: () => stdout };
89
+ });
90
+ vi.spyOn(UnderpostDeploy.API, 'gatewayNameFactory').mockImplementation(
91
+ ({ deployId, env }) => `${deployId}-${env}`,
92
+ );
93
+ const refs = UnderpostDeploy.API.acmeGatewayParentRefsFactory({ namespace: 'default', env: 'production' });
94
+ const names = refs.map(({ name }) => name);
95
+ expect(names).to.include('dd-core-production');
96
+ expect(names).to.include('other-production');
97
+ expect(names).to.deep.equal([...new Set(names)].sort());
98
+ expect(refs.every(({ namespace }) => namespace === 'default')).to.equal(true);
99
+ });
100
+ });
101
+ });
@@ -21,6 +21,35 @@ const renderLog = (message, metadata) => {
21
21
  };
22
22
 
23
23
  describe('central logger redaction', () => {
24
+ it('filters a credential a service reports next to its name, with no delimiter between them', () => {
25
+ // Regression: `Invalid api_key 4778…` reached the terminal in full. Every rule matched on a
26
+ // delimiter — `=`, `:`, a flag — and an error message carries none, so the pair went through.
27
+ expect(redactSensitiveText('Invalid api_key 477826957851424')).to.equal('Invalid api_key [REDACTED]');
28
+ expect(redactSensitiveText('Invalid api_secret 8kQvN2mLp0XyZ_abc')).to.equal('Invalid api_secret [REDACTED]');
29
+ expect(redactSensitiveText('CLOUDINARY_API_KEY 477826957851424')).to.equal('CLOUDINARY_API_KEY [REDACTED]');
30
+ });
31
+
32
+ it('leaves prose and identifiers that merely follow a sensitive word', () => {
33
+ // The name of a thing is not the thing: redacting the next word of every sentence would cost
34
+ // more legibility than it buys protection, so the value has to be shaped like a credential.
35
+ for (const kept of [
36
+ 'token verification failed',
37
+ 'password changed successfully',
38
+ 'authorization required for this route',
39
+ 'api_secret mismatch',
40
+ 'Target pod: dd-cyberia-production-blue-5b7c67bb8f-kwhz7 | Pod status: Running',
41
+ ])
42
+ expect(redactSensitiveText(kept), kept).to.equal(kept);
43
+ });
44
+
45
+ it('reaches a credential that a non-sensitive word precedes', () => {
46
+ // The scan looks ahead rather than consuming: matching `Invalid api_key` as a pair of its own
47
+ // would advance past the real one behind it.
48
+ const rendered = renderLog('cloudinary rejected: Invalid api_key 477826957851424');
49
+ expect(rendered).to.include('[REDACTED]');
50
+ expect(rendered).to.not.include('477826957851424');
51
+ });
52
+
24
53
  it('filters common secret forms from free-form messages', () => {
25
54
  const rawToken = `ghp_${'A'.repeat(30)}`;
26
55
  const input = [
@@ -14,6 +14,7 @@ import {
14
14
  deployPackagePathFactory,
15
15
  productDevDependenciesFactory,
16
16
  productPackageOptionsFactory,
17
+ publishedProductPackageJson,
17
18
  stagePackageArchive,
18
19
  syncDeployPackages,
19
20
  } from '../../src/server/build/package.js';
@@ -38,7 +39,7 @@ describe('generated product package dependencies', () => {
38
39
  },
39
40
  productDependencies: {
40
41
  'runtime-shared': '3.0.0',
41
- underpost: '^3.3.0',
42
+ nodemailer: '^10.0.0',
42
43
  },
43
44
  productDevDependencies: {
44
45
  'product-dev-only': '4.0.0',
@@ -72,7 +73,7 @@ describe('generated product package dependencies', () => {
72
73
  },
73
74
  catalog: { description: 'Cyberia CLI', keywords: ['cyberia'] },
74
75
  confName: 'dd-cyberia',
75
- customDependencies: { underpost: '^3.3.0' },
76
+ customDependencies: { nodemailer: '^10.0.0' },
76
77
  customScripts: { build: 'node bin client' },
77
78
  customBin: { cyberia: 'bin/index.js' },
78
79
  });
@@ -82,7 +83,7 @@ describe('generated product package dependencies', () => {
82
83
  description: 'Cyberia CLI',
83
84
  keywords: ['cyberia'],
84
85
  bin: { cyberia: 'bin/index.js' },
85
- dependencies: { underpost: '^3.3.0' },
86
+ dependencies: { nodemailer: '^10.0.0' },
86
87
  devDependencies: { express: '5.2.1', vitest: '4.1.11' },
87
88
  scripts: { test: 'node bin test', build: 'node bin client' },
88
89
  });
@@ -265,25 +266,78 @@ describe('generated deploy package manifests', () => {
265
266
  });
266
267
 
267
268
  describe('product manifest overrides read the catalog', () => {
268
- it('carries the published engine CLI alongside the catalog pins', () => {
269
- expect(
270
- productPackageOptionsFactory({
271
- catalog: {
272
- packageDependencies: { ethers: '~6.16.0' },
273
- packageBin: { cyberia: 'bin/index.js' },
274
- packageScripts: { 'docker:up': 'node bin docker-compose --up' },
275
- },
276
- underpostVersion: 'v3.3.0',
277
- }),
278
- ).to.deep.equal({
279
- customDependencies: { underpost: '^3.3.0', ethers: '~6.16.0' },
269
+ it('carries the engine runtime dependencies alongside the catalog pins', () => {
270
+ // Regression: the product depended on the published `underpost` package instead, which
271
+ // installed a second engine whose nested `nodemailer` `src/` could not resolve, while the
272
+ // hoistable copy survived only as a dev entry a production install omits.
273
+ const options = productPackageOptionsFactory({
274
+ catalog: {
275
+ packageDependencies: { ethers: '~6.16.0' },
276
+ packageBin: { cyberia: 'bin/index.js' },
277
+ packageScripts: { 'docker:up': 'node bin docker-compose --up' },
278
+ },
279
+ engineDependencies: { express: '^5.2.1', nodemailer: '^10.0.0' },
280
+ });
281
+
282
+ expect(options).to.deep.equal({
283
+ customDependencies: { express: '^5.2.1', nodemailer: '^10.0.0', ethers: '~6.16.0' },
280
284
  customScripts: { 'docker:up': 'node bin docker-compose --up' },
281
285
  customBin: { cyberia: 'bin/index.js' },
282
286
  });
287
+ expect(options.customDependencies).to.not.have.property('underpost');
288
+ });
289
+
290
+ it('lets a catalog pin override the engine version of the same dependency', () => {
291
+ expect(
292
+ productPackageOptionsFactory({
293
+ catalog: { packageDependencies: { sharp: '^0.35.3' } },
294
+ engineDependencies: { sharp: '^0.34.0' },
295
+ }).customDependencies,
296
+ ).to.deep.equal({ sharp: '^0.35.3' });
283
297
  });
284
298
 
285
299
  it('leaves the base template manifest alone for a catalog that declares no package', () => {
286
- expect(productPackageOptionsFactory({ catalog: {}, underpostVersion: 'v3.3.0' })).to.deep.equal({});
300
+ expect(productPackageOptionsFactory({ catalog: {}, engineDependencies: { express: '^5.2.1' } })).to.deep.equal({});
301
+ });
302
+ });
303
+
304
+ describe('the manifest a product publishes to npm', () => {
305
+ it('trades the engine runtime set for the published CLI and the catalog pins', () => {
306
+ const published = publishedProductPackageJson({
307
+ packageJson: {
308
+ name: 'cyberia',
309
+ version: '3.3.73',
310
+ dependencies: { express: '^5.2.1', nodemailer: '^10.0.0', sharp: '^0.35.3' },
311
+ devDependencies: { vitest: '5.0.0' },
312
+ },
313
+ catalog: { packageDependencies: { sharp: '^0.35.3', ethers: '~6.16.0' } },
314
+ });
315
+
316
+ expect(published.dependencies).to.deep.equal({ underpost: '^3.3.73', sharp: '^0.35.3', ethers: '~6.16.0' });
317
+ expect(published.devDependencies).to.deep.equal({
318
+ express: '^5.2.1',
319
+ nodemailer: '^10.0.0',
320
+ vitest: '5.0.0',
321
+ });
322
+ expect(published.name).to.equal('cyberia');
323
+ });
324
+
325
+ it('pins the engine CLI at the version it is given, tag stripped', () => {
326
+ expect(
327
+ publishedProductPackageJson({ packageJson: { version: '3.3.73' }, underpostVersion: 'v4.0.0' }).dependencies,
328
+ ).to.deep.equal({ underpost: '^4.0.0' });
329
+ });
330
+
331
+ it('refuses a manifest it cannot pin the engine CLI from', () => {
332
+ expect(() => publishedProductPackageJson()).to.throw('packageJson');
333
+ expect(() => publishedProductPackageJson({ packageJson: { name: 'cyberia' } })).to.throw('version');
334
+ });
335
+
336
+ it('leaves the repository manifest as the checkout installs from', () => {
337
+ const packageJson = { version: '1.0.0', dependencies: { express: '^5.2.1' } };
338
+ publishedProductPackageJson({ packageJson });
339
+
340
+ expect(packageJson).to.deep.equal({ version: '1.0.0', dependencies: { express: '^5.2.1' } });
287
341
  });
288
342
  });
289
343
 
@@ -81,7 +81,12 @@ describe('a node installs the deploy own package manifest', () => {
81
81
  try {
82
82
  const emitted = run(`install_deploy_dependencies ${root} dd-absent`).trim().split('\n');
83
83
  expect(emitted[0]).to.equal(`Install dependencies :: sudo -n -- /bin/bash -lc cd ${root} && npm install`);
84
- expect(emitted[1]).to.include(`Link underpost CLI :: sudo -n -- /bin/bash -lc cd ${root} && npm link --force`);
84
+ // The link is a symlink onto the checkout's entrypoint, and a restored checkout can leave
85
+ // that file without its executable mode, so the link step restores it before linking.
86
+ expect(emitted[1]).to.include(
87
+ `Link underpost CLI :: sudo -n -- /bin/bash -lc cd ${root} && chmod +x ./bin/index.js && npm link --force`,
88
+ );
89
+ expect(emitted[1]).to.include('test -x ./bin/index.js');
85
90
  } finally {
86
91
  fs.removeSync(root);
87
92
  }
@@ -297,3 +302,81 @@ describe('prepare_host brings a node up in an order it can recover from', () =>
297
302
  expect(emitted.at(-1)).to.include('node bin host load');
298
303
  });
299
304
  });
305
+
306
+ const cyberiaDeployScript = path.join(deployRoot, 'dd-cyberia/sync-deploy.sh');
307
+
308
+ describe.skipIf(!fs.existsSync(cyberiaDeployScript))('a pod lands its private configuration at engine-private', () => {
309
+ const source = fs.existsSync(cyberiaDeployScript) ? fs.readFileSync(cyberiaDeployScript, 'utf8') : '';
310
+
311
+ it('names the pod conf repository once and derives the directory the clone lands in', () => {
312
+ expect(source).to.match(/^POD_SRC_PRIVATE_REPO="\$\{POD_SRC_PRIVATE_REPO:-[^"]+\}"$/m);
313
+ expect(source).to.include('underpost clone ${POD_SRC_PRIVATE_REPO}');
314
+ expect(source).to.include('POD_SRC_PRIVATE_DIR="${POD_SRC_PRIVATE_REPO##*/}"');
315
+ expect(source).to.include('sudo mv ./${POD_SRC_PRIVATE_DIR} ./engine-private');
316
+ });
317
+
318
+ it('clears the destination first, so the checkout replaces engine-private instead of nesting in it', () => {
319
+ const cleared = source.indexOf('sudo rm -rf ./engine-private');
320
+ const moved = source.indexOf('sudo mv ./${POD_SRC_PRIVATE_DIR} ./engine-private');
321
+ expect(cleared).to.be.greaterThan(-1);
322
+ expect(cleared).to.be.lessThan(moved);
323
+ });
324
+ });
325
+
326
+ // Every script under deploy/ is the same shape on purpose: one entry point, one constants block,
327
+ // and no value repeated across files. A second shape is how the drift starts, so it is asserted
328
+ // here rather than left to review.
329
+ describe.skipIf(!shipsDeployIds)('every deploy script is written to one shape', () => {
330
+ const libDirectory = path.join(deployRoot, 'lib');
331
+ const scripts = fs
332
+ .readdirSync(deployRoot)
333
+ .filter((entry) => fs.statSync(path.join(deployRoot, entry)).isDirectory() && entry !== 'lib')
334
+ .flatMap((directory) =>
335
+ fs
336
+ .readdirSync(path.join(deployRoot, directory))
337
+ .filter((file) => file.endsWith('.sh'))
338
+ .map((file) => ({
339
+ id: `${directory}/${file}`,
340
+ source: fs.readFileSync(path.join(deployRoot, directory, file), 'utf8'),
341
+ })),
342
+ );
343
+ const libraries = fs
344
+ .readdirSync(libDirectory)
345
+ .filter((file) => file.endsWith('.sh'))
346
+ .map((file) => ({ id: `lib/${file}`, source: fs.readFileSync(path.join(libDirectory, file), 'utf8') }));
347
+
348
+ it('opens and closes every entry point the same way', () => {
349
+ expect(scripts).to.not.be.empty;
350
+ for (const { id, source } of scripts) {
351
+ expect(source.startsWith('#!/bin/bash\nset -euo pipefail\n'), id).to.equal(true);
352
+ expect(source, id).to.include('source "$SCRIPT_DIR/../lib/github-actions-logging.sh"');
353
+ expect(source.trimEnd().endsWith('main "$@"'), id).to.equal(true);
354
+ }
355
+ });
356
+
357
+ it('leaves the shared values to lib/config.sh, so no script carries a copy', () => {
358
+ // The engine root, the ingress node, the fleet's node names, the ssh key and the WordPress
359
+ // image were each pasted into a dozen scripts before this.
360
+ for (const { id, source } of scripts) {
361
+ expect(source, id).to.not.match(/^ENGINE_ROOT=/m);
362
+ expect(source, id).to.not.match(/^INGRESS_NODE=/m);
363
+ expect(source, id).to.not.include('/home/dd/engine');
364
+ expect(source, id).to.not.include('hp-envy');
365
+ expect(source, id).to.not.include('underpost/wp:');
366
+ }
367
+ });
368
+
369
+ it('states its deploy id once and reads it back from there', () => {
370
+ // A fleet-level script names no deploy of its own; one that names a deploy declares it.
371
+ for (const { id, source } of scripts.filter(({ source: text }) => /dd-[a-z0-9]/.test(text))) {
372
+ expect(source, id).to.match(/^DEPLOY_ID=dd-[a-z0-9-]+$/m);
373
+ const belowDeclaration = source.slice(source.indexOf('\n', source.search(/^DEPLOY_ID=/m)));
374
+ expect(belowDeclaration, id).to.not.match(/dd-[a-z0-9]/);
375
+ }
376
+ });
377
+
378
+ it('carries no library shebang and no trailing whitespace anywhere', () => {
379
+ for (const { id, source } of libraries) expect(source.startsWith('#!'), id).to.equal(false);
380
+ for (const { id, source } of [...scripts, ...libraries]) expect(source, id).to.not.match(/[ \t]+$/m);
381
+ });
382
+ });
@@ -307,7 +307,9 @@ describe('release pipeline commands', () => {
307
307
  commands = [];
308
308
  const { triggerCmd } = await UnderpostRelease.API.ci(deployId, 'fixture message');
309
309
  expect(triggerCmd, deployId).to.include('/engine-cyberia');
310
- expect(commands, deployId).to.include('node bin/build dd-cyberia');
310
+ // `--coverage`: the published source must carry the reports the deploy declares, or
311
+ // every pod started from it serves the unavailable page.
312
+ expect(commands, deployId).to.include('node bin/build dd-cyberia --coverage');
311
313
  }
312
314
  });
313
315
 
@@ -37,6 +37,18 @@ describe('start receives every flag its command registers', () => {
37
37
  expect(buildContract).to.include('{ force: Underpost.state.isInsideContainer() }');
38
38
  });
39
39
 
40
+ it('withdraws the ephemeral engine-private clone once a production container is serving', () => {
41
+ // Regression: the domain-split withdraw passed `force: false` to every domain, and only
42
+ // `host clean --force` removes `engine-private` — the clone outlived the boot in the pod.
43
+ const runStart = source.indexOf('async run(');
44
+ const withdraw = source.slice(
45
+ source.indexOf('for (const domain of', runStart),
46
+ source.indexOf('setTimeout', runStart),
47
+ );
48
+ expect(withdraw).to.include('[Underpost.secret, Underpost.host, Underpost.app]');
49
+ expect(withdraw).to.include('force: domain === Underpost.host');
50
+ });
51
+
40
52
  it('runs the container in one phase, with no re-entry through the global CLI', () => {
41
53
  // The pod's own bootstrap replaces the checkout and links the CLI before `start` is
42
54
  // invoked, so re-execing `underpost start` here only pulled and installed a second time.
@@ -6,10 +6,12 @@ import {
6
6
  TEST_TIERS,
7
7
  UNDERPOST_TESTING,
8
8
  coverageIncludeFactory,
9
+ coverageReportKey,
9
10
  coverageThresholdFactory,
10
11
  resolveTestSelection,
11
12
  testProjectsFactory,
12
13
  testSuiteNames,
14
+ vitestProjectSelector,
13
15
  } from '../../src/server/build/testing.js';
14
16
 
15
17
  // The table declares every tier the platform ships. A product build slices the
@@ -191,3 +193,28 @@ describe('coverage scope', () => {
191
193
  expect(() => coverageIncludeFactory(['--project', 'not-a-tier'])).to.throw(/unknown suite/);
192
194
  });
193
195
  });
196
+
197
+ describe('coverage report directory', () => {
198
+ it('names the report after the suites the selection spans', () => {
199
+ expect(coverageReportKey('unit,infra,app')).to.equal('unit-infra-app');
200
+ expect(coverageReportKey('cyberia')).to.equal('cyberia');
201
+ expect(coverageReportKey('infra:1-security,infra:2-network')).to.equal('infra');
202
+ });
203
+
204
+ it('names every Vitest suite for a full run and none for a delegated one', () => {
205
+ expect(coverageReportKey('')).to.equal(coverageReportKey('all'));
206
+ expect(coverageReportKey('all').split('-')).to.have.members([
207
+ ...new Set(TEST_TIERS.filter(({ delegate }) => !delegate).map(({ name }) => name.split(':')[0])),
208
+ ]);
209
+ expect(coverageReportKey('contracts')).to.equal('');
210
+ });
211
+
212
+ // A deploy names its report by suite and the runner is handed the same tiers as
213
+ // `--project` flags; both must land on one directory.
214
+ it('agrees between a suite selector and the projects the runner was handed', () => {
215
+ const { projects } = resolveTestSelection('unit,infra,app');
216
+ const argv = ['npx', 'vitest', 'run', ...projects.flatMap((project) => ['--project', project]), '--coverage'];
217
+ expect(coverageReportKey(vitestProjectSelector(argv))).to.equal(coverageReportKey('unit,infra,app'));
218
+ expect(vitestProjectSelector(['--project=cyberia'])).to.equal('cyberia');
219
+ });
220
+ });