cyberia 2.99.8 → 3.0.2

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 (81) hide show
  1. package/.env.production +1 -0
  2. package/.github/workflows/engine-cyberia.cd.yml +1 -0
  3. package/.github/workflows/gitlab.ci.yml +20 -0
  4. package/.github/workflows/publish.ci.yml +18 -38
  5. package/.github/workflows/publish.cyberia.ci.yml +18 -38
  6. package/.vscode/extensions.json +8 -50
  7. package/.vscode/settings.json +0 -77
  8. package/CHANGELOG.md +171 -1
  9. package/{cli.md → CLI-HELP.md} +49 -44
  10. package/README.md +139 -0
  11. package/bin/build.js +7 -15
  12. package/bin/cyberia.js +385 -71
  13. package/bin/deploy.js +14 -151
  14. package/bin/file.js +13 -8
  15. package/bin/index.js +385 -71
  16. package/bin/zed.js +63 -2
  17. package/conf.js +32 -3
  18. package/deployment.yaml +2 -2
  19. package/jsdoc.json +1 -2
  20. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
  21. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  22. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  23. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  24. package/manifests/deployment/fastapi/initial_data.sh +4 -52
  25. package/manifests/ipfs/configmap.yaml +64 -0
  26. package/manifests/ipfs/headless-service.yaml +35 -0
  27. package/manifests/ipfs/kustomization.yaml +8 -0
  28. package/manifests/ipfs/statefulset.yaml +149 -0
  29. package/manifests/ipfs/storage-class.yaml +9 -0
  30. package/package.json +15 -11
  31. package/scripts/k3s-node-setup.sh +89 -0
  32. package/scripts/lxd-vm-setup.sh +23 -0
  33. package/scripts/rocky-setup.sh +1 -13
  34. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +2 -0
  35. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.model.js +7 -0
  36. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +93 -2
  37. package/src/api/file/file.controller.js +3 -13
  38. package/src/api/file/file.ref.json +0 -21
  39. package/src/api/ipfs/ipfs.controller.js +104 -0
  40. package/src/api/ipfs/ipfs.model.js +71 -0
  41. package/src/api/ipfs/ipfs.router.js +31 -0
  42. package/src/api/ipfs/ipfs.service.js +193 -0
  43. package/src/api/object-layer/README.md +139 -0
  44. package/src/api/object-layer/object-layer.controller.js +3 -0
  45. package/src/api/object-layer/object-layer.model.js +15 -1
  46. package/src/api/object-layer/object-layer.router.js +6 -10
  47. package/src/api/object-layer/object-layer.service.js +311 -182
  48. package/src/api/user/user.router.js +0 -47
  49. package/src/cli/baremetal.js +7 -9
  50. package/src/cli/cluster.js +95 -152
  51. package/src/cli/deploy.js +8 -5
  52. package/src/cli/index.js +31 -31
  53. package/src/cli/ipfs.js +184 -0
  54. package/src/cli/lxd.js +192 -237
  55. package/src/cli/repository.js +4 -1
  56. package/src/cli/run.js +17 -2
  57. package/src/client/components/core/Docs.js +92 -6
  58. package/src/client/components/core/LoadingAnimation.js +2 -3
  59. package/src/client/components/core/Modal.js +1 -1
  60. package/src/client/components/core/VanillaJs.js +36 -25
  61. package/src/client/components/cyberia/ObjectLayerEngineModal.js +4 -5
  62. package/src/client/components/cyberia/ObjectLayerEngineViewer.js +280 -29
  63. package/src/client/services/ipfs/ipfs.service.js +144 -0
  64. package/src/client/services/object-layer/object-layer.management.js +161 -8
  65. package/src/client/services/user/user.management.js +0 -5
  66. package/src/client/services/user/user.service.js +1 -1
  67. package/src/index.js +12 -1
  68. package/src/runtime/express/Express.js +4 -3
  69. package/src/server/auth.js +18 -18
  70. package/src/server/client-build-docs.js +178 -41
  71. package/src/server/conf.js +1 -1
  72. package/src/server/ipfs-client.js +433 -0
  73. package/src/server/logger.js +22 -10
  74. package/src/server/object-layer.js +649 -18
  75. package/src/server/semantic-layer-generator.js +1083 -0
  76. package/src/server/shape-generator.js +952 -0
  77. package/test/shape-generator.test.js +457 -0
  78. package/.vscode/zed.keymap.json +0 -39
  79. package/.vscode/zed.settings.json +0 -20
  80. package/bin/ssl.js +0 -63
  81. package/manifests/lxd/underpost-setup.sh +0 -163
