@underpostnet/cyberia 3.3.76 → 3.4.0

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 (132) hide show
  1. package/.env.example +1 -0
  2. package/.github/workflows/coverall.cyberia.ci.yml +17 -3
  3. package/.github/workflows/cyberia-client.cd.yml +2 -1
  4. package/.github/workflows/cyberia-server.cd.yml +2 -1
  5. package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
  6. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
  7. package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
  8. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
  9. package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
  10. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
  11. package/.github/workflows/engine-cyberia.ci.yml +7 -1
  12. package/.github/workflows/ghpkg.ci.yml +19 -2
  13. package/.github/workflows/gitlab.ci.yml +8 -6
  14. package/.github/workflows/hardhat.ci.yml +4 -1
  15. package/.github/workflows/npmpkg.ci.yml +9 -5
  16. package/.github/workflows/publish.ci.yml +21 -12
  17. package/.github/workflows/publish.cyberia.ci.yml +25 -14
  18. package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -2
  19. package/CHANGELOG.md +110 -262
  20. package/CLI-HELP.md +29 -2
  21. package/Dockerfile +1 -1
  22. package/Dockerfile.dev +1 -1
  23. package/Dockerfile.test +1 -1
  24. package/README.md +1 -1
  25. package/bin/build.js +67 -14
  26. package/bin/cyberia.js +4 -4
  27. package/bin/index.js +4 -4
  28. package/compose.env +1 -0
  29. package/conf.js +1 -1043
  30. package/deploy/cyberia-client/deploy.sh +13 -11
  31. package/deploy/cyberia-server/deploy.sh +13 -11
  32. package/deploy/dd-cyberia/sync-deploy.sh +7 -28
  33. package/deploy/lib/config.sh +1 -1
  34. package/deploy/lib/github-actions-logging.sh +3 -2
  35. package/deploy/lib/host.sh +1 -1
  36. package/deploy/pwa-microservices-template/deploy.sh +7 -1
  37. package/deployment.yaml +1 -1
  38. package/docker-compose.yml +7 -7
  39. package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
  40. package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
  41. package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
  42. package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
  43. package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
  44. package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
  45. package/docs/coverage/cyberia/base.css +362 -0
  46. package/docs/coverage/cyberia/block-navigation.js +82 -0
  47. package/docs/coverage/cyberia/favicon.png +0 -0
  48. package/docs/coverage/cyberia/index.html +161 -0
  49. package/docs/coverage/cyberia/prettify.css +101 -0
  50. package/docs/coverage/cyberia/prettify.js +937 -0
  51. package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
  52. package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
  53. package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
  54. package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
  55. package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
  56. package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
  57. package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
  58. package/docs/coverage/cyberia/sorter.js +205 -0
  59. package/hardhat/package-lock.json +6 -6
  60. package/hardhat/package.json +2 -2
  61. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  62. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
  63. package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
  64. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  65. package/package.json +19 -17
  66. package/scripts/test-monitor.sh +1 -1
  67. package/src/api/file/file.router.js +7 -1
  68. package/src/cli/baremetal.js +1 -1
  69. package/src/cli/cluster.js +73 -18
  70. package/src/cli/deploy.js +211 -30
  71. package/src/cli/docker-compose.js +1 -1
  72. package/src/cli/index.js +36 -0
  73. package/src/cli/kubectl.js +4 -3
  74. package/src/cli/release.js +5 -1
  75. package/src/cli/repository.js +14 -11
  76. package/src/cli/run.js +10 -3
  77. package/src/cli/secrets.js +393 -200
  78. package/src/cli/socketsecurity.js +260 -0
  79. package/src/client/components/core/Badge.js +0 -1
  80. package/src/client/components/core/CssCore.js +11 -1
  81. package/src/client/components/core/Modal.js +189 -66
  82. package/src/client/components/core/SearchBox.js +0 -1
  83. package/src/client/components/core/ToolTip.js +15 -0
  84. package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
  85. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
  86. package/src/client/components/default/AppShellDefault.js +1 -4
  87. package/src/client/components/itemledger/AppShellItemledger.js +0 -2
  88. package/src/client/components/underpost/AppShellUnderpost.js +12 -0
  89. package/src/client/components/underpost/CssUnderpost.js +15 -2
  90. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +8 -0
  91. package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
  92. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +54 -56
  93. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  94. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +30 -29
  95. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  96. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +54 -25
  97. package/src/client/ssr/head/Css.js +12 -10
  98. package/src/client/ssr/views/Cyberia404.js +9 -13
  99. package/src/client/ssr/views/NotFound.js +75 -0
  100. package/src/client-builder/client-bundle.js +2 -2
  101. package/src/index.js +13 -1
  102. package/src/runtime/engine-cyberia/Dockerfile +1 -1
  103. package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
  104. package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
  105. package/src/runtime/engine-cyberia/compose.env +1 -0
  106. package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
  107. package/src/server/build/catalog.js +1 -0
  108. package/src/server/build/execution.js +19 -5
  109. package/src/server/build/package.js +166 -3
  110. package/src/server/build/testing.js +2 -1
  111. package/src/server/network/underpost-gateway.js +113 -14
  112. package/src/server/runtime/conf.js +57 -7
  113. package/src/server/runtime/config-scope.js +2 -0
  114. package/src/server/runtime/process.js +4 -3
  115. package/src/server/runtime/start.js +11 -2
  116. package/src/server/security/socketsecurity.js +1735 -0
  117. package/test/integration/infra/1-security/config-scope.test.js +3 -0
  118. package/test/integration/infra/1-security/socketsecurity.test.js +1041 -0
  119. package/test/integration/infra/1-security/sops-secret-store.test.js +201 -71
  120. package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
  121. package/test/unit/build-template.test.js +24 -0
  122. package/test/unit/catalog.test.js +13 -2
  123. package/test/unit/conf-resolution.test.js +41 -7
  124. package/test/unit/cyberia/instance-data.test.js +314 -0
  125. package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
  126. package/test/unit/deploy-log-table.test.js +38 -0
  127. package/test/unit/execution-profiles.test.js +1 -0
  128. package/test/unit/gateway-pod-fetch.test.js +95 -0
  129. package/test/unit/letsencrypt-issuer.test.js +101 -0
  130. package/test/unit/package.test.js +118 -0
  131. package/test/unit/release-bump.test.js +3 -1
  132. package/test/unit/start-options.test.js +12 -0
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "type": "module",
3
3
  "main": "src/index.js",
