@underpostnet/cyberia 3.3.73 → 3.3.76

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 (228) hide show
  1. package/.env.example +17 -5
  2. package/.github/workflows/coverall.cyberia.ci.yml +5 -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/publish.cyberia.ci.yml +44 -0
  10. package/AGENTS.md +61 -12
  11. package/CHANGELOG.md +126 -41
  12. package/CLI-HELP.md +19 -12
  13. package/Dockerfile +1 -1
  14. package/Dockerfile.dev +1 -1
  15. package/Dockerfile.test +2 -2
  16. package/README.md +28 -28
  17. package/bin/build.js +37 -17
  18. package/bin/cyberia.js +1247 -1373
  19. package/bin/index.js +1247 -1373
  20. package/compose.env +17 -5
  21. package/conf.js +7 -3
  22. package/deploy/cyberia-client/deploy.sh +10 -11
  23. package/deploy/cyberia-client/package.sh +0 -1
  24. package/deploy/cyberia-client/state.sh +3 -2
  25. package/deploy/cyberia-server/deploy.sh +10 -11
  26. package/deploy/cyberia-server/package.sh +0 -1
  27. package/deploy/cyberia-server/state.sh +3 -2
  28. package/deploy/dd-cyberia/deploy.sh +5 -5
  29. package/deploy/dd-cyberia/init.sh +15 -14
  30. package/deploy/dd-cyberia/package.sh +0 -1
  31. package/deploy/dd-cyberia/state.sh +2 -2
  32. package/deploy/dd-cyberia/sync-deploy.sh +139 -75
  33. package/deploy/lib/config.sh +29 -0
  34. package/deploy/lib/github-actions-logging.sh +92 -76
  35. package/deploy/lib/host.sh +27 -18
  36. package/deploy/lib/state.sh +4 -1
  37. package/deploy/pwa-microservices-template/deploy.sh +10 -8
  38. package/deploy/release/deploy.sh +0 -2
  39. package/deployment.yaml +1 -1
  40. package/docker-compose.yml +23 -17
  41. package/hardhat/package-lock.json +2 -2
  42. package/hardhat/package.json +1 -1
  43. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  44. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  45. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  46. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  47. package/manifests/deployment/mongo-express/deployment.yaml +46 -27
  48. package/manifests/deployment/mongo-express/kustomization.yaml +9 -0
  49. package/manifests/deployment/mongo-express/mongo-express-nodeport.yaml +18 -0
  50. package/manifests/deployment/mongo-express/service.yaml +16 -0
  51. package/manifests/deployment/mongo-express-no-auth/disable-mongodb-auth.yaml +16 -0
  52. package/manifests/deployment/mongo-express-no-auth/kustomization.yaml +16 -0
  53. package/package.json +19 -17
  54. package/scripts/test-monitor.sh +3 -3
  55. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +14 -9
  56. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +36 -5
  57. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +5 -0
  58. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +88 -184
  59. package/src/api/cyberia-action/cyberia-action.router.js +2 -2
  60. package/src/api/cyberia-action/cyberia-action.service.js +3 -4
  61. package/src/api/cyberia-audio/cyberia-audio.controller.js +6 -0
  62. package/src/api/cyberia-audio/cyberia-audio.model.js +61 -0
  63. package/src/api/cyberia-audio/cyberia-audio.router.js +21 -0
  64. package/src/api/cyberia-audio/cyberia-audio.service.js +220 -0
  65. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -3
  66. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +10 -19
  67. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +12 -34
  68. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +15 -32
  69. package/src/api/cyberia-dialogue/cyberia-dialogue.model.js +1 -1
  70. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  71. package/src/api/cyberia-entity/cyberia-entity.model.js +2 -0
  72. package/src/api/cyberia-entity/cyberia-entity.service.js +1 -1
  73. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +8 -2
  74. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.model.js +31 -20
  75. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +16 -1
  76. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +466 -3
  77. package/src/api/cyberia-instance/cyberia-fallback-capture.js +87 -51
  78. package/src/api/cyberia-instance/cyberia-fallback-world.js +21 -40
  79. package/src/api/cyberia-instance/cyberia-instance-boot.service.js +5 -5
  80. package/src/api/cyberia-instance/cyberia-instance-items.js +98 -0
  81. package/src/api/cyberia-instance/cyberia-instance-map.service.js +34 -141
  82. package/src/api/cyberia-instance/cyberia-instance.controller.js +1 -4
  83. package/src/api/cyberia-instance/cyberia-instance.model.js +0 -14
  84. package/src/api/cyberia-instance/cyberia-instance.router.js +3 -16
  85. package/src/api/cyberia-instance/cyberia-instance.service.js +3 -43
  86. package/src/api/cyberia-instance/cyberia-world-generator.js +8 -13
  87. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +49 -75
  88. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +74 -1
  89. package/src/api/cyberia-map/cyberia-map.service.js +6 -2
  90. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.controller.js +31 -0
  91. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.model.js +56 -0
  92. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.router.js +33 -0
  93. package/src/api/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +189 -0
  94. package/src/api/cyberia-quest/cyberia-quest.router.js +3 -3
  95. package/src/api/cyberia-quest/cyberia-quest.service.js +3 -3
  96. package/src/api/cyberia-quest-progress/cyberia-quest-progress.model.js +1 -1
  97. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +403 -188
  98. package/src/api/cyberia-skill/cyberia-skill.model.js +6 -6
  99. package/src/api/file/file.ref.js +153 -0
  100. package/src/api/file/file.ref.json +11 -2
  101. package/src/api/object-layer/object-layer.model.js +47 -12
  102. package/src/api/object-layer/object-layer.router.js +10 -3
  103. package/src/api/object-layer/object-layer.service.js +113 -120
  104. package/src/cli/app.js +12 -1
  105. package/src/cli/client.js +2 -2
  106. package/src/cli/cluster.js +7 -0
  107. package/src/cli/deploy.js +4 -4
  108. package/src/cli/docker-compose.js +1 -1
  109. package/src/cli/fs-selection.js +115 -0
  110. package/src/cli/fs.js +158 -342
  111. package/src/cli/index.js +22 -10
  112. package/src/cli/repository.js +28 -0
  113. package/src/cli/run.js +71 -197
  114. package/src/cli/test.js +4 -0
  115. package/src/cli/wireguard.js +32 -31
  116. package/src/client/components/core/Docs.js +77 -100
  117. package/src/client/components/core/Modal.js +56 -1
  118. package/src/client/components/cyberia/ActionEngineCyberia.js +12 -36
  119. package/src/client/components/cyberia/EntityEngineCyberia.js +492 -121
  120. package/src/client/components/cyberia/InstanceEngineCyberia.js +129 -160
  121. package/src/client/components/cyberia/InstanceSelectionView.js +7 -1
  122. package/src/client/components/cyberia/MapEngineCyberia.js +175 -69
  123. package/src/client/components/cyberia/ObjectLayerEngine.js +1011 -97
  124. package/src/client/components/cyberia/ObjectLayerEngineModal.js +81 -155
  125. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +113 -98
  126. package/src/client/components/cyberia/SharedDefaultsCyberia.js +215 -56
  127. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +0 -34
  128. package/src/client/components/cyberia-portal/CssCyberiaPortal.js +1 -1
  129. package/src/client/components/cyberia-portal/MainBodyCyberiaPortal.js +712 -76
  130. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +0 -4
  131. package/src/client/public/cyberia-docs/ARCHITECTURE.md +15 -15
  132. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +280 -24
  133. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +51 -29
  134. package/src/client/public/cyberia-docs/CYBERIA-SAGA.md +52 -53
  135. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +16 -11
  136. package/src/client/public/cyberia-docs/CYBERIA.md +28 -28
  137. package/src/client/public/cyberia-docs/ENTITY-PROFILE.md +11 -5
  138. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  139. package/src/client/public/cyberia-docs/STATS-PROGRESSION.md +217 -0
  140. package/src/client/public/cyberia-docs/WHITE-PAPER.md +20 -12
  141. package/src/client/services/cyberia-audio/cyberia-audio.service.js +99 -0
  142. package/src/client/services/cyberia-entity-type-default/cyberia-entity-type-default.service.js +38 -0
  143. package/src/client/services/cyberia-instance/cyberia-instance.management.js +5 -4
  144. package/src/client/services/cyberia-instance/cyberia-instance.service.js +0 -40
  145. package/src/client/services/cyberia-map/cyberia-map.management.js +5 -4
  146. package/src/client/services/cyberia-map-audio-conf/cyberia-map-audio-conf.service.js +138 -0
  147. package/src/client/services/object-layer/object-layer.management.js +6 -10
  148. package/src/client/services/object-layer/object-layer.service.js +3 -1
  149. package/src/client-builder/client-build-docs.js +52 -52
  150. package/src/client-builder/client-build.js +67 -44
  151. package/src/client-builder/client-bundle.js +347 -0
  152. package/src/db/mongo/MongoExpress.js +187 -0
  153. package/src/grpc/cyberia/grpc-server.js +5 -7
  154. package/src/index.js +1 -1
  155. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +143 -146
  156. package/src/projects/cyberia/atlas-sprite-sheet-store.js +391 -0
  157. package/src/projects/cyberia/besu-genesis-generator.js +5 -14
  158. package/src/projects/cyberia/boot-contract-fixtures.js +32 -0
  159. package/src/projects/cyberia/catalog-cyberia.js +3 -2
  160. package/src/projects/cyberia/gemini-client.js +3 -3
  161. package/src/projects/cyberia/generate-saga.js +4 -11
  162. package/src/projects/cyberia/hot-reload-trigger.js +17 -28
  163. package/src/projects/cyberia/instance-backup.js +210 -0
  164. package/src/projects/cyberia/instance-data.js +177 -257
  165. package/src/projects/cyberia/map-preview-generator.js +32 -68
  166. package/src/projects/cyberia/object-layer.js +152 -319
  167. package/src/projects/cyberia/seed-audio.js +139 -0
  168. package/src/projects/cyberia/semantic-layer-generator-skin.js +2 -8
  169. package/src/projects/cyberia/semantic-layer-generator.js +2 -18
  170. package/src/projects/cyberia/stat-balance.js +81 -0
  171. package/src/projects/cyberia/stat-commands.js +23 -0
  172. package/src/projects/cyberia/stat-contract-generator.js +101 -0
  173. package/src/runtime/cyberia-client/Dockerfile +8 -4
  174. package/src/runtime/cyberia-client/Dockerfile.dev +5 -2
  175. package/src/runtime/cyberia-server/Dockerfile +9 -2
  176. package/src/runtime/cyberia-server/Dockerfile.dev +8 -1
  177. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  178. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  179. package/src/runtime/engine-cyberia/Dockerfile.test +2 -2
  180. package/src/runtime/engine-cyberia/compose.env +17 -5
  181. package/src/runtime/engine-cyberia/docker-compose.yml +23 -17
  182. package/src/server/build/coverage.js +113 -42
  183. package/src/server/build/package.js +55 -12
  184. package/src/server/build/testing.js +59 -9
  185. package/src/server/network/middlewares.js +8 -2
  186. package/src/server/ops/logger.js +70 -35
  187. package/src/server/runtime/conf.js +4 -2
  188. package/src/server/storage/data-query.js +16 -0
  189. package/src/server/storage/downloader.js +62 -14
  190. package/src/server/storage/zip.js +153 -0
  191. package/test/integration/app/cyberia/atlas-sprite-sheet-store.test.js +402 -0
  192. package/test/integration/app/cyberia/atlas-sprite-sheet.test.js +86 -0
  193. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +34 -0
  194. package/test/integration/app/cyberia/cyberia-entity-type-default.test.js +842 -0
  195. package/test/integration/app/cyberia/cyberia-instance-conf-coerce.test.js +53 -0
  196. package/test/integration/app/cyberia/cyberia-instance-conf-defaults.test.js +16 -22
  197. package/test/integration/app/cyberia/cyberia-instance-items.test.js +90 -0
  198. package/test/integration/app/cyberia/cyberia-load.test.js +6 -7
  199. package/test/integration/app/cyberia/cyberia-map-audio-conf.test.js +256 -0
  200. package/test/integration/app/cyberia/cyberia-stats.test.js +150 -0
  201. package/test/integration/app/cyberia/fallback-world-capture.test.js +87 -2
  202. package/test/integration/app/cyberia/object-layer-item-selection.test.js +56 -0
  203. package/test/integration/app/cyberia/object-layer-natural-key.test.js +32 -0
  204. package/test/integration/app/cyberia/seed-audio.test.js +275 -0
  205. package/test/integration/infra/2-network/wireguard-cli.test.js +33 -9
  206. package/test/integration/infra/2-network/wireguard-edge.test.js +58 -18
  207. package/test/integration/infra/3-cluster/mongo-express-deploy.test.js +156 -0
  208. package/test/integration/infra/4-ingress/api-cross-origin.test.js +79 -0
  209. package/test/unit/client-build-docs.test.js +97 -70
  210. package/test/unit/client-bundle.test.js +313 -0
  211. package/test/unit/conf-resolution.test.js +17 -0
  212. package/test/unit/coverage-artifact.test.js +120 -29
  213. package/test/unit/cyberia/instance-backup.test.js +183 -0
  214. package/test/unit/cyberia/instance-object-layer-items.test.js +46 -0
  215. package/test/unit/cyberia/publish-workflow.test.js +22 -0
  216. package/test/unit/cyberia/stat-balance.test.js +69 -0
  217. package/test/unit/deploy-log-table.test.js +76 -5
  218. package/test/unit/downloader-integrity.test.js +98 -0
  219. package/test/unit/file-reference-registry.test.js +141 -0
  220. package/test/unit/fs-storage-paths.test.js +596 -0
  221. package/test/unit/logger-redaction.test.js +29 -0
  222. package/test/unit/package.test.js +70 -16
  223. package/test/unit/prepare-host.test.js +84 -1
  224. package/test/unit/test-tiers.test.js +27 -0
  225. package/test/unit/zip-archive.test.js +142 -0
  226. package/vitest.config.js +11 -3
  227. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +0 -63
  228. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +0 -368
