underpost 3.2.70 → 3.2.80

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 (38) hide show
  1. package/.github/workflows/publish.ci.yml +3 -3
  2. package/.github/workflows/release.cd.yml +1 -1
  3. package/CHANGELOG.md +1177 -1038
  4. package/CLI-HELP.md +3 -1
  5. package/README.md +3 -3
  6. package/bin/build.js +10 -4
  7. package/docker-compose.yml +1 -1
  8. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  9. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  10. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  11. package/package.json +10 -10
  12. package/scripts/test-monitor.sh +1 -1
  13. package/src/api/core/core.controller.js +4 -65
  14. package/src/api/core/core.router.js +8 -14
  15. package/src/api/default/default.controller.js +2 -70
  16. package/src/api/default/default.router.js +7 -17
  17. package/src/api/document/document.controller.js +5 -77
  18. package/src/api/document/document.router.js +9 -13
  19. package/src/api/file/file.controller.js +9 -53
  20. package/src/api/file/file.router.js +14 -6
  21. package/src/api/test/test.controller.js +8 -53
  22. package/src/api/test/test.router.js +1 -4
  23. package/src/cli/cluster.js +31 -11
  24. package/src/cli/db.js +4 -2
  25. package/src/cli/deploy.js +51 -9
  26. package/src/cli/docker-compose.js +163 -9
  27. package/src/cli/fs.js +0 -1
  28. package/src/cli/image.js +25 -7
  29. package/src/cli/index.js +5 -0
  30. package/src/cli/release.js +4 -0
  31. package/src/cli/repository.js +13 -2
  32. package/src/cli/run.js +151 -78
  33. package/src/cli/ssh.js +30 -11
  34. package/src/client/components/core/Modal.js +38 -4
  35. package/src/index.js +1 -1
  36. package/src/server/conf.js +163 -0
  37. package/src/server/downloader.js +3 -3
  38. package/src/server/middlewares.js +152 -0
@@ -1772,6 +1772,165 @@ const loadConfServerJson = (jsonPath, options) => {
1772
1772
  return options && options.resolve === true ? resolveConfSecrets(raw) : raw;
1773
1773
  };
1774
1774
 