4
4
  "name": "@underpostnet/cyberia",
5
- "version": "3.3.76",
5
+ "version": "3.4.0",
6
6
  "description": "Cyberia CLI — toolchain for the Cyberia MMO data layer, content pipeline, persistence, gRPC services, and ERC-1155 lifecycle on Hyperledger Besu.",
7
7
  "scripts": {
8
8
  "start": "node --max-old-space-size=8192 src/server",
@@ -18,14 +18,17 @@
18
18
  "docs": "typedoc --options typedoc.json",
19
19
  "install:global": "npm install -g prettier",
20
20
  "install:test": "npm install -g coveralls-next",
21
- "install": "npm run install:global && npm run install:test",
21
+ "install:tools": "npm run install:global && npm run install:test",
22
+ "prepare": "node bin socketsecurity --patch-apply",
22
23
  "prettier": "prettier --write .",
23
24
  "fix": "npm audit fix --force && npm audit",
24
25
  "baremetal": "node bin baremetal --dev --commission --ls --create-machine",
25
26
  "security:secrets": "gitleaks detect --source . --config .gitleaks.toml --report-path ./gitleaks-report.json --report-format json --redact=0",
26
27
  "security:secrets:ci": "gitleaks detect --source . --config .gitleaks.toml --redact=1",
27
28
  "security:deps": "npm audit",
28
- "security": "npm run security:secrets:ci && npm run security:deps",
29
+ "security:socket": "node bin socketsecurity --audit",
30
+ "security:socket:ci": "node bin socketsecurity --ci",
31
+ "security": "npm run security:secrets:ci && npm run security:deps && npm run security:socket",
29
32
  "docker:generate": "node bin docker-compose --generate --deploy-id dd-cyberia --docker-compose-id cyberia",
30
33
  "docker:up": "node bin docker-compose --up --deploy-id dd-cyberia --docker-compose-id cyberia",
31
34
  "docker:up:build": "node bin docker-compose --up --build --deploy-id dd-cyberia --docker-compose-id cyberia",
@@ -72,7 +75,7 @@
72
75
  },
73
76
  "homepage": "https://github.com/underpostnet/engine-cyberia#readme",
74
77
  "dependencies": {
75
- "underpost": "^3.3.76",
78
+ "@underpostnet/underpost": "^3.4.0",
76
79
  "maxrects-packer": "^2.7.3",
77
80
  "pngjs": "^7.0.0",
78
81
  "jimp": "^1.6.0",
@@ -89,7 +92,7 @@
89
92
  "minimatch": "^10.2.2",
90
93
  "glob": "^11.0.0",
91
94
  "diff": ">=8.0.3",
92
- "js-yaml": "$js-yaml",
95
+ "js-yaml": "^5.4.1",
93
96
  "debug": ">=4.3.6",
94
97
  "serialize-javascript": ">=7.0.5",
95
98
  "router": {
@@ -105,12 +108,8 @@
105
108
  "@grpc/grpc-js": "^1.14.4",
106
109
  "@grpc/proto-loader": "^0.8.1",
107
110
  "@neodrag/vanilla": "^2.3.1",
108
- "@vitest/coverage-v8": "5.0.0",
109
111
  "ag-grid-community": "^36.1.0",
110
- "allure-vitest": "3.12.0",
111
112
  "axios": "^1.20.0",
112
- "bumpp": "^12.3.0",
113
- "chai": "^6.2.2",
114
113
  "clipboardy": "^5.3.2",
115
114
  "cloudinary": "^2.11.0",
116
115
  "colors": "^1.4.0",
@@ -119,11 +118,10 @@
119
118
  "cookie-parser": "^1.4.7",
120
119
  "cors": "^2.8.6",
121
120
  "d3": "^7.9.0",
122
- "dexie": "^4.4.5",
121
+ "dexie": "^4.4.6",
123
122
  "dotenv": "^17.4.2",
124
123
  "easymde": "^2.21.0",
125
124
  "esbuild": "^0.28.2",
126
- "escape-string-regexp": "^5.0.0",
127
125
  "express": "^5.2.1",
128
126
  "express-fileupload": "^1.4.3",
129
127
  "express-rate-limit": "^8.7.0",
@@ -142,15 +140,14 @@
142
140
  "jsonwebtoken": "^9.0.3",
143
141
  "jszip": "^3.10.2",
144
142
  "mariadb": "^3.5.4",
145
- "marked": "^18.0.11",
146
- "mongoose": "^9.9.5",
143
+ "marked": "^18.0.12",
144
+ "mongoose": "^9.10.0",
147
145
  "morgan": "^1.12.0",
148
- "nodemailer": "^10.0.0",
146
+ "nodemailer": "^10.0.3",
149
147
  "nodemon": "^3.0.1",
150
148
  "peer": "^1.0.2",
151
149
  "peerjs": "^1.5.5",
152
150
  "prom-client": "^15.1.2",
153
- "read": "^6.0.0",
154
151
  "rrule": "^2.8.1",
155
152
  "shelljs": "^0.10.0",
156
153
  "sitemap": "^9.0.1",
@@ -163,7 +160,6 @@
163
160
  "typescript": "6.0.3",
164
161
  "validator": "^13.15.35",
165
162
  "vanilla-jsoneditor": "^3.13.0",
166
- "vitest": "5.0.0",
167
163
  "winston": "^3.19.0",
168
164
  "workbox-background-sync": "^7.4.1",
169
165
  "workbox-cacheable-response": "^7.4.1",
@@ -171,6 +167,12 @@
171
167
  "workbox-expiration": "^7.4.1",
172
168
  "workbox-precaching": "^7.4.1",
173
169
  "workbox-routing": "^7.4.1",
174
- "workbox-strategies": "^7.4.1"
170
+ "workbox-strategies": "^7.4.1",
171
+ "@socketsecurity/cli": "1.1.171",
172
+ "@vitest/coverage-v8": "5.0.0",
173
+ "allure-vitest": "3.12.1",
174
+ "bumpp": "^12.3.0",
175
+ "chai": "^6.2.2",
176
+ "vitest": "5.0.0"
175
177
  }
176
178
  }