@@ -48,6 +48,13 @@ const SENSITIVE_WORDS = new Set([
48
48
  'token',
49
49
  ]);
50
50
  const SENSITIVE_COMPOUNDS = ['apikey', 'privatekey', 'accesskey', 'sessionid', 'mysqlpwd', 'mariadbpwd'];
51
+ /**
52
+ * A value adjacent to a sensitive name, separated by nothing but whitespace, is only treated as
53
+ * the secret when it is shaped like one: long enough, and carrying a digit, a capital, or a
54
+ * separator. Prose reads the same way — `token verification failed` — and redacting the next
55
+ * word of every sentence would cost more than it protects.
56
+ */
57
+ const CREDENTIAL_SHAPED = /^(?=.{12,}$)(?=.*[0-9A-Z_-])[A-Za-z0-9_-]+$/;
51
58
  const sensitiveField = (key) => {
52
59
  const words = `${key || ''}`
53
60
  .replace(/([a-z0-9])([A-Z])/g, '$1_$2')
@@ -61,43 +68,71 @@ const sensitiveField = (key) => {
61
68
  );
62
69
  };
63
70
 
71
+ /**
72
+ * Redacts a value that follows a sensitive name with nothing but whitespace between them.
73
+ *
74
+ * Service errors report credentials this way — `Invalid api_key 4778…` — so the pair reaches a
75
+ * log through a message rather than through a command or a structured field, which is what every
76
+ * other rule matches on. The candidate is looked ahead rather than consumed: a plain match would
77
+ * take `Invalid api_key` as its own name/value pair and advance past the real one behind it.
78
+ * @param {string} text - Text to scan.
79
+ * @returns {string} The text with adjacent credentials redacted.
80
+ * @memberof Logger
81
+ */
82
+ const redactAdjacentCredentials = (text) => {
83
+ const pairs = new RegExp(`(${CONFIG_NAME_SOURCE})([ \\t]+)(?=([A-Za-z0-9_-]+)(?![\\w-]))`, 'g');
84
+ let out = '';
85
+ let cursor = 0;
86
+ for (const match of text.matchAll(pairs)) {
87
+ const [, key, separator, candidate] = match;
88
+ const valueStart = match.index + key.length + separator.length;
89
+ if (valueStart < cursor) continue;
90
+ if (!sensitiveField(key) || !CREDENTIAL_SHAPED.test(candidate)) continue;
91
+ out += text.slice(cursor, valueStart) + REDACTED;
92
+ cursor = valueStart + candidate.length;
93
+ }
94
+ return cursor === 0 ? text : out + text.slice(cursor);
95
+ };
96
+
64
97
  const redactSensitiveText = (value = '') =>