@@ -1,15 +1,20 @@
1
1
  import { DefaultManagement } from '../default/default.management.js';
2
2
  import { ObjectLayerService } from './object-layer.service.js';
3
- import { commonUserGuard } from '../../components/core/CommonJs.js';
3
+ import { commonUserGuard, commonModeratorGuard } from '../../components/core/CommonJs.js';
4
4
  import { getProxyPath, setPath, setQueryParams } from '../../components/core/Router.js';
5
5
  import { ObjectLayerEngineModal } from '../../components/cyberia/ObjectLayerEngineModal.js';
6
6
  import { ObjectLayerEngineViewer } from '../../components/cyberia/ObjectLayerEngineViewer.js';
7
7
  import { s } from '../../components/core/VanillaJs.js';
8
8
  import { Modal } from '../../components/core/Modal.js';
9
9
  import { BtnIcon } from '../../components/core/BtnIcon.js';
10
+ import { NotificationManager } from '../../components/core/NotificationManager.js';
11
+ import { AgGrid } from '../../components/core/AgGrid.js';
10
12
 
11
13
  const ObjectLayerManagement = {
12
- RenderTable: async ({ Elements, idModal }) => {
14
+ RenderTable: async ({ Elements, idModal: rawIdModal }) => {
15
+ const idModal = rawIdModal || 'modal-object-layer-engine-management';
16
+ const serviceId = 'object-layer-engine-management';
17
+ const gridId = `${serviceId}-grid-${idModal}`;
13
18
  const user = Elements.Data.user.main.model.user;
14
19
  const { role } = user;
15
20
 
@@ -41,9 +46,9 @@ const ObjectLayerManagement = {
41
46
  // Navigate to viewer route first
42
47
  setPath(`${getProxyPath()}object-layer-engine-viewer`);
43
48
  // Then add query param without replacing history
44
- setQueryParams({ cid: data._id }, { replace: true });
49
+ setQueryParams({ id: data._id }, { replace: true });
45
50
  if (s(`.modal-object-layer-engine-viewer`)) {
46
- await ObjectLayerEngineViewer.Reload({ Elements });
51
+ await ObjectLayerEngineViewer.Reload({ Elements, force: true });
47
52
  }
48
53
  s(`.main-btn-object-layer-engine-viewer`).click();
49
54
  });
@@ -87,7 +92,7 @@ const ObjectLayerManagement = {
87
92
  // Navigate to editor route first
88
93
  setPath(`${getProxyPath()}object-layer-engine`);
89
94
  // Then add query param without replacing history
90
- setQueryParams({ cid: data._id }, { replace: true });
95
+ setQueryParams({ id: data._id }, { replace: true });
91
96
  if (s(`.modal-object-layer-engine`)) await ObjectLayerEngineModal.Reload();
92
97
  else s(`.main-btn-object-layer-engine`).click();
93
98
  });
@@ -148,6 +153,123 @@ const ObjectLayerManagement = {
148
153
  }
149
154
  }
150
155
 