1775
+ /**
1776
+ * @method deepReplaceToken
1777
+ * @description Recursively replaces every occurrence of `from` with `to` in all
1778
+ * string leaves of a JSON-shaped value, returning a new structure.
1779
+ * @param {*} value - Any JSON-serialisable value.
1780
+ * @param {string} from - Token to replace.
1781
+ * @param {string} to - Replacement token.
1782
+ * @returns {*} A structurally identical value with the token replaced.
1783
+ * @memberof ServerConfBuilder
1784
+ */
1785
+ const deepReplaceToken = (value, from, to) => {
1786
+ if (typeof value === 'string') return value.split(from).join(to);
1787
+ if (Array.isArray(value)) return value.map((entry) => deepReplaceToken(entry, from, to));
1788
+ if (value && typeof value === 'object')
1789
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deepReplaceToken(v, from, to)]));
1790
+ return value;
1791
+ };
1792
+
1793
+ /**
1794
+ * @method readConfInstances
1795
+ * @description Reads `conf.instances.json` and returns the bare array of instance
1796
+ * entries. The canonical shape is a plain array; the historical `{ instances }`
1797
+ * object wrapper is still unwrapped for backward compatibility. Multi-instance is
1798
+ * declared per entry under `entry.multiInstance` (not deploy-wide).
1799
+ * @param {string} deployId - Deployment identifier (e.g. `dd-cyberia`).
1800
+ * @returns {Array<object>} Instance entries.
1801
+ * @memberof ServerConfBuilder
1802
+ */
1803
+ const readConfInstances = (deployId) => {
1804
+ const raw = JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.instances.json`, 'utf8'));
1805
+ return Array.isArray(raw) ? raw : raw.instances || [];
1806
+ };
1807
+
1808
+ /**
1809
+ * @method loadInstanceTopology
1810
+ * @description Returns `{ default, variants }` describing the deploy's instance
1811
+ * variants, or `null` when the deploy is single-instance. The topology is
1812
+ * declared per instance entry (`entry.multiInstance.default` / `.variants`); all
1813
+ * multi-instance entries share the same variant set, so this returns it from the
1814
+ * first entry that declares one.
1815
+ * @param {string} deployId - Deployment identifier (e.g. `dd-cyberia`).
1816
+ * @returns {?{default: string, variants: Array<object>}} The topology, or `null`.
1817
+ * @memberof ServerConfBuilder
1818
+ */
1819
+ const loadInstanceTopology = (deployId) => {
1820
+ for (const entry of readConfInstances(deployId)) {
1821
+ const mi = entry.multiInstance;
1822
+ if (mi && Array.isArray(mi.variants) && mi.variants.length > 0)
1823
+ return { default: mi.default || mi.variants[0].code, variants: mi.variants };
1824
+ }
1825
+ return null;
1826
+ };
1827
+
1828
+ /**
1829
+ * @method resolveInstanceEnvValue
1830
+ * @description Resolves one declared env value. A plain string is used verbatim;
1831
+ * an object is treated as env-scoped (`{ development, production }`), matching the
1832
+ * convention already used by `lifecycle` / `readinessProbe` / `livenessProbe`.
1833
+ * Placeholders are substituted from the variant tokens.
1834
+ * @param {string|object} value - Declared value.
1835
+ * @param {string} env - `development` | `production`.
1836
+ * @param {Object<string,string>} tokens - Placeholder name → replacement.
1837
+ * @returns {?string} Resolved value, or `null` when the env has no entry.
1838
+ * @memberof ServerConfBuilder
1839
+ */
1840
+ const resolveInstanceEnvValue = (value, env, tokens) => {
1841
+ const scoped = value && typeof value === 'object' && !Array.isArray(value) ? value[env] : value;
1842
+ if (scoped === undefined || scoped === null) return null;
1843
+ return `${scoped}`.replace(/\{\{(\w+)\}\}/g, (match, token) => (token in tokens ? tokens[token] : match));
1844
+ };
1845
+
1846
+ /**
1847
+ * @method loadConfInstances
1848
+ * @description Loads `conf.instances.json` and expands every entry carrying a
1849
+ * `multiInstance` block into one concrete instance per declared variant.
1850
+ *
1851
+ * A template entry is never deployed as-is once variants exist: each variant
1852
+ * produces its own entry whose id, env file path, volume mount and
1853
+ * container-status strings are derived by replacing the template id token
1854
+ * throughout. The variant whose `slug` is empty keeps the template id verbatim,
1855
+ * so pre-existing deployments, PVCs and env directories survive the move to
1856
+ * multi-instance untouched.
1857
+ *
1858
+ * Nothing here is application-specific: the variant set (`default` + `variants`),
1859
+ * which env keys an instance needs (`env`), and prefix-stripping (`stripPathPrefix`)
1860
+ * are all declared per entry under `entry.multiInstance`. Env values may use the
1861
+ * `{{code}}`, `{{slug}}`, `{{path}}`, `{{id}}` and `{{default}}` placeholders and
1862
+ * may be env-scoped objects.
1863
+ *
1864
+ * Every expanded entry carries three extra fields consumed by the deploy runners:
1865
+ * `instanceCode` (the variant this instance serves), `templateId` (so targeting
1866
+ * the template id acts on the whole family) and `instanceEnv` (the resolved env
1867
+ * keys to write, keyed by environment: `{ development, production }`, so a build
1868
+ * in either mode can emit a complete env directory).
1869
+ *
1870
+ * @param {string} deployId - Deployment identifier (e.g. `dd-cyberia`).
1871
+ * @returns {Array<object>} Expanded instance entries.
1872
+ * @memberof ServerConfBuilder
1873
+ */
1874
+ const INSTANCE_ENVS = ['development', 'production'];
1875
+
1876
+ const loadConfInstances = (deployId) => {
1877
+ const expanded = [];
1878
+ for (const entry of readConfInstances(deployId)) {
1879
+ const spec = entry.multiInstance;
1880
+ const variants = spec?.variants;
1881
+ if (!Array.isArray(variants) || 0 === variants.length) {
1882
+ expanded.push(entry);
1883
+ continue;
1884
+ }
1885
+ for (const variant of variants) {
1886
+ const id = variant.slug ? `${entry.id}-${variant.slug}` : entry.id;
1887
+ const instance = variant.slug ? deepReplaceToken(entry, entry.id, id) : JSON.parse(JSON.stringify(entry));
1888
+ delete instance.multiInstance;
1889
+ instance.id = id;
1890
+ instance.path = variant.path;
1891
+ instance.instanceCode = variant.code;
1892
+ instance.templateId = entry.id;
1893
+
1894
+ const tokens = {
1895
+ code: variant.code,
1896
+ slug: variant.slug || '',
1897
+ path: variant.path,
1898
+ id,
1899
+ default: spec.default || '',
1900
+ };
1901
+ // Resolve the declared env keys for both environments so a build in either
1902
+ // mode writes a complete env directory (development.env + production.env).
1903
+ instance.instanceEnv = Object.fromEntries(INSTANCE_ENVS.map((e) => [e, {}]));
1904
+ for (const [key, value] of Object.entries(spec.env || {}))
1905
+ for (const e of INSTANCE_ENVS) {
1906
+ const resolved = resolveInstanceEnvValue(value, e, tokens);
1907
+ if (resolved !== null) instance.instanceEnv[e][key] = resolved;
1908
+ }
1909
+
1910
+ // A backend that serves its routes at the root knows nothing about the
1911
+ // variant prefix — it is selected by env instead. Strip the prefix at the
1912
+ // proxy so the runtime stays instance-agnostic.
1913
+ if (spec.stripPathPrefix && variant.path !== '/')
1914
+ instance.pathRewritePolicy = [{ prefix: variant.path, replacement: '/' }];
1915
+ expanded.push(instance);
1916
+ }
1917
+ }
1918
+ return expanded;
1919
+ };
1920
+
1921
+ /**
1922
+ * @method selectConfInstances
1923
+ * @description Filters expanded instances for a deploy target. Targeting a
1924
+ * template id (`mmo-server`) selects the whole instance family; targeting a
1925
+ * concrete id (`mmo-server-forest`) selects just that one.
1926
+ * @param {Array<object>} instances - Expanded entries from {@link loadConfInstances}.
1927
+ * @param {string} id - Target instance id or template id.
1928
+ * @returns {Array<object>} Matching entries.
1929
+ * @memberof ServerConfBuilder
1930
+ */
1931
+ const selectConfInstances = (instances, id) =>
1932
+ instances.filter((instance) => instance.id === id || instance.templateId === id);
1933
+
1775
1934
  /**
1776
1935
  * Creates and writes the /etc/hosts file for a deployment.
1777
1936
  * @method etcHostFactory
@@ -2100,6 +2259,10 @@ git add .`);
2100
2259
  export {
2101
2260
  Config,
2102
2261
  loadConf,
2262
+ loadConfInstances,
2263
+ loadInstanceTopology,
2264
+ readConfInstances,
2265
+ selectConfInstances,
2103
2266
  loadReplicas,
2104
2267
  cloneConf,
2105
2268
  getCapVariableName,
@@ -41,18 +41,18 @@ class Downloader {
41
41
  const writer = fs.createWriteStream(fullPath);
42
42
  response.data.pipe(writer);
43
43
  writer.on('finish', () => {
44
- logger.info('Download complete. File saved at', fullPath);
44
+ logger.info('Download completet');
45
45
  return resolve(fullPath);
46
46
  });
47
47
  writer.on('error', (error) => {
48
- logger.error(error, 'Error downloading the file');
48
+ logger.error('Error downloading the file');
49
49
  // Cleanup incomplete file if possible
50
50
  if (fs.existsSync(fullPath)) fs.unlinkSync(fullPath);
51
51
  return reject(error);
52
52
  });
53
53
  })
