@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
@@ -26,7 +26,7 @@ class ObjectLayerManagement {
26
26
  this.eGui = document.createElement('div');
27
27
  const { data } = params;
28
28
 
29
- if (!data || !data._id) {
29
+ if (!data?.data?.item?.id) {
30
30
  this.eGui.innerHTML = '';
31
31
  return;
32
32
  }
@@ -43,10 +43,8 @@ class ObjectLayerManagement {
43
43
  if (btn)
44
44
  btn.onclick = async () =>
45
45
  setTimeout(async () => {
46
- // Navigate to viewer route first
47
46
  setPath(`${getProxyPath()}object-layer-engine-viewer`);
48
- // Then add query param without replacing history
49
- setQueryParams({ id: data._id }, { replace: true });
47
+ setQueryParams({ id: null, itemId: data.data.item.id }, { replace: true });
50
48
  if (s(`.modal-object-layer-engine-viewer`)) {
51
49
  await ObjectLayerEngineViewer.Reload({ appStore, force: true });
52
50
  }
@@ -72,7 +70,7 @@ class ObjectLayerManagement {
72
70
  this.eGui = document.createElement('div');
73
71
  const { data } = params;
74
72
 
75
- if (!data || !data._id) {
73
+ if (!data?.data?.item?.id) {
76
74
  this.eGui.innerHTML = '';
77
75
  return;
78
76
  }
@@ -89,10 +87,8 @@ class ObjectLayerManagement {
89
87
  if (btn)
90
88
  btn.onclick = async () =>
91
89
  setTimeout(async () => {
92
- // Navigate to editor route first
93
90
  setPath(`${getProxyPath()}object-layer-engine`);
94
- // Then add query param without replacing history
95
- setQueryParams({ id: data._id }, { replace: true });
91
+ setQueryParams({ id: null, itemId: data.data.item.id }, { replace: true });
96
92
  if (s(`.modal-object-layer-engine`)) await ObjectLayerEngineModal.Reload();
97
93
  else s(`.main-btn-object-layer-engine`).click();
98
94
  });
@@ -121,8 +117,8 @@ class ObjectLayerManagement {
121
117
  return;
122
118
  }
123
119
 
124
- const { type, id } = data.data.item;
125
- const imagePath = `${getProxyPath()}assets/${type}/${id}/08/0.png`;
120
+ const { id } = data.data.item;
121
+ const imagePath = `${getProxyPath()}api/atlas-sprite-sheet/idle-preview/${id}`;
126
122
 
127
123
  // Container with both image and fallback
128
124
  this.eGui.innerHTML = html`
@@ -183,9 +183,11 @@ class ObjectLayerService {
183
183
  }),
184
184
  );
185
185
  };
186
- static searchItemIds = (options = { q: '' }) => {
186
+ /** Item identity ({ id, type }) by id prefix (`q`) or by an exact id list (`ids`). */
187
+ static searchItemIds = (options = { q: '', ids: [] }) => {
187
188
  const url = new URL(getApiBaseUrl({ id: `search-item-ids`, endpoint }));
188
189
  if (options.q) url.searchParams.set('q', options.q);
190
+ if (options.ids?.length) url.searchParams.set('ids', options.ids.join(','));
189
191
  return new Promise((resolve, reject) =>
190
192
  fetch(url.toString(), {
191
193
  method: 'GET',
@@ -155,7 +155,7 @@ const main = () => {
155
155
  text-align: center;
156
156
  padding: 24px;
157
157
  }
158
- .badge {
158
+ .status-badge {
159
159
  display: inline-flex;
160
160
  align-items: center;
161
161
  gap: 8px;
@@ -168,7 +168,7 @@ const main = () => {
168
168
  margin-bottom: 26px;
169
169
  box-shadow: 0 0 12px rgba(34, 211, 238, 0.35);
170
170
  }
171
- .badge .dot {
171
+ .status-badge .dot {
172
172
  width: 8px;
173
173
  height: 8px;
174
174
  background: var(--green);
@@ -296,7 +296,7 @@ const main = () => {
296
296
  </style>
297
297
  <div class="grid"></div>
298
298
  <main class="wrap">
299
- <span class="badge"><span class="dot"></span> INSTANCE ${instanceLabel()}</span>
299
+ <span class="status-badge"><span class="dot"></span> INSTANCE ${instanceLabel()}</span>
300
300
  <div class="code" data-text="404">404</div>
301
301
  <div class="headline">SIGNAL LOST</div>
302
302
  <p class="sub">
@@ -11,6 +11,7 @@ import { shellExec } from '../server/runtime/process.js';
11
11
  import { loggerFactory } from '../server/ops/logger.js';
12
12
  import {
13
13
  coverageReportCandidates,
14
+ coverageReportsFactory,
14
15
  coverageUnavailablePage,
15
16
  resolveCoverageReportPath,
16
17
  } from '../server/build/coverage.js';
@@ -296,43 +297,41 @@ const buildApiDocs = async ({
296
297
  ),
297
298
  'utf8',
298
299
  );
299
- setTimeout(async () => {
300
- const { default: swaggerAutoGen } = await import('swagger-autogen');
301
- const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
302
- const routes = [];
303
- for (const api of apis) {
304
- if (['user', 'object-layer'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
305
- }
300
+ // Imported after the patch above, and awaited: the spec is part of the client build
301
+ // output, so a bundle zipped before it landed shipped without one and the pod that
302
+ // restored that bundle served no `api-docs`.
303
+ const { default: swaggerAutoGen } = await import('swagger-autogen');
304
+ const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
305
+ const routes = [];
306
+ for (const api of apis) {
307
+ if (['user', 'object-layer'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
308
+ }
306
309
 
307
- await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
310
+ await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
308
311
 
309
- // Post-process: inject requestBody into operations — swagger-autogen silently
310
- // ignores #swagger.requestBody annotations and has no internal OAS-3 body support.
311
- if (fs.existsSync(outputFile)) {
312
- const swaggerJson = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
313
- let patched = false;
312
+ // Post-process: inject requestBody into operations — swagger-autogen silently
313
+ // ignores #swagger.requestBody annotations and has no internal OAS-3 body support.
314
+ if (fs.existsSync(outputFile)) {
315
+ const swaggerJson = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
316
+ let patched = false;
314
317
 
315
- for (const [key, requestBody] of Object.entries(requestBodies)) {
316
- const [method, ...pathParts] = key.split(' ');
317
- const opPath = pathParts.join(' ');
318
- if (swaggerJson.paths?.[opPath]?.[method]) {
319
- swaggerJson.paths[opPath][method].requestBody = requestBody;
320
- // Remove any stale in:body entry from parameters (OAS 3.0 doesn't allow it)
321
- if (Array.isArray(swaggerJson.paths[opPath][method].parameters)) {
322
- swaggerJson.paths[opPath][method].parameters = swaggerJson.paths[opPath][method].parameters.filter(
323
- (p) => p.in !== 'body',
324
- );
325
- }
326
- patched = true;
318
+ for (const [key, requestBody] of Object.entries(requestBodies)) {
319
+ const [method, ...pathParts] = key.split(' ');
320
+ const opPath = pathParts.join(' ');
321
+ if (swaggerJson.paths?.[opPath]?.[method]) {
322
+ swaggerJson.paths[opPath][method].requestBody = requestBody;
323
+ // Remove any stale in:body entry from parameters (OAS 3.0 doesn't allow it)
324
+ if (Array.isArray(swaggerJson.paths[opPath][method].parameters)) {
325
+ swaggerJson.paths[opPath][method].parameters = swaggerJson.paths[opPath][method].parameters.filter(
326
+ (p) => p.in !== 'body',
327
+ );
327
328
  }
328
- }
329
-
330
- if (patched) {
331
- fs.writeFileSync(outputFile, JSON.stringify(swaggerJson, null, 2), 'utf8');
332
- // logger.warn('swagger post-process: requestBody injected', Object.keys(requestBodies));
329
+ patched = true;
333
330
  }
334
331
  }
335
- });
332
+
333
+ if (patched) fs.writeFileSync(outputFile, JSON.stringify(swaggerJson, null, 2), 'utf8');
334
+ }
336
335
  };
337
336
 
338
337
  /**
@@ -401,9 +400,9 @@ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId, docs, do
401
400
  };
402
401
 
403
402
  /**
404
- * Publishes the coverage HTML report a build carried into the docs route.
403
+ * Publishes every coverage HTML report the deploy declares, each at `docs/coverage/<id>`.
405
404
  *
406
- * Never generates it: the report belongs to the test stage, which bundles it into the
405
+ * Never generates one: a report belongs to the test stage, which bundles it into the
407
406
  * deploy artifact (see {@link module:src/server/build/coverage.js}). A client build that
408
407
  * shelled out to `npm test` here spent minutes of a pod's build phase on a runner the
409
408
  * workload has no business holding, and every expected non-zero exit of that suite latched
@@ -412,31 +411,32 @@ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId, docs, do
412
411
  * @memberof clientBuildDocs
413
412
  * @param {Object} options - Coverage build options
414
413
  * @param {Object} options.docs - Documentation config from server conf
415
- * @param {string} options.docs.coveragePath - Source tree root the report was bundled into
416
- * @param {string} [options.docs.coverageOutputDir] - Route directory under the docs path
414
+ * @param {Array<{id: string, label?: string, suite?: string, path?: string}>} [options.docs.coverage] - Declared reports
417
415
  * @param {string} options.docsDestination - Resolved output path where docs were built
418
416
  */
419
417
  const buildCoverage = async ({ docs, docsDestination }) => {
420
418
  const logger = loggerFactory(import.meta);
421
- const { coveragePath, coverageOutputDir = 'coverage' } = docs;
422
- // No configured source tree is a deploy that never declared coverage — not a missing report.
423
- if (!coveragePath) return;
419
+ const reports = coverageReportsFactory(docs);
420
+ if (reports.length === 0) return;
421
+ // The route holds exactly the declared reports: one dropped from the conf leaves with it.
422
+ fs.emptyDirSync(`${docsDestination}coverage`);
423
+ for (const report of reports) {
424
+ const coverageBuildPath = `${docsDestination}coverage/${report.id}`;
425
+ const reportPath = resolveCoverageReportPath(report);
424
426
 
425
- const coverageBuildPath = `${docsDestination}${coverageOutputDir}`;
426
- const reportPath = resolveCoverageReportPath(coveragePath);
427
+ if (!reportPath) {
428
+ fs.outputFileSync(`${coverageBuildPath}/index.html`, coverageUnavailablePage(report), 'utf8');
429
+ logger.warn('no coverage report bundled, publishing the unavailable page', {
430
+ id: report.id,
431
+ searched: coverageReportCandidates(report),
432
+ published: coverageBuildPath,
433
+ });
434
+ continue;
435
+ }
427
436
 
428
- if (!reportPath) {
429
- fs.outputFileSync(`${coverageBuildPath}/index.html`, coverageUnavailablePage(), 'utf8');
430
- logger.warn('no coverage report bundled, publishing the unavailable page', {
431
- searched: coverageReportCandidates(coveragePath),
432
- published: coverageBuildPath,
433
- });
434
- return;
437
+ fs.copySync(reportPath, coverageBuildPath);
438
+ logger.warn('build coverage', coverageBuildPath);
435
439
  }
436
-
437
- fs.emptyDirSync(coverageBuildPath);
438
- fs.copySync(reportPath, coverageBuildPath);
439
- logger.warn('build coverage', coverageBuildPath);
440
440
  };
441
441
 
442
442
  /**
@@ -473,7 +473,7 @@ const buildDocs = async ({
473
473
  // TypeDoc output is versioned: served at /docs/engine/{version}/
474
474
  const version = (packageData?.version || '').replace(/^v/, '');
475
475
  const jsDocsDestination = `./public/${host}${pathPrefix}docs/engine/${version}/`;
476
- // Coverage output at /docs/coverage/ (or /docs/{coverageOutputDir}/)
476
+ // Coverage reports at /docs/coverage/<id>/
477
477
  const coverageBaseDestination = `./public/${host}${pathPrefix}docs/`;
478
478
  await buildJsDocs({ host, path, metadata, publicClientId, docs, docsDestination: jsDocsDestination });
479
479
  await buildCoverage({ docs, docsDestination: coverageBaseDestination });
@@ -17,7 +17,7 @@ import {
17
17
  } from '../client/components/core/CommonJs.js';
18
18
  import { readConfJson } from '../server/runtime/conf.js';
19
19
  import { minify } from 'html-minifier-terser';
20
- import AdmZip from 'adm-zip';
20
+ import { extractZipTo, findZipEntry, isZipBuffer, loadZip, zipFromLocalFiles } from '../server/storage/zip.js';
21
21
  import * as dir from 'path';
22
22
  import { shellExec } from '../server/runtime/process.js';
23
23
  import { SitemapStream, streamToPromise } from 'sitemap';
@@ -27,6 +27,7 @@ import { statusPageBuildSegment } from '../server/network/underpost-gateway.js';
27
27
  import { repositoryIdentityFactory } from '../server/storage/repository.js';
28
28
  import Underpost from '../index.js';
29
29
  import { buildDocs } from './client-build-docs.js';
30
+ import { coverageReportsFactory } from '../server/build/coverage.js';
30
31
  import { ssrFactory } from './ssr.js';
31
32
 
32
33
  // Static Site Generation (SSG)
@@ -180,6 +181,17 @@ const splitFileByMb = ({ filePath, partSizeMb, logger }) => {
180
181
  return partPaths;
181
182
  };
182
183
 
184
+ /**
185
+ * Names the zip artifact a served route builds to. Single source of truth for the bundle
186
+ * identity: the build writes `<id>.zip`, and push/pull address the same file by this name.
187
+ * @function clientBundleIdFactory
188
+ * @param {string} host - The served host.
189
+ * @param {string} routePath - The route's proxy sub-path (`/`, `/peer`, ...).
190
+ * @returns {string} The bundle id, e.g. `underpost.net-` for `/` or `underpost.net-peer`.
191
+ * @memberof clientBuild
192
+ */
193
+ const clientBundleIdFactory = (host, routePath) => `${host}-${`${routePath ?? ''}`.replaceAll('/', '')}`;
194
+
183
195
  const getZipPartPaths = (zipPath) => {
184
196
  const zipDir = dir.dirname(zipPath);
185
197
  const zipBase = dir.basename(zipPath);
@@ -269,9 +281,9 @@ const resolveClientBuildZip = (buildPrefix) => {
269
281
  * @param {object} options
270
282
  * @param {string} options.buildPrefix - The build prefix path (e.g. build/underpost.net/underpost.net-).
271
283
  * @param {object} options.logger - Logger instance.
272
- * @returns {{ zipPath: string, partPaths: string[], mergedBytes: number }}
284
+ * @returns {Promise<{ zipPath: string, partPaths: string[], mergedBytes: number }>}
273
285
  */
274
- const mergeClientBuildZip = ({ buildPrefix, logger }) => {
286
+ const mergeClientBuildZip = async ({ buildPrefix, logger }) => {
275
287
  // Normalize to get the zip path, then look for parts directly (bypassing resolveClientBuildZip
276
288
  // which prefers an existing monolithic zip over parts).
277
289
  const normalizedPrefix = buildPrefix.replace(/\.zip(?:[.-]part\d+)?$/, '').replace(/[-.]$/, '') + '-';
@@ -301,31 +313,41 @@ const mergeClientBuildZip = ({ buildPrefix, logger }) => {
301
313
  // For each part, extract raw bytes: if the part file is a Cloudinary wrapper zip
302
314
  // (downloaded via pull without --omit-unzip or with --omit-unzip keeping the .zip),
303
315
  // extract the inner entry rather than using the wrapper bytes.
304
- const readPartBytes = (partPath) => {
316
+ const readPartBytes = async (partPath) => {
305
317
  const rawBytes = fs.readFileSync(partPath);
306
- // Check for ZIP magic bytes (PK\x03\x04)
307
- if (rawBytes[0] === 0x50 && rawBytes[1] === 0x4b && rawBytes[2] === 0x03 && rawBytes[3] === 0x04) {
308
- try {
309
- const wrapperZip = new AdmZip(rawBytes);
310
- const entries = wrapperZip.getEntries();
311
- // The inner entry is the original part file (without the outer .zip wrapper)
312
- const partBase = dir.basename(partPath).replace(/\.zip$/i, '');
313
- const entry = entries.find((e) => e.entryName === partBase || e.entryName.endsWith('/' + partBase));
314
- if (entry) {
315
- return entry.getData();
316
- }
317
- // Fallback: single-entry archive
318
- if (entries.length === 1) {
319
- return entries[0].getData();
320
- }
321
- } catch (_) {
322
- // Not a valid zip or extraction failed — use raw bytes
323
- }
318
+ if (!isZipBuffer(rawBytes)) return rawBytes;
319
+
320
+ let wrapper;
321
+ try {
322
+ wrapper = await loadZip(rawBytes);
323
+ } catch (_) {
324
+ // A raw split part carries the zip signature but no central directory, so failing to load
325
+ // is how a part that was never wrapped identifies itself.
326
+ return rawBytes;
324
327
  }
325
- return rawBytes;
328
+
329
+ // A part pulled from storage arrives inside the archive the download produced. Falling back
330
+ // to that wrapper's own bytes merges compressed data into the bundle and surfaces much later
331
+ // as a corrupt archive, so a wrapper whose part cannot be found fails here and names itself.
332
+ const entry = findZipEntry(wrapper, dir.basename(partPath).replace(/\.zip$/i, ''));
333
+ if (!entry) throw new Error(`Bundle part wrapper holds no matching entry: ${partPath}`);
334
+ return await entry.async('nodebuffer');
326
335
  };
327
336
 
328
- const mergedBuffer = Buffer.concat(partPaths.map(readPartBytes));
337
+ const partBuffers = [];
338
+ for (const partPath of partPaths) partBuffers.push(await readPartBytes(partPath));
339
+ const mergedBuffer = Buffer.concat(partBuffers);
340
+
341
+ // A short part anywhere in the sequence only shows up as a byte count against the assembled
342
+ // archive, which names nothing. Reporting each part's contribution is what makes the one that
343
+ // came up short identifiable.
344
+ logger.info('merge-zip: part sizes', {
345
+ zipPath,
346
+ parts: partPaths.map((partPath, index) => ({
347
+ part: dir.basename(partPath),
348
+ bytes: partBuffers[index].length,
349
+ })),
350
+ });
329
351
  fs.writeFileSync(zipPath, mergedBuffer);
330
352
 
331
353
  logger.warn('merge-zip: merged split parts into zip', {
@@ -337,19 +359,16 @@ const mergeClientBuildZip = ({ buildPrefix, logger }) => {
337
359
  return { zipPath, partPaths, mergedBytes: mergedBuffer.length };
338
360
  };
339
361
 
340
- const unzipClientBuild = ({ buildPrefix, logger }) => {
362
+ const unzipClientBuild = async ({ buildPrefix, logger }) => {
341
363
  const { zipPath, partPaths, buildPrefix: resolvedBuildPrefix } = resolveClientBuildZip(buildPrefix);
342
364
  const outputPath = resolvedBuildPrefix.replace(/-$/, '');
343
365
 
344
366
  fs.removeSync(outputPath);
345
- fs.mkdirSync(outputPath, { recursive: true });
346
-
347
- const zip =
348
- partPaths.length > 0
349
- ? new AdmZip(Buffer.concat(partPaths.map((partPath) => fs.readFileSync(partPath))))
350
- : new AdmZip(zipPath);
351
367
 
352
- zip.extractAllTo(outputPath, true);
368
+ await extractZipTo(
369
+ partPaths.length > 0 ? Buffer.concat(partPaths.map((partPath) => fs.readFileSync(partPath))) : zipPath,
370
+ outputPath,
371
+ );
353
372
 
354
373
  logger.warn('unzip build', {
355
374
  source: partPaths.length > 0 ? partPaths : [zipPath],
@@ -704,6 +723,8 @@ const buildClient = async (
704
723
  apiBasePath: process.env.BASE_API,
705
724
  version: Underpost.version,
706
725
  repository: repositoryIdentityFactory(),
726
+ // The reports the docs menu offers, each published at /docs/coverage/<id>.
727
+ coverage: coverageReportsFactory(docs).map(({ id, label }) => ({ id, label })),
707
728
  ...(isDevelopment ? { dev: true } : undefined),
708
729
  };
709
730
 
@@ -833,14 +854,12 @@ const buildClient = async (
833
854
  }
834
855
 
835
856
  if (!ssrOnly && views) {
836
- if (
837
- !(
838
- enableLiveRebuild &&
839
- !options.liveClientBuildPaths.find(
840
- (p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
841
- )
857
+ if (!(
858
+ enableLiveRebuild &&
859
+ !options.liveClientBuildPaths.find(
860
+ (p) => p.srcBuildPath.startsWith(`./src/client/ssr`) || p.srcBuildPath.slice(-9) === '.index.js',
842
861
  )
843
- )
862
+ ))
844
863
  for (const view of views) {
845
864
  const buildPath = `${
846
865
  rootClientPath[rootClientPath.length - 1] === '/' ? rootClientPath.slice(0, -1) : rootClientPath
@@ -1147,23 +1166,25 @@ ${swTransformedJs}`,
1147
1166
 
1148
1167
  if (!fs.existsSync('./build')) fs.mkdirSync('./build');
1149
1168
 
1150
- const zip = new AdmZip();
1151
1169
  const files = await fs.readdir(rootClientPath, { recursive: true });
1170
+ const zipEntries = [];
1152
1171
 
1153
1172
  for (const relativePath of files) {
1154
1173
  const filePath = dir.resolve(`${rootClientPath}/${relativePath}`);
1155
1174
  if (!fs.lstatSync(filePath).isDirectory()) {
1156
1175
  const folder = dir.relative(`public/${host}${path}`, dir.dirname(filePath));
1157
- zip.addLocalFile(filePath, folder);
1176
+ zipEntries.push({
1177
+ localPath: filePath,
1178
+ entryName: folder ? `${folder}/${dir.basename(filePath)}` : dir.basename(filePath),
1179
+ });
1158
1180
  }
1159
1181
  }
1160
1182
 
1161
- const buildId = `${host}-${path.replaceAll('/', '')}`;
1162
- const zipPath = `./build/${buildId}.zip`;
1183
+ const zipPath = `./build/${clientBundleIdFactory(host, path)}.zip`;
1163
1184
 
1164
1185
  logger.warn('write zip', zipPath);
1165
1186
 
1166
- zip.writeZip(zipPath);
1187
+ fs.writeFileSync(zipPath, await zipFromLocalFiles(zipEntries));
1167
1188
 
1168
1189
  if (options.split) {
1169
1190
  splitFileByMb({
@@ -1181,7 +1202,9 @@ ${swTransformedJs}`,
1181
1202
 
1182
1203
  export {
1183
1204
  buildClient,
1205
+ clientBundleIdFactory,
1184
1206
  copyNonExistingFiles,
1207
+ getZipPartPaths,
1185
1208
  unzipClientBuild,
1186
1209
  mergeClientBuildZip,
1187
1210
  staticContextRoutesFactory,