65
- `${value ?? ''}`
66
- .replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g, REDACTED)
67
- .replace(
68
- /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g,
69
- REDACTED,
70
- )
71
- .replace(/([a-z][a-z0-9+.-]*\\?:(?:\\?\/){2})(?:(?:\\.)|[^/\\\s@])+(\\?@)/gi, '$1***$2')
72
- .replace(/\b(x-access-token\\?:)[^\s"']+?(?=\\?@github\.com\b)/gi, '$1***')
73
- .replace(
74
- /((?:"|')?(?:authorization|proxy-authorization)(?:"|')?\s*:\s*)(?:"[^"]*"|'[^']*'|(?:bearer|basic)\s+\S+|\S+)/gi,
75
- `$1"${REDACTED}"`,
76
- )
77
- .replace(
78
- new RegExp(
79
- `(\\b(?:host|app|state|secret)\\s+set\\s+)(${CONFIG_NAME_SOURCE})(\\s+)(${SENSITIVE_VALUE_SOURCE})`,
80
- 'gi',
98
+ redactAdjacentCredentials(
99
+ `${value ?? ''}`
100
+ .replace(/-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g, REDACTED)
101
+ .replace(
102
+ /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\b/g,
103
+ REDACTED,
104
+ )
105
+ .replace(/([a-z][a-z0-9+.-]*\\?:(?:\\?\/){2})(?:(?:\\.)|[^/\\\s@])+(\\?@)/gi, '$1***$2')
106
+ .replace(/\b(x-access-token\\?:)[^\s"']+?(?=\\?@github\.com\b)/gi, '$1***')
107
+ .replace(
108
+ /((?:"|')?(?:authorization|proxy-authorization)(?:"|')?\s*:\s*)(?:"[^"]*"|'[^']*'|(?:bearer|basic)\s+\S+|\S+)/gi,
109
+ `$1"${REDACTED}"`,
110
+ )
111
+ .replace(
112
+ new RegExp(
113
+ `(\\b(?:host|app|state|secret)\\s+set\\s+)(${CONFIG_NAME_SOURCE})(\\s+)(${SENSITIVE_VALUE_SOURCE})`,
114
+ 'gi',
115
+ ),
116
+ (match, prefix, key, separator) => (sensitiveField(key) ? `${prefix}${key}${separator}${REDACTED}` : match),
117
+ )
118
+ .replace(
119
+ new RegExp(`(\\s--?)(${CONFIG_NAME_SOURCE})(=|\\s+)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
120
+ (match, prefix, key, separator) => (sensitiveField(key) ? `${prefix}${key}${separator}${REDACTED}` : match),
121
+ )
122
+ .replace(
123
+ new RegExp(`(^|\\s)(-p)(\\s*)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
124
+ (match, prefix, flag, separator, secret) =>
125
+ secret.startsWith('-') ? match : `${prefix}${flag}${separator}${REDACTED}`,
126
+ )
127
+ .replace(
128
+ new RegExp(`\\b(${CONFIG_NAME_SOURCE})(\\s*=\\s*)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
129
+ (match, key, separator) => (sensitiveField(key) ? `${key}${separator}${REDACTED}` : match),
130
+ )
131
+ .replace(
132
+ new RegExp(`((?:"|')?)(${CONFIG_NAME_SOURCE})((?:"|')?\\s*:\\s*)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
133
+ (match, quote, key, separator) => (sensitiveField(key) ? `${quote}${key}${separator}"${REDACTED}"` : match),
81
134
  ),
82
- (match, prefix, key, separator) => (sensitiveField(key) ? `${prefix}${key}${separator}${REDACTED}` : match),
83
- )
84
- .replace(
85
- new RegExp(`(\\s--?)(${CONFIG_NAME_SOURCE})(=|\\s+)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
86
- (match, prefix, key, separator) => (sensitiveField(key) ? `${prefix}${key}${separator}${REDACTED}` : match),
87
- )
88
- .replace(
89
- new RegExp(`(^|\\s)(-p)(\\s*)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
90
- (match, prefix, flag, separator, secret) =>
91
- secret.startsWith('-') ? match : `${prefix}${flag}${separator}${REDACTED}`,
92
- )
93
- .replace(
94
- new RegExp(`\\b(${CONFIG_NAME_SOURCE})(\\s*=\\s*)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
95
- (match, key, separator) => (sensitiveField(key) ? `${key}${separator}${REDACTED}` : match),
96
- )
97
- .replace(
98
- new RegExp(`((?:"|')?)(${CONFIG_NAME_SOURCE})((?:"|')?\\s*:\\s*)(${SENSITIVE_VALUE_SOURCE})`, 'gi'),
99
- (match, quote, key, separator) => (sensitiveField(key) ? `${quote}${key}${separator}"${REDACTED}"` : match),
100
- );
135
+ );
101
136
 
102
137
  const serializeLogValue = (value) => {
103
138
  const seen = new WeakSet();
@@ -2986,8 +2986,10 @@ const syncPrivateConf = (deployId, extraPaths = []) => {
2986
2986
  if (fs.existsSync(replicaSrcDir))
2987
2987
  for (const entry of fs.readdirSync(replicaSrcDir))
2988
2988
  if (entry.match(deployId)) fs.copySync(`${replicaSrcDir}/${entry}`, `${privateRepoPath}/replica/${entry}`);
2989
-
2990
- for (const extraPath of extraPaths) fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
2989
+ for (const extraPath of extraPaths) {
2990
+ fs.removeSync(`${privateRepoPath}/${extraPath}`);
2991
+ fs.copySync(`./engine-private/${extraPath}`, `${privateRepoPath}/${extraPath}`);
2992
+ }
2991
2993
 
2992
2994
  shellExec(
2993
2995
  `cd ${privateRepoPath}` +
@@ -15,6 +15,22 @@
15
15
  * @memberof DataQuery
16
16
  */
17
17
  class DataQuery {
18
+ /**
19
+ * Filter that addresses one document by its natural key.
20
+ *
21
+ * The natural key is the value people navigate by: an object layer item id, an
22
+ * instance code. A key shaped like a document id matches either field, so a
23
+ * document id still resolves.
24
+ *
25
+ * @param {string} field - Path of the natural key, e.g. 'data.item.id'.
26
+ * @param {string} key - The key taken from the request.
27
+ * @memberof DataQuery
28
+ * @returns {Object} A Mongoose filter selecting at most one document.
29
+ */
30
+ static naturalKeyFilter(field, key) {
31
+ return /^[0-9a-f]{24}$/i.test(String(key)) ? { $or: [{ [field]: key }, { _id: key }] } : { [field]: key };
32
+ }
33
+
18
34
  /**
19
35
  * Parse request query parameters into Mongoose query options
20
36
  * @param {Object} params - The request query parameters (req.query)
@@ -31,31 +31,79 @@ class Downloader {
31
31
  * @memberof Downloader
32
32
  */
33
33
  static downloadFile(url, fullPath, options = { method: 'get', responseType: 'stream' }) {
34
- return new Promise((resolve, reject) =>
34
+ /**
35
+ * Renders an error response body, whatever transport shape it arrived in, capped for a log
36
+ * line. A stream request carries even its errors as a stream, so the body has to be read
37
+ * before it can be reported.
38
+ */
39
+ const responseBody = async (data) => {
40
+ if (!data) return undefined;
41
+ try {
42
+ if (Buffer.isBuffer(data)) return data.toString('utf8').slice(0, 500);
43
+ if (typeof data === 'string') return data.slice(0, 500);
44
+ if (typeof data.on === 'function') {
45
+ let text = '';
46
+ for await (const chunk of data) {
47
+ text += chunk;
48
+ if (text.length > 500) break;
49
+ }
50
+ return text.slice(0, 500) || undefined;
51
+ }
52
+ return JSON.stringify(data).slice(0, 500);
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ };
57
+
58
+ return new Promise((resolve, reject) => {
59
+ let settled = false;
60
+ const fail = (error) => {
61
+ if (settled) return;
62
+ settled = true;
63
+ logger.error('Error downloading the file', { fullPath, error: error?.message });
64
+ // A partial file is worse than none: the caller cannot tell it apart from a whole one.
65
+ if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
66
+ return reject(error);
67
+ };
68
+
35
69
  axios({
36
70
  url,
37
71
  ...options,
38
72
  })
39
73
  .then((response) => {
40
- // Create a write stream to save the file to the specified path
41
74
  const writer = fs.createWriteStream(fullPath);
42
- response.data.pipe(writer);
75
+ const expectedBytes = Number(response.headers?.['content-length'] ?? NaN);
76
+
77
+ // A response stream that ends early still ends, so the writer emits `finish` and the
78
+ // download reports success over a truncated file. The declared length is what proves
79
+ // the transfer whole — without this check a short part reached the caller intact-looking
80
+ // and only surfaced much later, as a corrupt archive assembled from it.
43
81
  writer.on('finish', () => {
44
- logger.info('Download completet');
82
+ if (settled) return;
83
+ const receivedBytes = writer.bytesWritten;
84
+ if (Number.isFinite(expectedBytes) && receivedBytes !== expectedBytes)
85
+ return fail(new Error(`Truncated download: expected ${expectedBytes} bytes, received ${receivedBytes}`));
86
+ settled = true;
87
+ logger.info('Download complete', { fullPath, bytes: receivedBytes });
45
88
  return resolve(fullPath);
46
89
  });
47
- writer.on('error', (error) => {
48
- logger.error('Error downloading the file');
49
- // Cleanup incomplete file if possible
50
- if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
51
- return reject(error);
52
- });
90
+ writer.on('error', fail);
91
+ response.data.on('error', fail);
92
+ response.data.pipe(writer);
53
93
  })
54
- .catch((error) => {
55
- logger.error('Error in the request');
94
+ .catch(async (error) => {
95
+ // The status alone names nothing. A storage service explains a refusal in the body —
96
+ // which delivery type it looked under, which id it could not find — and without it a
97
+ // caller sees `status code 400` and has to reproduce the request by hand to learn why.
98
+ logger.error('Error in the request', {
99
+ fullPath,
100
+ error: error?.message,
101
+ status: error?.response?.status,
102
+ body: await responseBody(error?.response?.data),
103
+ });
56
104
  return reject(error);
57
- }),
58
- );
105
+ });
106
+ });
59
107
  }
60
108
  }
61
109
 
@@ -0,0 +1,153 @@
1
+ /**
2
+ * ZIP archive helpers built on JSZip, shared by the file-storage CLI and the client builder.
3
+ * @module src/server/storage/zip.js
4
+ * @namespace Zip
5
+ */
6
+
7
+ import JSZip from 'jszip';
8
+ import * as dir from 'path';
9
+ import fs from 'fs-extra';
10
+
11
+ const ZIP_LOCAL_FILE_HEADER = Buffer.from([0x50, 0x4b, 0x03, 0x04]);
12
+
13
+ /**
14
+ * Reports whether a buffer starts with the ZIP local file header signature.
15
+ * @function isZipBuffer
16
+ * @param {Buffer} buffer - Candidate archive bytes.
17
+ * @returns {boolean} True when the buffer looks like a ZIP archive.
18
+ * @memberof Zip
19
+ */
20
+ const isZipBuffer = (buffer) => Buffer.isBuffer(buffer) && buffer.subarray(0, 4).equals(ZIP_LOCAL_FILE_HEADER);
21
+
22
+ /**
23
+ * Loads a ZIP archive from a buffer or a local path.
24
+ * @function loadZip
25
+ * @param {Buffer|string} source - Archive bytes or a path to the archive.
26
+ * @returns {Promise<JSZip>} The loaded archive.
27
+ * @memberof Zip
28
+ */
29
+ const loadZip = async (source) => JSZip.loadAsync(Buffer.isBuffer(source) ? source : fs.readFileSync(source));
30
+
31
+ /**
32
+ * Resolves an archive entry by exact name, then by trailing path segment, then by
33
+ * falling back to the only entry of a single-file archive.
34
+ * @function findZipEntry
35
+ * @param {JSZip} zip - The loaded archive.
36
+ * @param {string} entryName - Entry name or basename to look up.
37
+ * @returns {object|null} The matching JSZip entry, or null when nothing matches.
38
+ * @memberof Zip
39
+ */
40
+ const findZipEntry = (zip, entryName) => {
41
+ const files = Object.values(zip.files).filter((entry) => !entry.dir);
42
+ return (
43
+ files.find((entry) => entry.name === entryName) ??
44
+ files.find((entry) => entry.name.endsWith(`/${entryName}`)) ??
45
+ (files.length === 1 ? files[0] : null)
46
+ );
47
+ };
48
+
49
+ /**
50
+ * Reads a single entry out of an archive.
51
+ * @function readZipEntry
52
+ * @param {Buffer|string} source - Archive bytes or a path to the archive.
53
+ * @param {string} entryName - Entry name or basename to read.
54
+ * @returns {Promise<Buffer|null>} The entry contents, or null when the entry is absent.
55
+ * @memberof Zip
56
+ */
57
+ const readZipEntry = async (source, entryName) => {
58
+ const entry = findZipEntry(await loadZip(source), entryName);
59
+ return entry ? await entry.async('nodebuffer') : null;
60
+ };
61
+
62
+ /**
63
+ * Builds an archive buffer from local files.
64
+ * @function zipFromLocalFiles
65
+ * @param {Array<{localPath: string, entryName: string}>} entries - Files to archive.
66
+ * @returns {Promise<Buffer>} The generated archive bytes.
67
+ * @memberof Zip
68
+ */
69
+ const zipFromLocalFiles = async (entries) => {
70
+ const zip = new JSZip();
71
+ for (const { localPath, entryName } of entries) {
72
+ const stats = fs.statSync(localPath);
73
+ zip.file(entryName, fs.readFileSync(localPath), {
74
+ date: stats.mtime,
75
+ unixPermissions: stats.mode & 0o777,
76
+ });
77
+ }
78
+ return await zip.generateAsync({
79
+ type: 'nodebuffer',
80
+ platform: 'UNIX',
81
+ compression: 'DEFLATE',
82
+ compressionOptions: { level: 6 },
83
+ });
84
+ };
85
+
86
+ /**
87
+ * Resolves an entry name against an output directory, rejecting archive paths that
88
+ * escape it (zip slip).
89
+ * @function resolveEntryTarget
90
+ * @param {string} outputPath - Extraction root.
91
+ * @param {string} entryName - Entry name from the archive.
92
+ * @returns {string} The absolute target path.
93
+ * @throws {Error} When the entry resolves outside the extraction root.
94
+ * @memberof Zip
95
+ */
96
+ const resolveEntryTarget = (outputPath, entryName) => {
97
+ const root = dir.resolve(outputPath);
98
+ const target = dir.resolve(root, entryName);
99
+ if (target !== root && !target.startsWith(root + dir.sep)) {
100
+ throw new Error(`Blocked zip entry outside extraction directory: ${entryName}`);
101
+ }
102
+ return target;
103
+ };
104
+
105
+ /**
106
+ * Extracts every entry of an archive into a directory.
107
+ * @function extractZipTo
108
+ * @param {Buffer|string} source - Archive bytes or a path to the archive.
109
+ * @param {string} outputPath - Extraction root; created when missing.
110
+ * @returns {Promise<string[]>} The written file paths.
111
+ * @memberof Zip
112
+ */
113
+ const extractZipTo = async (source, outputPath) => {
114
+ const zip = await loadZip(source);
115
+ fs.mkdirSync(outputPath, { recursive: true });
116
+
117
+ const writtenPaths = [];
118
+ for (const entry of Object.values(zip.files)) {
119
+ const target = resolveEntryTarget(outputPath, entry.name);
120
+ if (entry.dir) {
121
+ fs.mkdirSync(target, { recursive: true });
122
+ continue;
123
+ }
124
+ fs.mkdirSync(dir.dirname(target), { recursive: true });
125
+ fs.writeFileSync(target, await entry.async('nodebuffer'));
126
+ if (entry.unixPermissions) fs.chmodSync(target, entry.unixPermissions & 0o777);
127
+ writtenPaths.push(target);
128
+ }
129
+ return writtenPaths;
130
+ };
131
+
132
+ /**
133
+ * Extracts a single archive entry into a directory, flattening its archive path.
134
+ * @function extractZipEntryTo
135
+ * @param {Buffer|string} source - Archive bytes or a path to the archive.
136
+ * @param {string} entryName - Entry name or basename to extract.
137
+ * @param {string} targetDir - Destination directory; created when missing.
138
+ * @returns {Promise<string>} The written file path.
139
+ * @throws {Error} When the entry is not present in the archive.
140
+ * @memberof Zip
141
+ */
142
+ const extractZipEntryTo = async (source, entryName, targetDir) => {
143
+ const entry = findZipEntry(await loadZip(source), entryName);
144
+ if (!entry) throw new Error(`Zip entry not found: ${entryName}`);
145
+
146
+ const target = resolveEntryTarget(targetDir, dir.basename(entryName));
147
+ fs.mkdirSync(dir.dirname(target), { recursive: true });
148
+ fs.writeFileSync(target, await entry.async('nodebuffer'));
149
+ if (entry.unixPermissions) fs.chmodSync(target, entry.unixPermissions & 0o777);
150
+ return target;
151
+ };
152
+
153
+ export { isZipBuffer, loadZip, findZipEntry, readZipEntry, zipFromLocalFiles, extractZipTo, extractZipEntryTo };