156
+ // Custom renderer for delete button (moderator+ only)
157
+ const canDelete = commonModeratorGuard(role);
158
+
159
+ class DeleteButtonRenderer {
160
+ eGui;
161
+
162
+ async init(params) {
163
+ this.eGui = document.createElement('div');
164
+ const { data } = params;
165
+
166
+ if (!data || !data._id || !canDelete) {
167
+ this.eGui.innerHTML = '';
168
+ return;
169
+ }
170
+
171
+ this.eGui.innerHTML = html` ${await BtnIcon.Render({
172
+ label: html`<div class="abs center">
173
+ <i class="fas fa-trash" style="color: #dc3545;"></i>
174
+ </div> `,
175
+ class: `in fll section-mp management-table-btn-mini btn-delete-object-layer-${data._id}`,
176
+ })}`;
177
+
178
+ setTimeout(() => {
179
+ const btn = this.eGui.querySelector(`.btn-delete-object-layer-${data._id}`);
180
+ if (btn)
181
+ btn.onclick = async () => {
182
+ const itemId = data?.data?.item?.id || data._id;
183
+ const confirmResult = await Modal.RenderConfirm({
184
+ id: `delete-object-layer-${data._id}`,
185
+ html: async () => html`
186
+ <div class="in section-mp" style="text-align: center">
187
+ <p>Are you sure you want to permanently delete object layer <strong>"${itemId}"</strong>?</p>
188
+ <p style="color: #dc3545; font-size: 13px; margin-top: 8px;">
189
+ This will remove all associated data including render frames, atlas sprite sheet, IPFS pins, and
190
+ static asset files.
191
+ </p>
192
+ </div>
193
+ `,
194
+ });
195
+ if (confirmResult.status !== 'confirm') return;
196
+ try {
197
+ const result = await ObjectLayerService.delete({ id: data._id });
198
+ if (result.status === 'success') {
199
+ NotificationManager.Push({
200
+ html: `Object layer "${itemId}" deleted successfully`,
201
+ status: 'success',
202
+ });
203
+ if (AgGrid.grids[gridId]) {
204
+ AgGrid.grids[gridId].applyTransaction({ remove: [data] });
205
+ }
206
+ const token = DefaultManagement.Tokens[idModal];
207
+ if (token) {
208
+ const newTotal = token.total - 1;
209
+ const newTotalPages = Math.ceil(newTotal / token.limit);
210
+ if (token.page > newTotalPages && newTotalPages > 0) {
211
+ token.page = newTotalPages;
212
+ }
213
+ await DefaultManagement.loadTable(idModal, { reload: false });
214
+ }
215
+ } else {
216
+ throw new Error(result.message || 'Failed to delete object layer');
217
+ }
218
+ } catch (error) {
219
+ NotificationManager.Push({
220
+ html: `Failed to delete: ${error.message}`,
221
+ status: 'error',
222
+ });
223
+ }
224
+ };
225
+ });
226
+ }
227
+
228
+ getGui() {
229
+ return this.eGui;
230
+ }
231
+
232
+ refresh(params) {
233
+ return true;
234
+ }
235
+ }
236
+
237
+ const createCidRenderer = (cidAccessor) => {
238
+ return class {
239
+ eGui;
240
+
241
+ async init(params) {
242
+ this.eGui = document.createElement('div');
243
+ const { data } = params;
244
+ const cid = cidAccessor(data) || '';
245
+
246
+ if (!cid) {
247
+ this.eGui.innerHTML = html`<span style="color: #666; font-style: italic;">—</span>`;
248
+ return;
249
+ }
250
+
251
+ this.eGui.innerHTML = html`<span
252
+ title="${cid}"
253
+ style="font-family: monospace; font-size: 11px; cursor: default; user-select: all;"
254
+ >${cid}</span
255
+ >`;
256
+ }
257
+
258
+ getGui() {
259
+ return this.eGui;
260
+ }
261
+
262
+ refresh(params) {
263
+ return true;
264
+ }
265
+ };
266
+ };
267
+
268
+ // IPFS CID of object layer data JSON (fast-json-stable-stringify)
269
+ const CidRenderer = createCidRenderer((d) => d?.cid);
270
+ // IPFS CID of the consolidated atlas sprite sheet PNG
271
+ const AtlasCidRenderer = createCidRenderer((d) => d?.data?.atlasSpriteSheetCid || d?.atlasSpriteSheetId?.cid);
272
+
151
273
  let columnDefs = [
152
274
  // {
153
275
  // field: '_id',
@@ -163,6 +285,24 @@ const ObjectLayerManagement = {
163
285
  },
164
286
  { field: 'data.item.type', headerName: 'Item Type', editable: role === 'user' },
165
287
  { field: 'data.item.description', headerName: 'Description', flex: 1, editable: role === 'user' },
288
+ {
289
+ field: 'cid',
290
+ headerName: 'IPFS CID',
291
+ width: 160,
292
+ cellRenderer: CidRenderer,
293
+ editable: false,
294
+ sortable: false,
295
+ filter: false,
296
+ },
297
+ {
298
+ field: 'data.atlasSpriteSheetCid',
299
+ headerName: 'Atlas CID',
300
+ width: 160,
301
+ cellRenderer: AtlasCidRenderer,
302
+ editable: false,
303
+ sortable: false,
304
+ filter: false,
305
+ },
166
306
  {
167
307
  field: 'frame08',
168
308
  headerName: 'Frame 08 Preview',
@@ -190,15 +330,28 @@ const ObjectLayerManagement = {
190
330
  sortable: false,
191
331
  filter: false,
192
332
  },
333
+ ...(canDelete
334
+ ? [
335
+ {
336
+ field: 'delete',
337
+ headerName: '',
338
+ width: 100,
339
+ cellRenderer: DeleteButtonRenderer,
340
+ editable: false,
341
+ sortable: false,
342
+ filter: false,
343
+ },
344
+ ]
345
+ : []),
193
346
  ];
194
347
 
195
348
  return await DefaultManagement.RenderTable({
196
- idModal: idModal ? idModal : 'modal-object-layer-engine-management',
197
- serviceId: 'object-layer-engine-management',
349
+ idModal,
350
+ serviceId,
198
351
  entity: 'object-layer',
199
352
  permissions: {
200
353
  add: commonUserGuard(role),
201
- remove: commonUserGuard(role),
354
+ remove: false,
202
355
  reload: commonUserGuard(role),
203
356
  },
204
357
  customEvent: {
@@ -45,11 +45,6 @@ const UserManagement = {
45
45
  ],
46
46
  defaultColKeyFocus: 'username',
47
47
  ServiceProvider: UserService,
48
- serviceOptions: {
49
- get: {
50
- id: 'all',
51
- },
52
- },
53
48
  });
54
49
  },
55
50
  };