@@ -24,7 +24,7 @@ MODE=runtime # runtime | instance
24
24
  ENV=development # development | production
25
25
  DEPLOY_ID=dd-test # deploy id (instance mode: parent of conf.instances.json)
26
26
  INSTANCE_IDS= # instance mode: csv of ids (default: all in conf.instances.json)
27
- IMAGE=underpost/wp:v3.3.76 # runtime mode image (instance mode reads image from conf)
27
+ IMAGE=underpost/wp:v3.4.0 # runtime mode image (instance mode reads image from conf)
28
28
  VERSIONS=green # csv of blue/green versions
29
29
  REPLICAS=1 # replicas per deployment
30
30
  NAMESPACE=default # k8s namespace
@@ -4,7 +4,7 @@ import { FileController } from './file.controller.js';
4
4
 
5
5
  class FileRouter {
6
6
  /**
7
- * authenticated writes, admin-only deletes.
7
+ * authenticated writes, admin-only deletes and full listing.
8
8
  * @param {import('../types.js').RouterOptions} options
9
9
  * @returns {import('express').Router}
10
10
  */
@@ -13,6 +13,12 @@ class FileRouter {
13
13
  router.post(`/:id`, options.authMiddleware, async (req, res) => await FileController.post(req, res, options));
14
14
  router.post(`/`, options.authMiddleware, async (req, res) => await FileController.post(req, res, options));
15
15
  router.get(`/blob/:id`, async (req, res) => await FileController.get(req, res, options));
16
+ router.get(
17
+ `/all`,
18
+ options.authMiddleware,
19
+ adminGuard,
20
+ async (req, res) => await FileController.get(req, res, options),
21
+ );
16
22
  router.get(`/:id`, async (req, res) => await FileController.get(req, res, options));
17
23
  router.get(`/`, async (req, res) => await FileController.get(req, res, options));
18
24
  router.delete(
@@ -3389,7 +3389,7 @@ fi
3389
3389
  throw new Error('Invalid token format');
3390
3390
  }
3391
3391
 
3392
- logger.info('Maas api token generated', { consumer_key, consumer_secret, token_key, token_secret });
3392
+ logger.info('Maas api token generated', { consumer_key, token_key });
3393
3393
  return { consumer_key, consumer_secret, token_key, token_secret };
3394
3394
  },
3395
3395
 
@@ -36,6 +36,13 @@ import os from 'os';
36
36
  import fs from 'fs-extra';
37
37
  import Underpost from '../index.js';
38
38
 
39
+ /**
40
+ * @constant CERT_MANAGER_VERSION
41
+ * @description Pinned cert-manager chart version.
42
+ * @memberof UnderpostCluster
43
+ */
44
+ const CERT_MANAGER_VERSION = 'v1.17.0';
45
+
39
46
  const logger = loggerFactory(import.meta);
40
47
 
41
48
  const GATEWAY_API_RELEASE = 'v1.5.1';
@@ -670,22 +677,12 @@ EOF
670
677
  }
671
678
  }
672
679
 
673
- if (options.certManager) {
674
- if (!Underpost.kubectl.get('cert-manager').find((p) => p.STATUS === 'Running')) {
675
- shellExec(`helm repo add jetstack https://charts.jetstack.io --force-update`);
676
- shellExec(
677
- `helm install cert-manager jetstack/cert-manager \
678
- --namespace cert-manager \
679
- --create-namespace \
680
- --version v1.17.0 \
681
- --set crds.enabled=true`,
682
- );
683
- }
684
-
685
- const letsEncName = 'letsencrypt-prod';
686
- shellExec(`sudo kubectl delete ClusterIssuer ${letsEncName} --ignore-not-found`);
687
- shellExec(`sudo kubectl apply -f ${underpostRoot}/manifests/${letsEncName}.yaml -n ${options.namespace}`);
688
- }
680
+ if (options.certManager)
681
+ Underpost.cluster.installCertManager({
682
+ namespace: options.namespace,
683
+ env: options.dev ? 'development' : 'production',
684
+ solver: Underpost.cluster.acmeSolverFactory({ options, presence: Underpost.cluster.ingressStackPresence() }),
685
+ });
689
686
  },