54
54
  .catch((error) => {
55
- logger.error(error, 'Error in the request');
55
+ logger.error('Error in the request');
56
56
  return reject(error);
57
57
  }),
58
58
  );
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Express middleware and controller/router helpers for engine APIs.
3
+ *
4
+ * @module src/server/middlewares.js
5
+ */
6
+
7
+ import { loggerFactory } from './logger.js';
8
+ import { moderatorGuard, adminGuard } from './auth.js';
9
+
10
+ const logger = loggerFactory(import.meta);
11
+
12
+ /**
13
+ * The public-read CORS policy: reflect the request origin (or allow any)
14
+ * and mark the resource embeddable cross-origin.
15
+ * @param {import('express').Request} req
16
+ * @param {import('express').Response} res
17
+ */
18
+ const setCrossOriginHeaders = (req, res) => {
19
+ if (req && req.headers && req.headers.origin) res.set('Access-Control-Allow-Origin', req.headers.origin);
20
+ else res.setHeader('Access-Control-Allow-Origin', '*');
21
+ res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
22
+ };
23
+
24
+ /** Express middleware form of {@link setCrossOriginHeaders}. */
25
+ const crossOriginMiddleware = (req, res, next) => {
26
+ setCrossOriginHeaders(req, res);
27
+ next();
28
+ };
29
+
30
+ /**
31
+ * Shallow request copy with `page`/`limit` parsed to integers.
32
+ * `path` and `params` are copied explicitly because spreading an Express
33
+ * request drops prototype getters.
34
+ * @param {import('express').Request} req
35
+ */
36
+ const withParsedPagination = (req) => {
37
+ const { page, limit } = req.query;
38
+ return {
39
+ ...req,
40
+ path: req.path,
41
+ params: req.params,
42
+ query: { ...req.query, page: parseInt(page), limit: parseInt(limit) },
43
+ };
44
+ };
45
+
46
+ const sendSuccess = (res, data) => res.status(200).json({ status: 'success', data });
47
+
48
+ const sendError = (res, error, status = 400) => res.status(status).json({ status: 'error', message: error.message });
49
+
50
+ /**
51
+ * Binary response with cross-origin and content headers.
52
+ * @param {import('express').Request} req
53
+ * @param {import('express').Response} res
54
+ * @param {{ buffer: Buffer, mimetype: string, filename: string, disposition?: 'inline'|'attachment' }} blob
55
+ */
56
+ const sendBlob = (req, res, { buffer, mimetype, filename, disposition = 'inline' }) => {
57
+ setCrossOriginHeaders(req, res);
58
+ res.setHeader('Content-Type', mimetype);
59
+ res.setHeader('Content-Length', buffer.length);
60
+ res.setHeader('Content-Disposition', `${disposition}; filename="${filename}"`);
61
+ return res.status(200).end(buffer);
62
+ };
63
+
64
+ /**
65
+ * Wraps a controller body with error logging and the error response envelope.
66
+ * @param {(req, res, options) => Promise<any>} fn
67
+ * @param {{ errorStatus?: number }} [config]
68
+ */
69
+ const controllerHandler =
70
+ (fn, { errorStatus = 400 } = {}) =>
71
+ async (req, res, options) => {
72
+ try {
73
+ return await fn(req, res, options);
74
+ } catch (error) {
75
+ logger.error(error, error.stack);
76
+ return sendError(res, error, errorStatus);
77
+ }
78
+ };
79
+
80
+ /**
81
+ * Builds a controller method that delegates to a service method and wraps the
82
+ * result in the success envelope.
83
+ * @param {(req, res, options) => Promise<any>} serviceFn
84
+ * @param {{ errorStatus?: number, crossOrigin?: boolean, pagination?: boolean }} [config]
85
+ */
86
+ const serviceHandler = (serviceFn, { errorStatus = 400, crossOrigin = false, pagination = false } = {}) =>
87
+ controllerHandler(
88
+ async (req, res, options) => {
89
+ if (crossOrigin) setCrossOriginHeaders(req, res);
90
+ const result = await serviceFn(pagination ? withParsedPagination(req) : req, res, options);
91
+ return sendSuccess(res, result);
92
+ },
93
+ { errorStatus },
94
+ );
95
+
96
+ /**
97
+ * Builds a standard CRUD controller class (static post/get/put/delete) from a
98
+ * service exposing the same methods. `get` parses pagination.
99
+ * @param {{ post, get, put, delete }} service
100
+ * @param {Object<string, Function>} [extend] - Extra or overriding static handlers.
101
+ */
102
+ const buildCrudController = (service, extend = {}) => {
103
+ class CrudController {
104
+ static post = serviceHandler(service.post);
105
+ static get = serviceHandler(service.get, { pagination: true });
106
+ static put = serviceHandler(service.put);
107
+ static delete = serviceHandler(service.delete);
108
+ }
109
+ Object.assign(CrudController, extend);
110
+ return CrudController;
111
+ };
112
+
113
+ /**
114
+ * Registers the standard CRUD routes with the standard guard policy:
115
+ * public reads, moderator-guarded writes, admin-guarded collection delete.
116
+ * Custom routes must be registered before calling this (generic `/:id` routes
117
+ * capture everything).
118
+ * @param {import('express').Router} router
119
+ * @param {{ post, get, put, delete }} Controller
120
+ * @param {import('../../api/types.js').RouterOptions} options
121
+ * @param {{ readGuards?: Function[], writeGuards?: Function[], deleteAllGuards?: Function[] }} [config]
122
+ * Pass empty arrays for unguarded endpoints (e.g. player-written progress)
123
+ * or explicit guard chains (e.g. admin-only reads).
124
+ * @returns {import('express').Router}
125
+ */
126
+ const registerCrudRoutes = (router, Controller, options, { readGuards = [], writeGuards, deleteAllGuards } = {}) => {
127
+ const write = writeGuards ?? [options.authMiddleware, moderatorGuard];
128
+ const deleteAll = deleteAllGuards ?? [options.authMiddleware, adminGuard];
129
+ const handle = (method) => async (req, res) => await Controller[method](req, res, options);
130
+ router.post(`/:id`, ...write, handle('post'));
131
+ router.post(`/`, ...write, handle('post'));
132
+ router.get(`/:id`, ...readGuards, handle('get'));
133
+ router.get(`/`, ...readGuards, handle('get'));
134
+ router.put(`/:id`, ...write, handle('put'));
135
+ router.put(`/`, ...write, handle('put'));
136
+ router.delete(`/:id`, ...write, handle('delete'));
137
+ router.delete(`/`, ...deleteAll, handle('delete'));
138
+ return router;
139
+ };
140
+
141
+ export {
142
+ setCrossOriginHeaders,
143
+ crossOriginMiddleware,
144
+ withParsedPagination,
145
+ sendSuccess,
146
+ sendError,
147
+ sendBlob,
148
+ controllerHandler,
149
+ serviceHandler,
150
+ buildCrudController,
151
+ registerCrudRoutes,
152
+ };