@@ -38,7 +38,7 @@ const UserService = {
38
38
  }),
39
39
  ),
40
40
  get: (options = {}) => {
41
- const { id, page, limit, filterModel, sortModel, sort, asc, order } = options;
41
+ const { id = 'all', page, limit, filterModel, sortModel, sort, asc, order } = options;
42
42
  const url = buildQueryUrl(getApiBaseUrl({ id, endpoint }), {
43
43
  page,
44
44
  limit,
package/src/index.js CHANGED
@@ -12,6 +12,7 @@ import UnderpostDB from './cli/db.js';
12
12
  import UnderpostDeploy from './cli/deploy.js';
13
13
  import UnderpostRootEnv from './cli/env.js';
14
14
  import UnderpostFileStorage from './cli/fs.js';
15
+ import UnderpostIPFS from './cli/ipfs.js';
15
16
  import UnderpostImage from './cli/image.js';
16
17
  import UnderpostLxd from './cli/lxd.js';
17
18
  import UnderpostMonitor from './cli/monitor.js';
@@ -41,7 +42,7 @@ class Underpost {
41
42
  * @type {String}
42
43
  * @memberof Underpost
43
44
  */
44
- static version = 'v2.99.8';
45
+ static version = 'v3.0.2';
45
46
 
46
47
  /**
47
48
  * Required Node.js major version
@@ -143,6 +144,15 @@ class Underpost {
143
144
  static get fs() {
144
145
  return UnderpostFileStorage.API;
145
146
  }
147
+ /**
148
+ * IPFS cli API
149
+ * @static
150
+ * @type {UnderpostIPFS.API}
151
+ * @memberof Underpost
152
+ */
153
+ static get ipfs() {
154
+ return UnderpostIPFS.API;
155
+ }
146
156
  /**
147
157
  * Monitor cli API
148
158
  * @static
@@ -296,6 +306,7 @@ export {
296
306
  UnderpostStatic,
297
307
  UnderpostLxd,
298
308
  UnderpostKickStart,
309
+ UnderpostIPFS,
299
310
  UnderpostMonitor,
300
311
  UnderpostRepository,
301
312
  UnderpostRun,
@@ -20,6 +20,7 @@ import { createPeerServer } from '../../server/peer.js';
20
20
  import { createValkeyConnection } from '../../server/valkey.js';
21
21
  import { applySecurity, authMiddlewareFactory } from '../../server/auth.js';
22
22
  import { ssrMiddlewareFactory } from '../../server/ssr.js';
23
+ import { buildSwaggerUiOptions } from '../../server/client-build-docs.js';
23
24
 
24
25
  import { shellExec } from '../../server/process.js';
25
26
  import { devProxyHostFactory, isDevProxyContext, isTlsDevProxy } from '../../server/conf.js';
@@ -98,7 +99,7 @@ class ExpressService {
98
99
 
99
100
  if (origins && isDevProxyContext())
100
101
  origins.push(devProxyHostFactory({ host, includeHttp: true, tls: isTlsDevProxy() }));
101
- app.set('trust proxy', true);
102
+ app.set('trust proxy', 1);
102
103
 
103
104
  app.use((req, res, next) => {
104
105
  res.on('finish', () => {
@@ -167,8 +168,8 @@ class ExpressService {
167
168
  // Swagger UI setup
168
169
  if (fs.existsSync(swaggerJsonPath)) {
169
170
  const swaggerDoc = JSON.parse(fs.readFileSync(swaggerJsonPath, 'utf8'));
170
- // Reusing swaggerPath defined outside, removing unnecessary redeclaration
171
- app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc));
171
+ const swaggerUiOptions = await buildSwaggerUiOptions();
172
+ app.use(swaggerPath, swaggerUi.serve, swaggerUi.setup(swaggerDoc, swaggerUiOptions));
172
173
  }
173
174
 
174
175
  // Security and CORS
@@ -647,24 +647,24 @@ function applySecurity(app, opts = {}) {
647
647
  }),
648
648
  );
649
649
  logger.info('Cors origin', origin);
650
-
651
- // Rate limiting + slow down
652
- const limiter = rateLimit({
653
- windowMs: rate.windowMs,
654
- max: rate.max,
655
- standardHeaders: true,
656
- legacyHeaders: false,
657
- message: { error: 'Too many requests, please try again later.' },
658
- });
659
- app.use(limiter);
660
-
661
- const speedLimiter = slowDown({
662
- windowMs: slowdown.windowMs,
663
- delayAfter: slowdown.delayAfter,
664
- delayMs: () => slowdown.delayMs,
665
- });
666
- app.use(speedLimiter);
667
-
650
+ if (!process.env.DISABLE_API_RATE_LIMIT) {
651
+ // Rate limiting + slow down
652
+ const limiter = rateLimit({
653
+ windowMs: rate.windowMs,
654
+ max: rate.max,
655
+ standardHeaders: true,
656
+ legacyHeaders: false,
657
+ message: { error: 'Too many requests, please try again later.' },
658
+ });
659
+ app.use(limiter);
660
+
661
+ const speedLimiter = slowDown({
662
+ windowMs: slowdown.windowMs,
663
+ delayAfter: slowdown.delayAfter,
664
+ delayMs: () => slowdown.delayMs,
665
+ });
666
+ app.use(speedLimiter);
667
+ }
668
668
  // Cookie parsing
669
669
  app.use(cookieParser(process.env.JWT_SECRET));
670
670
  }
@@ -7,10 +7,10 @@
7
7
  */
8
8
 
9
9
  import fs from 'fs-extra';
10
- import swaggerAutoGen from 'swagger-autogen';
11
10
  import { shellExec } from './process.js';
12
11
  import { loggerFactory } from './logger.js';
13
12
  import { JSONweb } from './client-formatted.js';
13
+ import { ssrFactory } from './ssr.js';
14
14
 
15
15
  /**
16
16
  * Builds API documentation using Swagger
@@ -63,53 +63,91 @@ const buildApiDocs = async ({
63
63
  components: {
64
64
  schemas: {
65
65
  userRequest: {
66
- username: 'user123',
67
- password: 'Password123',
68
- email: 'user@example.com',
66
+ type: 'object',
67
+ required: ['username', 'password', 'email'],
68
+ properties: {
69
+ username: { type: 'string', example: 'user123' },
70
+ password: { type: 'string', example: 'Password123!' },
71
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
72
+ },
69
73
  },
70
74
  userResponse: {
71
- status: 'success',
72
- data: {
73
- token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjY2YzM3N2Y1N2Y5OWU1OTY5YjgxZG...',
74
- user: {
75
- _id: '66c377f57f99e5969b81de89',
76
- email: 'user@example.com',
77
- emailConfirmed: false,
78
- username: 'user123',
79
- role: 'user',
80
- profileImageId: '66c377f57f99e5969b81de87',
75
+ type: 'object',
76
+ properties: {
77
+ status: { type: 'string', example: 'success' },
78
+ data: {
79
+ type: 'object',
80
+ properties: {
81
+ token: {
82
+ type: 'string',
83
+ example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7Il9pZCI6IjY2YzM3N2Y1N2Y5OWU1OTY5YjgxZG...',
84
+ },
85
+ user: {
86
+ type: 'object',
87
+ properties: {
88
+ _id: { type: 'string', example: '66c377f57f99e5969b81de89' },
89
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
90
+ emailConfirmed: { type: 'boolean', example: false },
91
+ username: { type: 'string', example: 'user123' },
92
+ role: { type: 'string', example: 'user' },
93
+ profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
94
+ },
95
+ },
96
+ },
81
97
  },
82
98
  },
83
99
  },
84
100
  userUpdateResponse: {
85
- status: 'success',
86
- data: {
87
- _id: '66c377f57f99e5969b81de89',
88
- email: 'user@example.com',
89
- emailConfirmed: false,
90
- username: 'user123222',
91
- role: 'user',
92
- profileImageId: '66c377f57f99e5969b81de87',
101
+ type: 'object',
102
+ properties: {
103
+ status: { type: 'string', example: 'success' },
104
+ data: {
105
+ type: 'object',
106
+ properties: {
107
+ _id: { type: 'string', example: '66c377f57f99e5969b81de89' },
108
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
109
+ emailConfirmed: { type: 'boolean', example: false },
110
+ username: { type: 'string', example: 'user123222' },
111
+ role: { type: 'string', example: 'user' },
112
+ profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
113
+ },
114
+ },
93
115
  },
94
116
  },
95
117
  userGetResponse: {
96
- status: 'success',
97
- data: {
98
- _id: '66c377f57f99e5969b81de89',
99
- email: 'user@example.com',
100
- emailConfirmed: false,
101
- username: 'user123222',
102
- role: 'user',
103
- profileImageId: '66c377f57f99e5969b81de87',
118
+ type: 'object',
119
+ properties: {
120
+ status: { type: 'string', example: 'success' },
121
+ data: {
122
+ type: 'object',
123
+ properties: {
124
+ _id: { type: 'string', example: '66c377f57f99e5969b81de89' },
125
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
126
+ emailConfirmed: { type: 'boolean', example: false },
127
+ username: { type: 'string', example: 'user123222' },
128
+ role: { type: 'string', example: 'user' },
129
+ profileImageId: { type: 'string', example: '66c377f57f99e5969b81de87' },
130
+ },
131
+ },
104
132
  },
105
133
  },
106
134
  userLogInRequest: {
107
- email: 'user@example.com',
108
- password: 'Password123',
135
+ type: 'object',
136
+ required: ['email', 'password'],
137
+ properties: {
138
+ email: { type: 'string', format: 'email', example: 'user@example.com' },
139
+ password: { type: 'string', example: 'Password123!' },
140
+ },
109
141
  },
110
142
  userBadRequestResponse: {
111
- status: 'error',
112
- message: 'Bad request. Please check your inputs, and try again',
143
+ type: 'object',
144
+ properties: {
145
+ status: { type: 'string', example: 'error' },
146
+ message: {
147
+ type: 'string',
148
+ example: 'Bad request. Please check your inputs, and try again',
149
+ },
150
+ },
113
151
  },
114
152
  },
115
153
  securitySchemes: {
@@ -121,15 +159,100 @@ const buildApiDocs = async ({
121
159
  },
122
160
  };
123
161
 
162
+ /**
163
+ * swagger-autogen has no requestBody annotation support — it only handles
164
+ * #swagger.parameters, responses, security, etc. We define the requestBody
165
+ * objects here and inject them into the generated JSON as a post-processing step.
166
+ *
167
+ * Each key is an "<method> <path>" pair matching the generated paths object.
168
+ * The value is a valid OAS 3.0 requestBody object.
169
+ */
170
+ const requestBodies = {
171
+ 'post /user': {
172
+ description: 'User registration data',
173
+ required: true,
174
+ content: {
175
+ 'application/json': {
176
+ schema: { $ref: '#/components/schemas/userRequest' },
177
+ },
178
+ },
179
+ },
180
+ 'post /user/auth': {
181
+ description: 'User login credentials',
182
+ required: true,
183
+ content: {
184
+ 'application/json': {
185
+ schema: { $ref: '#/components/schemas/userLogInRequest' },
186
+ },
187
+ },
188
+ },
189
+ 'put /user/{id}': {
190
+ description: 'User fields to update',
191
+ required: true,
192
+ content: {
193
+ 'application/json': {
194
+ schema: { $ref: '#/components/schemas/userRequest' },
195
+ },
196
+ },
197
+ },
198
+ };
199
+
124
200
  logger.warn('build swagger api docs', doc.info);
125
201
 
126
- const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
127
- const routes = [];
128
- for (const api of apis) {
129
- if (['user'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
130
- }
202
+ // swagger-autogen@2.9.2 bug: getProducesTag, getConsumesTag, getResponsesTag missing __¬¬¬__ decode before eval
203
+ fs.writeFileSync(
204
+ `node_modules/swagger-autogen/src/swagger-tags.js`,
205
+ fs
206
+ .readFileSync(`node_modules/swagger-autogen/src/swagger-tags.js`, 'utf8')
207
+ // getProducesTag and getConsumesTag: already decode &quot; but not __¬¬¬__
208
+ .replaceAll(
209
+ `data.replaceAll('\\n', ' ').replaceAll('\u201c', '\u201d')`,
210
+ `data.replaceAll('\\n', ' ').replaceAll('\u201c', '\u201d').replaceAll('__\u00ac\u00ac\u00ac__', '"')`,
211
+ )
212
+ // getResponsesTag: decodes neither &quot; nor __¬¬¬__
213
+ .replaceAll(
214
+ `data.replaceAll('\\n', ' ');`,
215
+ `data.replaceAll('\\n', ' ').replaceAll('__\u00ac\u00ac\u00ac__', '"');`,
216
+ ),
217
+ 'utf8',
218
+ );
219
+ setTimeout(async () => {
220
+ const { default: swaggerAutoGen } = await import('swagger-autogen');
221
+ const outputFile = `./public/${host}${path === '/' ? path : `${path}/`}swagger-output.json`;
222
+ const routes = [];
223
+ for (const api of apis) {
224
+ if (['user'].includes(api)) routes.push(`./src/api/${api}/${api}.router.js`);
225
+ }
226
+
227
+ await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
228
+
229
+ // Post-process: inject requestBody into operations — swagger-autogen silently
230
+ // ignores #swagger.requestBody annotations and has no internal OAS-3 body support.
231
+ if (fs.existsSync(outputFile)) {
232
+ const swaggerJson = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
233
+ let patched = false;
234
+
235
+ for (const [key, requestBody] of Object.entries(requestBodies)) {
236
+ const [method, ...pathParts] = key.split(' ');
237
+ const opPath = pathParts.join(' ');
238
+ if (swaggerJson.paths?.[opPath]?.[method]) {
239
+ swaggerJson.paths[opPath][method].requestBody = requestBody;
240
+ // Remove any stale in:body entry from parameters (OAS 3.0 doesn't allow it)
241
+ if (Array.isArray(swaggerJson.paths[opPath][method].parameters)) {
242
+ swaggerJson.paths[opPath][method].parameters = swaggerJson.paths[opPath][method].parameters.filter(
243
+ (p) => p.in !== 'body',
244
+ );
245
+ }
246
+ patched = true;
247
+ }
248
+ }
131
249
 
132
- await swaggerAutoGen({ openapi: '3.0.0' })(outputFile, routes, doc);
250
+ if (patched) {
251
+ fs.writeFileSync(outputFile, JSON.stringify(swaggerJson, null, 2), 'utf8');
252
+ // logger.warn('swagger post-process: requestBody injected', Object.keys(requestBodies));
253
+ }
254
+ }
255
+ });
133
256
  };
134
257
 
135
258
  /**
@@ -228,4 +351,18 @@ const buildDocs = async ({
228
351
  });
229
352
  };
230
353
 
231
- export { buildDocs };
354
+ /**
355
+ * Builds Swagger UI customization options by rendering the SwaggerDarkMode SSR body component.
356
+ * Returns the customCss and customJsStr strings required by swagger-ui-express to enable
357
+ * a dark/light mode toggle button with a black/gray gradient dark theme.
358
+ * @function buildSwaggerUiOptions
359
+ * @memberof clientBuildDocs
360
+ * @returns {Promise<{customCss: string, customJsStr: string}>} Swagger UI setup options
361
+ */
362
+ const buildSwaggerUiOptions = async () => {
363
+ const swaggerDarkMode = await ssrFactory('./src/client/ssr/body/SwaggerDarkMode.js');
364
+ const { css, js } = swaggerDarkMode();
365
+ return { customCss: css, customJsStr: js };
366
+ };
367
+
368
+ export { buildDocs, buildSwaggerUiOptions };
@@ -1335,7 +1335,7 @@ const buildCliDoc = (program, oldVersion, newVersion) => {
1335
1335
  });
1336
1336
  md = md.replaceAll(oldVersion, newVersion);
1337
1337
  fs.writeFileSync(`./src/client/public/nexodev/docs/references/Command Line Interface.md`, md, 'utf8');
1338
- fs.writeFileSync(`./cli.md`, md, 'utf8');
1338
+ fs.writeFileSync(`./CLI-HELP.md`, md, 'utf8');
1339
1339
  const readme = fs.readFileSync(`./README.md`, 'utf8');
1340
1340
  fs.writeFileSync('./README.md', readme.replaceAll(oldVersion, newVersion), 'utf8');
1341
1341
  };