690
687
 
691
688
  /**
@@ -1100,6 +1097,57 @@ EOF
1100
1097
  return true;
1101
1098
  },
1102
1099
 
1100
+ /**
1101
+ * @method acmeSolverFactory
1102
+ * @description Which ingress stack answers the HTTP-01 challenge.
1103
+ *
1104
+ * Read from the cluster, like {@link UnderpostCluster.ingressStackPresence}: the
1105
+ * `cluster` runner installs the issuer in a separate invocation that carries no stack
1106
+ * flag, so the flags alone would always name the Contour solver. `--contour` is the
1107
+ * only explicit override; a cluster with the Gateway API data plane installed routes
1108
+ * every hostname through it, so an Ingress solver there is unreachable.
1109
+ * @param {object} params
1110
+ * @param {object} [params.options] - Cluster init options.
1111
+ * @param {{contour: boolean, gateway: boolean}} params.presence - Installed stacks.
1112
+ * @returns {'gateway'|'ingress'} Solver kind.
1113
+ * @memberof UnderpostCluster
1114
+ */
1115
+ acmeSolverFactory({ options = {}, presence }) {
1116
+ if (options.contour === true && options.gatewayApi !== true) return 'ingress';
1117
+ return options.gatewayApi === true || presence.gateway ? 'gateway' : 'ingress';
1118
+ },
1119
+
1120
+ /**
1121
+ * @method installCertManager
1122
+ * @description Installs or upgrades cert-manager and applies the Let's Encrypt
1123
+ * ClusterIssuer for the stack in use.
1124
+ *
1125
+ * `upgrade --install` rather than a one-time install: the Gateway API solver is
1126
+ * ignored by a controller that was not started with `enableGatewayAPI`, so a cluster
1127
+ * whose cert-manager predates the Gateway API migration must be re-configured, not
1128
+ * skipped because a pod is Running. The version is pinned, so on a cluster already at
1129
+ * it the upgrade is a no-op.
1130
+ * @param {object} params
1131
+ * @param {string} [params.namespace] - Namespace the deploys' Gateways live in.
1132
+ * @param {string} [params.env] - Environment the routed deploys publish under.
1133
+ * @param {'gateway'|'ingress'} params.solver - Which stack answers the challenge.
1134
+ * @returns {{solver: string, parentRefs: Array<{name: string, namespace: string}>}} What was applied.
1135
+ * @memberof UnderpostCluster
1136
+ */
1137
+ installCertManager({ namespace = 'default', env = 'production', solver }) {
1138
+ const gatewayValues =
1139
+ solver === 'gateway'
1140
+ ? ' --set config.apiVersion=controller.config.cert-manager.io/v1alpha1' +
1141
+ ' --set config.kind=ControllerConfiguration --set config.enableGatewayAPI=true'
1142
+ : '';
1143
+ shellExec(`helm repo add jetstack https://charts.jetstack.io --force-update`);
1144
+ shellExec(
1145
+ `helm upgrade --install cert-manager jetstack/cert-manager --namespace cert-manager --create-namespace` +
1146
+ ` --version ${CERT_MANAGER_VERSION} --set crds.enabled=true${gatewayValues} --wait --timeout 5m`,
1147
+ );
1148
+ return Underpost.deploy.applyLetsEncryptIssuer({ solver, namespace, env });
1149
+ },
1150
+
1103
1151
  /**
1104
1152
  * @method ingressStackPresence
1105
1153
  * @description Which ingress data planes are installed right now.
@@ -1358,14 +1406,21 @@ EOF
1358
1406
  }
1359
1407
 
1360
1408
  try {
1361
- shellExec(`kubectl apply -f - -n ${namespace} <<'EOF'
1409
+ // The manifest carries the whole nginx.conf: echoing it as the command line
1410
+ // buries the lines that follow it — the fallback probes above all — under
1411
+ // a page of routing tables on every sync.
1412
+ logger.info('Applying the underpost ingress manifests', { namespace, node: ingressNode });
1413
+ shellExec(
1414
+ `kubectl apply -f - -n ${namespace} <<'EOF'
1362
1415
  ${underpostIngressManifestsFactory({
1363
1416
  namespace,
1364
1417
  conf,
1365
1418
  nodeName: ingressNode,
1366
1419
  })}
1367
1420
  EOF
1368
- `);
1421
+ `,
1422
+ { disableLog: true },
1423
+ );
1369
1424
  } catch (error) {
1370
1425
  if (shouldHotReload)
1371
1426
  execIngress(`sh -c 'cp /tmp/nginx.previous.conf /tmp/nginx.conf && nginx -s reload -c /tmp/nginx.conf'`, {
package/src/cli/deploy.js CHANGED
@@ -50,6 +50,8 @@ import nodePath from 'node:path';
50
50
  import dotenv from 'dotenv';
51
51
  import os from 'node:os';
52
52
  import crypto from 'node:crypto';
53
+ import { fileURLToPath } from 'node:url';
54
+ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
53
55
  import { appSecretName } from './app.js';
54
56
  import { domainContextFactory } from './domains.js';
55
57
  import Underpost from '../index.js';
@@ -93,6 +95,23 @@ const UPSTREAM_FAILURE_STATUSES = [502, 503, 504];
93
95
 
94
96
  const CONTAINER_ENGINE_ROOT = '/home/dd/engine';
95
97
 
98
+ /**
99
+ * @constant LETSENCRYPT_ISSUER_NAME
100
+ * @description The ClusterIssuer every generated Certificate references.
101
+ * @memberof UnderpostDeploy
102
+ */
103
+ const LETSENCRYPT_ISSUER_NAME = 'letsencrypt-prod';
104
+
105
+ /**
106
+ * @constant LETSENCRYPT_ISSUER_MANIFEST
107
+ * @description Base ClusterIssuer manifest: account, ACME server and key Secret. Resolved
108
+ * from this module so an installed CLI and a checkout read the same file.
109
+ * @memberof UnderpostDeploy
110
+ */
111
+ const LETSENCRYPT_ISSUER_MANIFEST = fileURLToPath(
112
+ new URL(`../../manifests/${LETSENCRYPT_ISSUER_NAME}.yaml`, import.meta.url),
113
+ );
114
+
96
115
  // The manifest set `deploy --build-manifest` produces for one environment.
97
116
  const DEFAULT_MANIFEST_FILES = [
98
117
  'deployment.yaml',
@@ -255,12 +274,68 @@ ${buildKindPorts(fromPort, toPort)}`;
255
274
  : Underpost.deploy.trafficServiceYamlFactory({ deployId, env, traffic, namespace, fromPort, toPort });
256
275
  const escaped = `${deployId}-${env}`.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
257
276
  manifest = manifest.replace(new RegExp(`app: ${escaped}-(?:blue|green)`), `app: ${deployId}-${env}-${traffic}`);
258
- shellExec(`kubectl apply -f - -n ${namespace} <<'EOF'
277
+ logger.info('Applying the traffic Service', { deployId, env, traffic, namespace });
278
+ shellExec(
279
+ `kubectl apply -f - -n ${namespace} <<'EOF'
259
280
  ${manifest}
260
281
  EOF
261
- `);
282
+ `,
283
+ { disableLog: true },
284
+ );
262
285
  return Underpost.deploy.trafficServiceNameFactory({ deployId, env });
263
286
  },
287
+ /**
288
+ * Removes the Gateway API objects an earlier model of this deploy left behind, so
289
+ * they stop competing with the consolidated `<deployId>-<env>` Gateway.
290
+ *
291
+ * The per-host model emitted one Gateway and one `<host>-http3` ClientTrafficPolicy
292
+ * per hostname. Left in place they duplicate that hostname's listeners in the same
293
+ * merged set, and the oldest resource retains traffic. `undefined` / `undefined-http3`
294
+ * is the same problem under a different name: the consolidated objects were briefly
295
+ * emitted with an unresolved host in their metadata, and merged listeners are
296
+ * configured by the oldest policy that targets them.
297
+ *
298
+ * The namespace is listed once and only objects that exist are deleted: a sweep that
299
+ * issued a blind delete per host per kind spent two dozen `kubectl` round-trips per
300
+ * sync on names that were gone after the first migration, and buried the one line
301
+ * that mattered when something was actually removed.
302
+ * @param {object} params
303
+ * @param {string[]} params.hosts - Hostnames the deploy's `conf.server.json` declares.
304
+ * @param {string} params.namespace - Namespace the deploy's Gateway objects live in.
305
+ * @returns {{gateways: string[], policies: string[]}} Names removed, per kind.
306
+ * @memberof UnderpostDeploy
307
+ */
308
+ sweepLegacyGatewayObjects({ hosts = [], namespace = 'default' }) {
309
+ const existing = (kind) =>
310
+ new Set(
311
+ `${
312
+ shellExec(`kubectl get ${kind} -n ${namespace} -o name --ignore-not-found`, {
313
+ stdout: true,
314
+ silent: true,
315
+ silentOnError: true,
316
+ disableLog: true,
317
+ }) ?? ''
318
+ }`
319
+ .split('\n')
320
+ .map((line) => line.trim().split('/').pop())
321
+ .filter(Boolean),
322
+ );
323
+ const legacy = [...new Set(hosts.filter(Boolean)), 'undefined'];
324
+ const gateways = existing('Gateway');
325
+ const policies = existing('ClientTrafficPolicy');
326
+ const removed = {
327
+ gateways: legacy.filter((name) => gateways.has(name)),
328
+ policies: legacy.map((name) => `${name}-http3`).filter((name) => policies.has(name)),
329
+ };
330
+ for (const name of removed.gateways)
331
+ shellExec(`sudo kubectl delete Gateway ${name} -n ${namespace} --ignore-not-found`);
332
+ for (const name of removed.policies)
333
+ shellExec(`sudo kubectl delete ClientTrafficPolicy ${name} -n ${namespace} --ignore-not-found`);
334
+ if (removed.gateways.length > 0 || removed.policies.length > 0)
335
+ logger.warn('Legacy per-host Gateway API objects removed', { namespace, ...removed });
336
+ return removed;
337
+ },
338
+
264
339
  /**
265
340
  * Removes the route kind owned by the inactive ingress stack. The active
266
341
  * route is already published before this runs, so migration never creates a
@@ -1112,6 +1187,125 @@ spec:
1112
1187
  kind: ClusterIssuer
1113
1188
  secretName: ${host}`;
1114
1189
  },
1190
+
1191
+ /**
1192
+ * The Gateways an ACME HTTP-01 challenge route must attach to: every Gateway the
1193
+ * namespace holds now, plus the one each routed deploy publishes under, whether or
1194
+ * not it exists yet. A challenge HTTPRoute lists them all as parents; the data plane
1195
+ * attaches it to whichever one carries a listener for the challenged hostname, and
1196
+ * reports the rest as unmatched, which is harmless.
1197
+ *
1198
+ * The union rather than either side alone: the live list covers Gateways a deploy
1199
+ * outside `dd.routes` created, the route table covers a deploy that has not been
1200
+ * applied yet — an issuer that only knew the live list would have to be re-applied
1201
+ * after every first deploy before that deploy's certificate could issue.
1202
+ * @param {object} params
1203
+ * @param {string} [params.namespace] - Namespace the Gateways live in.
1204
+ * @param {string} [params.env] - Environment the routed deploys publish under.
1205
+ * @returns {Array<{name: string, namespace: string}>} Parent references, sorted by name.
1206
+ * @memberof UnderpostDeploy
1207
+ */
1208
+ acmeGatewayParentRefsFactory({ namespace = 'default', env = 'production' } = {}) {
1209
+ const live = `${
1210
+ shellExec(`kubectl get gateway -n ${namespace} -o name --ignore-not-found`, {
1211
+ stdout: true,
1212
+ silent: true,
1213
+ silentOnError: true,
1214
+ disableLog: true,
1215
+ }) ?? ''
1216
+ }`
1217
+ .split('\n')
1218
+ .map((line) => line.trim().split('/').pop())
1219
+ .filter(Boolean);
1220
+ const routed = readDeployRoutes().map((deployId) => Underpost.deploy.gatewayNameFactory({ deployId, env }));
1221
+ return [...new Set([...live, ...routed])].sort().map((name) => ({ name, namespace }));
1222
+ },
1223
+
1224
+ /**
1225
+ * Renders the Let's Encrypt ClusterIssuer for the ingress stack that actually
1226
+ * carries the HTTP-01 challenge.
1227
+ *
1228
+ * The base manifest fixes the account (email, ACME server, key Secret); only the
1229
+ * solver is decided here. With the Gateway API stack, the challenge must be answered
1230
+ * through an HTTPRoute on the deploy's Gateway: an `ingress`-class solver creates an
1231
+ * Ingress nothing on the request path reads, so `/.well-known/acme-challenge/*` is
1232
+ * routed by the application's own HTTPRoute to the workload — a 404 — or, with no
1233
+ * workload behind it, to the maintenance fallback. Every renewal fails that way while
1234
+ * the solver pods sit there ready, and the failure only surfaces when the certificate
1235
+ * expires.
1236
+ * @param {object} params
1237
+ * @param {'gateway'|'ingress'} params.solver - Which stack answers the challenge.
1238
+ * @param {Array<{name: string, namespace: string}>} [params.parentRefs] - Gateways for the `gateway` solver.
1239
+ * @param {string} [params.basePath] - Base ClusterIssuer manifest.
1240
+ * @returns {string} ClusterIssuer YAML.
1241
+ * @throws {Error} When the Gateway solver is requested with no Gateway to attach to.
1242
+ * @memberof UnderpostDeploy
1243
+ */
1244
+ letsEncryptIssuerYamlFactory({ solver, parentRefs = [], basePath = LETSENCRYPT_ISSUER_MANIFEST }) {
1245
+ const issuer = yamlLoad(fs.readFileSync(basePath, 'utf8'));
1246
+ if (solver === 'gateway') {
1247
+ if (parentRefs.length === 0)
1248
+ throw new Error('[cert-manager] the Gateway API HTTP-01 solver needs at least one Gateway parentRef');
1249
+ issuer.spec.acme.solvers = [
1250
+ {
1251
+ http01: {
1252
+ gatewayHTTPRoute: {
1253
+ parentRefs: parentRefs.map(({ name, namespace }) => ({ name, namespace, kind: 'Gateway' })),
1254
+ },
1255
+ },
1256
+ },
1257
+ ];
1258
+ } else issuer.spec.acme.solvers = [{ http01: { ingress: { class: 'contour' } } }];
1259
+ return yamlDump(issuer, { lineWidth: -1, noRefs: true });
1260
+ },
1261
+
1262
+ /**
1263
+ * Whether the Let's Encrypt ClusterIssuer is installed. False both when it was never
1264
+ * applied and when cert-manager's CRDs are absent, which is the same answer here.
1265
+ * @returns {boolean}
1266
+ * @memberof UnderpostDeploy
1267
+ */
1268
+ letsEncryptIssuerExists() {
1269
+ return (
1270
+ `${
1271
+ shellExec(`kubectl get clusterissuer ${LETSENCRYPT_ISSUER_NAME} -o name --ignore-not-found`, {
1272
+ stdout: true,
1273
+ silent: true,
1274
+ silentOnError: true,
1275
+ disableLog: true,
1276
+ }) ?? ''
1277
+ }`.trim().length > 0
1278
+ );
1279
+ },
1280
+
1281
+ /**
1282
+ * Applies the Let's Encrypt ClusterIssuer for the stack in use, so certificates keep
1283
+ * renewing through whichever data plane the deploy's hostnames are served by.
1284
+ * @param {object} params
1285
+ * @param {'gateway'|'ingress'} params.solver - Which stack answers the challenge.
1286
+ * @param {string} [params.namespace] - Namespace the Gateways live in.
1287
+ * @param {string} [params.env] - Environment the routed deploys publish under.
1288
+ * @param {string} [params.basePath] - Base ClusterIssuer manifest.
1289
+ * @returns {{solver: string, parentRefs: Array<{name: string, namespace: string}>}} What was applied.
1290
+ * @memberof UnderpostDeploy
1291
+ */
1292
+ applyLetsEncryptIssuer({ solver, namespace = 'default', env = 'production', basePath } = {}) {
1293
+ const parentRefs = solver === 'gateway' ? Underpost.deploy.acmeGatewayParentRefsFactory({ namespace, env }) : [];
1294
+ const manifest = Underpost.deploy.letsEncryptIssuerYamlFactory({ solver, parentRefs, basePath });
1295
+ const stagePath = `/dev/shm/underpost-${LETSENCRYPT_ISSUER_NAME}.yaml`;
1296
+ fs.writeFileSync(stagePath, manifest, 'utf8');
1297
+ try {
1298
+ shellExec(`sudo kubectl apply -f ${stagePath}`);
1299
+ } finally {
1300
+ fs.removeSync(stagePath);
1301
+ }
1302
+ logger.info("Let's Encrypt ClusterIssuer applied", {
1303
+ issuer: LETSENCRYPT_ISSUER_NAME,
1304
+ solver,
1305
+ gateways: parentRefs.map(({ name }) => name),
1306
+ });
1307
+ return { solver, parentRefs };
1308
+ },
1115
1309
  /**
1116
1310
  * Mirrors each deploy's built development manifests into the project tree, and refreshes the
1117
1311
  * public default configuration they belong to.
@@ -2341,34 +2535,15 @@ EOF`);
2341
2535
  });
2342
2536
  }
2343
2537
 
2344
- for (const host of Object.keys(confServer)) {
2345
- if (!options.disableUpdateProxy) {
2346
- // The host's route object is left in place and replaced by the `apply`
2347
- // below. Deleting it first unpublished the hostname for the whole
2348
- // reconciliation window, so every promote dropped live requests before
2349
- // the new colour was ever the question.
2350
- //
2351
- // A deploy that previously ran the per-host model left a Gateway
2352
- // named after each host. Those are superseded by the consolidated
2353
- // one, and leaving them behind duplicates that hostname's listeners
2354
- // in the same merged set. The oldest resource would retain traffic.
2355
- //
2356
- // `undefined-http3` is the same problem under a different name: the
2357
- // consolidated policy was briefly emitted with an unresolved host in
2358
- // its metadata. Merged listeners are configured by the oldest policy
2359
- // that targets them, so that object outranks the correctly named one
2360
- // for as long as it exists.
2361
- if (options.gatewayApi)
2362
- for (const name of [host, 'undefined']) {
2363
- shellExec(`sudo kubectl delete Gateway ${name} -n ${namespace} --ignore-not-found`, { silent: true });
2364
- shellExec(`sudo kubectl delete ClientTrafficPolicy ${name}-http3 -n ${namespace} --ignore-not-found`, {
2365
- silent: true,
2366
- });
2367
- }
2368
- if (Underpost.deploy.isCertManagerContext({ host, env, options }))
2369
- shellExec(`sudo kubectl delete Certificate ${host} -n ${namespace} --ignore-not-found`);
2370
- }
2371
- }
2538
+ // The host's route object is left in place and replaced by the `apply`
2539
+ // below. Deleting it first unpublished the hostname for the whole
2540
+ // reconciliation window, so every promote dropped live requests before
2541
+ // the new colour was ever the question.
2542
+ if (!options.disableUpdateProxy && options.gatewayApi)
2543
+ Underpost.deploy.sweepLegacyGatewayObjects({ hosts: Object.keys(confServer), namespace });
2544
+ for (const host of Object.keys(confServer))
2545
+ if (!options.disableUpdateProxy && Underpost.deploy.isCertManagerContext({ host, env, options }))
2546
+ shellExec(`sudo kubectl delete Certificate ${host} -n ${namespace} --ignore-not-found`);
2372
2547
 
2373
2548
  const manifestsPath =
2374
2549
  env === 'production'
@@ -2480,6 +2655,11 @@ EOF`);
2480
2655
  }
2481
2656
 
2482
2657
  if (Underpost.deploy.isCertManagerContext({ host: Object.keys(confServer)[0], env, options })) {
2658
+ // The issuer's challenge route must list this deploy's Gateway as a parent, or
2659
+ // the certificates applied below can never issue. Re-rendered from the live
2660
+ // Gateway list, so a deploy created after `cluster --cert-manager` is covered.
2661
+ if (options.gatewayApi && Underpost.deploy.letsEncryptIssuerExists())
2662
+ Underpost.deploy.applyLetsEncryptIssuer({ solver: 'gateway', namespace, env });
2483
2663
  const secretPath = `./${manifestsPath}/secret.yaml`;
2484
2664
  if (fs.existsSync(secretPath) && fs.readFileSync(secretPath, 'utf8').trim()) {
2485
2665
  shellExec(`sudo kubectl apply -f ${secretPath} -n ${namespace}`);
@@ -3143,3 +3323,4 @@ spec:
3143
3323
  }
3144
3324
 
3145
3325
  export default UnderpostDeploy;
3326
+ export { LETSENCRYPT_ISSUER_MANIFEST, LETSENCRYPT_ISSUER_NAME };
@@ -192,7 +192,7 @@ services:
192
192
  MONGO_IMAGE=mongo:latest
193
193
  VALKEY_IMAGE=valkey/valkey:latest
194
194
  APP_IMAGE=underpost/underpost-engine
195
- APP_TAG=v3.3.76
195
+ APP_TAG=v3.4.0
196
196
  PROXY_IMAGE=nginx:stable-alpine
197
197
  PROMETHEUS_IMAGE=prom/prometheus:latest
198
198
  GRAFANA_IMAGE=grafana/grafana:latest
package/src/cli/index.js CHANGED
@@ -7,6 +7,7 @@ import { commitData } from '../client/components/core/CommonJs.js';
7
7
  import { registerDomainCommand } from './domains.js';
8
8
  import { TEST_TIERS, testSuiteNames } from '../server/build/testing.js';
9
9
  import { EXECUTION_PROFILES, profileFromOptionsFactory, setExecutionProfile } from '../server/build/execution.js';
10
+ import { SEVERITIES } from '../server/security/socketsecurity.js';
10
11
 
11
12
  import Underpost from '../index.js';
12
13
 
@@ -1403,6 +1404,41 @@ program
1403
1404
  )
1404
1405
  .action(Underpost.package.callback);
1405
1406
 
1407
+ program
1408
+ .command('socketsecurity')
1409
+ .option(
1410
+ '--audit',
1411
+ "Audits the checkout (default). Dependency security: segregation, npm audit, the Socket patch scan and, with SOCKET_CLI_API_TOKEN set, the Socket full scan of the dependency manifests. Source code risk: with SOCKET_CLI_API_TOKEN set, the alerts Socket's registry analysis raises on this project's own published package, as refactoring tasks. Writes the JSON and Markdown reports.",
1412
+ )
1413
+ .option(
1414
+ '--reach',
1415
+ 'Runs the full application reachability analysis over the local working tree with the Socket scan (tier 1 evidence); no pushed commit is needed.',
1416
+ )
1417
+ .option(
1418
+ '--ci',
1419
+ 'Audits, then exits non-zero when the Socket scan violates the organization policy or a finding remains at --fail-on.',
1420
+ )
1421
+ .option('--fail-on <severity>', `Severity --ci fails at. One of: ${SEVERITIES.join(', ')}. Defaults to high.`)
1422
+ .option(
1423
+ '--fix',
1424
+ 'Runs the remediation pass before the audit: npm audit fix, every available Socket patch, socket fix without major upgrades, socket patch apply.',
1425
+ )
1426
+ .option('--major', 'With --fix: lets socket fix apply major upgrades.')
1427
+ .option('--patch-scan', 'Lists the Socket patches available for the installed packages.')
1428
+ .option(
1429
+ '--patch-get <identifier>',
1430
+ 'Downloads one patch (UUID, CVE, GHSA or PURL) into .socket/manifest.json and applies it.',
1431
+ )
1432
+ .option('--patch-apply', 'Applies the patch manifest; the prepare hook. A host without the Socket CLI skips it.')
1433
+ .option('--patch-setup', 'Installs the prepare hook in package.json.')
1434
+ .option('--out <dir>', 'Report directory (default: security-reports).')
1435
+ .option('--dry-run', 'Previews --patch-setup; verifies --patch-apply without writing.')
1436
+ .description(
1437
+ 'Security audit through Socket.dev: dependency security (advisories, supply chain alerts, reachability, ' +
1438
+ "security patches in .socket/manifest.json) and source code risk (alerts on this project's own code).",
1439
+ )
1440
+ .action(Underpost.socketSecurity.callback);
1441
+
1406
1442
  program
1407
1443
  .command('release')
1408
1444
  .argument('[version]', 'The new version string to set (e.g., "3.1.4"). Defaults to current version.')
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { loggerFactory, redactSensitiveText } from '../server/ops/logger.js';
10
- import { shellExec, sleepSync } from '../server/runtime/process.js';
10
+ import { shellArgumentFactory, shellExec, sleepSync } from '../server/runtime/process.js';
11
11
  import { timer } from '../client/components/core/CommonJs.js';
12
12
  import Underpost from '../index.js';
13
13
 
@@ -275,7 +275,8 @@ class UnderpostKubectl {
275
275
  },
276
276
 
277
277
  /**
278
- * Runs a shell command inside a pod container via `kubectl exec`.
278
+ * Runs a shell command inside a pod container via `kubectl exec`. The command is quoted as one
279
+ * argument, so the pod's shell parses it and the host shell never expands it.
279
280
  * @param {object} params
280
281
  * @param {string} params.podName - Target pod name.
281
282
  * @param {string} params.namespace - Pod namespace.
@@ -287,7 +288,7 @@ class UnderpostKubectl {
287
288
  * @memberof UnderpostKubectl
288
289
  */
289
290
  exec({ podName, namespace, command, retries = DEFAULT_POD_RETRIES, timeoutSeconds = 0 }) {
290
- const kubectlCmd = `sudo kubectl exec -n ${namespace} -i ${podName} -- sh -c "${command}"`;
291
+ const kubectlCmd = `sudo kubectl exec -n ${shellArgumentFactory(namespace)} -i ${shellArgumentFactory(podName)} -- sh -c ${shellArgumentFactory(command)}`;
291
292
  return Underpost.kubectl.run(kubectlCmd, {
292
293
  context: `exec in pod ${podName}`,
293
294
  retries,