@nocobase/server 2.3.0-alpha.1 → 3.0.0-alpha.1

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.
@@ -53,17 +53,18 @@ export interface EventQueueOptions {
53
53
  export declare class MemoryEventQueueAdapter implements IEventQueueAdapter {
54
54
  private options;
55
55
  private connected;
56
- private emitter;
57
56
  private reading;
57
+ private scheduledChannels;
58
58
  protected events: Map<string, QueueEventOptions>;
59
59
  protected queues: Map<string, {
60
60
  id: string;
61
61
  content: any;
62
62
  options?: QueueMessageOptions;
63
63
  }[]>;
64
- get processing(): Promise<Promise<void>[][]>;
64
+ get processing(): Promise<void[]>;
65
65
  private get storagePath();
66
66
  listen: (channel: string) => void;
67
+ private scheduleListen;
67
68
  constructor(options: {
68
69
  appName: string;
69
70
  logger: SystemLogger;
@@ -46,7 +46,6 @@ __export(event_queue_exports, {
46
46
  });
47
47
  module.exports = __toCommonJS(event_queue_exports);
48
48
  var import_crypto = require("crypto");
49
- var import_events = require("events");
50
49
  var import_path = __toESM(require("path"));
51
50
  var import_promises = __toESM(require("fs/promises"));
52
51
  var import_utils = require("@nocobase/utils");
@@ -56,15 +55,14 @@ const QUEUE_DEFAULT_ACK_TIMEOUT = 15e3;
56
55
  const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
57
56
  constructor(options) {
58
57
  this.options = options;
59
- this.emitter.setMaxListeners(0);
60
58
  }
61
59
  connected = false;
62
- emitter = new import_events.EventEmitter();
63
60
  reading = /* @__PURE__ */ new Map();
61
+ scheduledChannels = /* @__PURE__ */ new Set();
64
62
  events = /* @__PURE__ */ new Map();
65
63
  queues = /* @__PURE__ */ new Map();
66
64
  get processing() {
67
- const processing = Array.from(this.reading.values());
65
+ const processing = Array.from(this.reading.values()).flat();
68
66
  if (processing.length > 0) {
69
67
  return Promise.all(processing);
70
68
  }
@@ -80,7 +78,6 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
80
78
  const { logger } = this.options;
81
79
  const event = this.events.get(channel);
82
80
  if (!event) {
83
- logger.warn(`memory queue (${channel}) not found, skipping...`);
84
81
  return;
85
82
  }
86
83
  if (!event.idle()) {
@@ -99,10 +96,24 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
99
96
  if (index > -1) {
100
97
  reading.splice(index, 1);
101
98
  }
99
+ this.scheduleListen(channel);
102
100
  });
103
101
  });
104
102
  this.reading.set(channel, reading);
105
103
  }, "listen");
104
+ scheduleListen(channel) {
105
+ if (this.scheduledChannels.has(channel)) {
106
+ return;
107
+ }
108
+ this.scheduledChannels.add(channel);
109
+ setImmediate(() => {
110
+ this.scheduledChannels.delete(channel);
111
+ if (!this.events.has(channel)) {
112
+ return;
113
+ }
114
+ this.listen(channel);
115
+ });
116
+ }
106
117
  isConnected() {
107
118
  return this.connected;
108
119
  }
@@ -181,7 +192,6 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
181
192
  if (!this.queues.has(channel)) {
182
193
  this.queues.set(channel, []);
183
194
  }
184
- this.emitter.on(channel, this.listen);
185
195
  if (this.connected) {
186
196
  this.consume(channel);
187
197
  }
@@ -191,12 +201,12 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
191
201
  return;
192
202
  }
193
203
  this.events.delete(channel);
194
- this.emitter.off(channel, this.listen);
195
204
  }
196
205
  publish(channel, content, options = { timestamp: Date.now() }) {
206
+ const { logger } = this.options;
197
207
  const event = this.events.get(channel);
198
208
  if (!event) {
199
- console.debug(`memory queue (${channel}) not subscribed, skip`);
209
+ logger.debug(`memory queue (${channel}) not subscribed, skip`);
200
210
  return;
201
211
  }
202
212
  if (!this.queues.get(channel)) {
@@ -205,11 +215,8 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
205
215
  const queue = this.queues.get(channel);
206
216
  const message = { id: (0, import_crypto.randomUUID)(), content, options };
207
217
  queue.push(message);
208
- const { logger } = this.options;
209
218
  logger.debug(`memory queue (${channel}) published message`, content);
210
- setImmediate(() => {
211
- this.emitter.emit(channel, channel);
212
- });
219
+ this.scheduleListen(channel);
213
220
  }
214
221
  async consume(channel, once = false) {
215
222
  while (this.connected && this.events.get(channel)) {
@@ -59,6 +59,7 @@ export declare class Gateway extends EventEmitter {
59
59
  private host;
60
60
  private socketPath;
61
61
  private v2IndexTemplateCache;
62
+ private settingsIndexTemplateCache;
62
63
  private terminating;
63
64
  private getOriginalRequestUrl;
64
65
  private proxyRequestToSubApp;
@@ -80,6 +81,17 @@ export declare class Gateway extends EventEmitter {
80
81
  responseErrorWithCode(code: any, res: any, options: any): void;
81
82
  private getV2PublicPath;
82
83
  private getAppPublicPath;
84
+ private getSettingsPublicPath;
85
+ private getPathWithinAppPublicPath;
86
+ private isSettingsRequest;
87
+ private isSettingsIndexRequest;
88
+ private isSettingsAssetsRequest;
89
+ private resolveLegacyV2SettingsRedirect;
90
+ private getPortalRootPublicPath;
91
+ private getPortalAppPublicPath;
92
+ private getPortalMatch;
93
+ private isPortalIndexRequest;
94
+ private getPortalDistRoot;
83
95
  private isV2Request;
84
96
  private isV2IndexRequest;
85
97
  private getV2RuntimeConfig;
@@ -87,6 +99,11 @@ export declare class Gateway extends EventEmitter {
87
99
  private getV2AssetPublicPath;
88
100
  private getV2IndexTemplate;
89
101
  private renderV2IndexHtml;
102
+ private getSettingsRuntimeConfig;
103
+ private getSettingsRuntimeConfigScript;
104
+ private getSettingsAssetPublicPath;
105
+ private getSettingsIndexTemplate;
106
+ private renderSettingsIndexHtml;
90
107
  requestHandler(req: IncomingMessage, res: ServerResponse): Promise<void>;
91
108
  getAppSelectorMiddlewares(): Toposort<AppSelectorMiddleware>;
92
109
  getRequestHandleAppName(req: IncomingMessage | IncomingRequest): Promise<string>;
@@ -114,6 +114,7 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
114
114
  host = "0.0.0.0";
115
115
  socketPath = getSocketPath();
116
116
  v2IndexTemplateCache = null;
117
+ settingsIndexTemplateCache = null;
117
118
  terminating = false;
118
119
  getOriginalRequestUrl(req) {
119
120
  return req.originalUrl || req.url;
@@ -156,6 +157,7 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
156
157
  }, "onTerminate");
157
158
  constructor() {
158
159
  super();
160
+ (0, import_utils3.normalizeModernClientPrefix)(import_node_process.default.env.APP_MODERN_CLIENT_PREFIX);
159
161
  this.reset();
160
162
  import_node_process.default.once("SIGTERM", this.onTerminate);
161
163
  import_node_process.default.once("SIGINT", this.onTerminate);
@@ -188,19 +190,28 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
188
190
  this.selectorMiddlewares = new import_utils.Toposort();
189
191
  this.addAppSelectorMiddleware(
190
192
  async (ctx, next) => {
191
- var _a, _b;
193
+ var _a, _b, _c;
192
194
  const { req } = ctx;
193
195
  const parsedUrl = (0, import_url.parse)(req.url);
194
196
  const appName = (_a = import_qs.default.parse(parsedUrl.query)) == null ? void 0 : _a.__appName;
195
197
  const apiBasePath = normalizeBasePath(import_node_process.default.env.API_BASE_PATH || "/api");
196
198
  const appPathPrefix = `${apiBasePath}/__app/`;
199
+ const appPublicPath = (0, import_utils3.resolvePublicPath)(import_node_process.default.env.APP_PUBLIC_PATH || "/");
200
+ const portalAppsPathPrefix = `${appPublicPath.replace(/\/$/, "")}/${import_utils3.PORTAL_CLIENT_PREFIX}/apps/`;
197
201
  if (req.headers["x-app"]) {
198
202
  ctx.resolvedAppName = req.headers["x-app"];
199
203
  }
200
204
  if (appName) {
201
205
  ctx.resolvedAppName = appName;
202
206
  }
203
- if ((_b = parsedUrl.pathname) == null ? void 0 : _b.startsWith(appPathPrefix)) {
207
+ if ((_b = parsedUrl.pathname) == null ? void 0 : _b.startsWith(portalAppsPathPrefix)) {
208
+ const restPath = parsedUrl.pathname.slice(portalAppsPathPrefix.length);
209
+ const [pathAppName] = restPath.split("/");
210
+ if (pathAppName) {
211
+ ctx.resolvedAppName = (0, import_utils3.normalizePortalAppName)(pathAppName);
212
+ }
213
+ }
214
+ if ((_c = parsedUrl.pathname) == null ? void 0 : _c.startsWith(appPathPrefix)) {
204
215
  const restPath = parsedUrl.pathname.slice(appPathPrefix.length);
205
216
  const [pathAppName, ...segments] = restPath.split("/");
206
217
  if (pathAppName) {
@@ -292,6 +303,144 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
292
303
  getAppPublicPath() {
293
304
  return (0, import_utils3.resolvePublicPath)(import_node_process.default.env.APP_PUBLIC_PATH || "/");
294
305
  }
306
+ getSettingsPublicPath() {
307
+ return (0, import_utils3.resolveSettingsPublicPath)(import_node_process.default.env.APP_PUBLIC_PATH || "/");
308
+ }
309
+ getPathWithinAppPublicPath(pathname) {
310
+ const appPublicPath = this.getAppPublicPath();
311
+ if (appPublicPath === "/") {
312
+ return pathname;
313
+ }
314
+ const appPublicPathWithoutSlash = appPublicPath.slice(0, -1);
315
+ if (pathname === appPublicPathWithoutSlash) {
316
+ return "/";
317
+ }
318
+ if (!pathname.startsWith(appPublicPath)) {
319
+ return null;
320
+ }
321
+ return pathname.slice(appPublicPath.length - 1);
322
+ }
323
+ isSettingsRequest(pathname) {
324
+ const appPath = this.getPathWithinAppPublicPath(pathname);
325
+ if (!appPath) {
326
+ return false;
327
+ }
328
+ return /^\/settings(?:\/|$)/.test(appPath);
329
+ }
330
+ isSettingsIndexRequest(pathname) {
331
+ if (!this.isSettingsRequest(pathname)) {
332
+ return false;
333
+ }
334
+ if (pathname.endsWith("/index.html")) {
335
+ return true;
336
+ }
337
+ return !(0, import_path.extname)(pathname);
338
+ }
339
+ isSettingsAssetsRequest(pathname) {
340
+ const appPath = this.getPathWithinAppPublicPath(pathname);
341
+ return appPath ? /^\/settings\/assets\//.test(appPath) : false;
342
+ }
343
+ resolveLegacyV2SettingsRedirect(pathname) {
344
+ const appPath = this.getPathWithinAppPublicPath(pathname);
345
+ if (!appPath) {
346
+ return null;
347
+ }
348
+ const modernPrefix = (0, import_utils3.normalizeModernClientPrefix)(import_node_process.default.env.APP_MODERN_CLIENT_PREFIX);
349
+ const escapedModernPrefix = modernPrefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
350
+ const match = appPath.match(new RegExp(`^/${escapedModernPrefix}((?:/(?:apps|_app)/[^/]+)?)(/admin(?:/.*)?)$`));
351
+ if (!match) {
352
+ return null;
353
+ }
354
+ const appScope = match[1] || "";
355
+ const legacyPath = match[2];
356
+ const mappings = [
357
+ {
358
+ from: "/admin/settings/mail/oauth2",
359
+ to: "/admin/settings/mail/oauth2"
360
+ },
361
+ {
362
+ from: "/admin/ai/knowledge-base/detail",
363
+ to: "/settings/ai/knowledge-base/detail"
364
+ },
365
+ {
366
+ from: "/admin/workflow/executions",
367
+ to: "/settings/workflow/executions"
368
+ },
369
+ {
370
+ from: "/admin/workflow/workflows",
371
+ to: "/settings/workflow/workflows"
372
+ },
373
+ {
374
+ from: "/admin/settings",
375
+ to: "/settings"
376
+ }
377
+ ];
378
+ for (const { from, to } of mappings) {
379
+ if (legacyPath !== from && !legacyPath.startsWith(`${from}/`)) {
380
+ continue;
381
+ }
382
+ const appPublicPath = this.getAppPublicPath().replace(/\/$/, "");
383
+ const targetPath = from === "/admin/settings/mail/oauth2" ? `${appScope}${to}` : appScope ? `/settings${appScope}${to.replace(/^\/settings(?=\/|$)/, "")}` : to;
384
+ return `${appPublicPath}${targetPath}${legacyPath.slice(from.length)}`;
385
+ }
386
+ return null;
387
+ }
388
+ getPortalRootPublicPath() {
389
+ return `${this.getAppPublicPath().replace(/\/$/, "")}/${import_utils3.PORTAL_CLIENT_PREFIX}/`;
390
+ }
391
+ getPortalAppPublicPath(appName) {
392
+ if (appName === import_utils3.DEFAULT_PORTAL_APP_NAME) {
393
+ return this.getPortalRootPublicPath();
394
+ }
395
+ return `${this.getPortalRootPublicPath()}apps/${(0, import_utils3.normalizePortalAppName)(appName)}/`;
396
+ }
397
+ getPortalMatch(pathname) {
398
+ const portalRootPublicPath = this.getPortalRootPublicPath();
399
+ if (!pathname.startsWith(portalRootPublicPath)) {
400
+ return null;
401
+ }
402
+ let appName = import_utils3.DEFAULT_PORTAL_APP_NAME;
403
+ let publicRoot = portalRootPublicPath;
404
+ let restPath = pathname.slice(portalRootPublicPath.length).replace(/^\/+/, "");
405
+ const [firstSegment, secondSegment, ...remainingSegments] = restPath.split("/");
406
+ if (firstSegment === "apps") {
407
+ if (!secondSegment || !/^[A-Za-z0-9_-]+$/.test(secondSegment)) {
408
+ return null;
409
+ }
410
+ appName = (0, import_utils3.normalizePortalAppName)(secondSegment);
411
+ publicRoot = this.getPortalAppPublicPath(appName);
412
+ if (!pathname.startsWith(publicRoot)) {
413
+ return null;
414
+ }
415
+ restPath = remainingSegments.join("/").replace(/^\/+/, "");
416
+ }
417
+ const [portalName] = restPath.split("/");
418
+ if (!portalName || !/^[A-Za-z0-9_-]+$/.test(portalName)) {
419
+ return null;
420
+ }
421
+ return {
422
+ appName,
423
+ portalName,
424
+ publicPath: `${publicRoot}${portalName}/`
425
+ };
426
+ }
427
+ isPortalIndexRequest(pathname, portalPublicPath) {
428
+ if (pathname === portalPublicPath || pathname === portalPublicPath.slice(0, -1) || pathname === `${portalPublicPath}index.html`) {
429
+ return true;
430
+ }
431
+ return !(0, import_path.extname)(pathname);
432
+ }
433
+ getPortalDistRoot(portalMatch) {
434
+ const scopedRoot = (0, import_utils.storagePathJoin)("portals", portalMatch.appName, portalMatch.portalName, "dist");
435
+ if (portalMatch.appName !== import_utils3.DEFAULT_PORTAL_APP_NAME) {
436
+ return scopedRoot;
437
+ }
438
+ const legacyRoot = (0, import_utils.storagePathJoin)("portals", portalMatch.portalName, "dist");
439
+ if (!import_fs.default.existsSync((0, import_path.resolve)(scopedRoot, "index.html")) && import_fs.default.existsSync((0, import_path.resolve)(legacyRoot, "index.html"))) {
440
+ return legacyRoot;
441
+ }
442
+ return scopedRoot;
443
+ }
295
444
  isV2Request(pathname) {
296
445
  const v2PublicPath = this.getV2PublicPath();
297
446
  return pathname === v2PublicPath.slice(0, -1) || pathname.startsWith(v2PublicPath);
@@ -358,6 +507,57 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
358
507
  const html = (0, import_utils3.rewriteV2AssetPublicPath)(template, this.getV2AssetPublicPath());
359
508
  return (0, import_utils3.injectRuntimeScript)(html, this.getV2RuntimeConfigScript());
360
509
  }
510
+ getSettingsRuntimeConfig() {
511
+ return {
512
+ __nocobase_public_path__: this.getAppPublicPath(),
513
+ __nocobase_modern_client_prefix__: (0, import_utils3.normalizeModernClientPrefix)(import_node_process.default.env.APP_MODERN_CLIENT_PREFIX),
514
+ __webpack_public_path__: import_node_process.default.env.CDN_BASE_URL ? `${import_node_process.default.env.CDN_BASE_URL.replace(/\/+$/, "")}/` : "",
515
+ __nocobase_api_base_url__: import_node_process.default.env.API_BASE_URL || import_node_process.default.env.API_BASE_PATH,
516
+ __nocobase_api_client_storage_prefix__: import_node_process.default.env.API_CLIENT_STORAGE_PREFIX,
517
+ __nocobase_api_client_storage_type__: import_node_process.default.env.API_CLIENT_STORAGE_TYPE,
518
+ __nocobase_api_client_share_token__: import_node_process.default.env.API_CLIENT_SHARE_TOKEN === "true",
519
+ __nocobase_ws_url__: import_node_process.default.env.WEBSOCKET_URL || "",
520
+ __nocobase_ws_path__: import_node_process.default.env.WS_PATH,
521
+ __nocobase_app_dev__: import_node_process.default.env.NOCOBASE_APP_DEV === "true",
522
+ __esm_cdn_base_url__: import_node_process.default.env.ESM_CDN_BASE_URL || "https://esm.sh",
523
+ __esm_cdn_suffix__: import_node_process.default.env.ESM_CDN_SUFFIX || ""
524
+ };
525
+ }
526
+ getSettingsRuntimeConfigScript() {
527
+ const scriptContent = Object.entries(this.getSettingsRuntimeConfig()).map(([key, value]) => `window['${key}'] = ${JSON.stringify(value)};`).join("\n");
528
+ return `<script>${scriptContent}</script>`;
529
+ }
530
+ getSettingsAssetPublicPath() {
531
+ if (import_node_process.default.env.CDN_BASE_URL) {
532
+ return `${import_node_process.default.env.CDN_BASE_URL.replace(/\/+$/, "")}/${import_utils3.SETTINGS_CLIENT_DIST_DIR}/`;
533
+ }
534
+ return this.getSettingsPublicPath();
535
+ }
536
+ getSettingsIndexTemplate() {
537
+ const file = `${import_node_process.default.env.APP_PACKAGE_ROOT}/dist/client/${import_utils3.SETTINGS_CLIENT_DIST_DIR}/index.html`;
538
+ if (!import_fs.default.existsSync(file)) {
539
+ return null;
540
+ }
541
+ const stat = import_fs.default.statSync(file);
542
+ if (this.settingsIndexTemplateCache && this.settingsIndexTemplateCache.file === file && this.settingsIndexTemplateCache.mtimeMs === stat.mtimeMs) {
543
+ return this.settingsIndexTemplateCache.html;
544
+ }
545
+ const html = import_fs.default.readFileSync(file, "utf-8");
546
+ this.settingsIndexTemplateCache = {
547
+ file,
548
+ mtimeMs: stat.mtimeMs,
549
+ html
550
+ };
551
+ return html;
552
+ }
553
+ renderSettingsIndexHtml() {
554
+ const template = this.getSettingsIndexTemplate();
555
+ if (!template) {
556
+ return null;
557
+ }
558
+ const html = (0, import_utils3.rewriteSettingsAssetPublicPath)(template, this.getSettingsAssetPublicPath());
559
+ return (0, import_utils3.injectRuntimeScript)(html, this.getSettingsRuntimeConfigScript());
560
+ }
361
561
  async requestHandler(req, res) {
362
562
  const { pathname, search } = (0, import_url.parse)(req.url);
363
563
  const { PLUGIN_STATICS_PATH } = import_node_process.default.env;
@@ -373,6 +573,13 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
373
573
  res.end();
374
574
  return;
375
575
  }
576
+ const settingsRedirect = this.resolveLegacyV2SettingsRedirect(pathname);
577
+ if (settingsRedirect) {
578
+ res.statusCode = 302;
579
+ res.setHeader("Location", `${settingsRedirect}${search || ""}`);
580
+ res.end();
581
+ return;
582
+ }
376
583
  const supervisor = import_app_supervisor.AppSupervisor.getInstance();
377
584
  let handleApp = "main";
378
585
  try {
@@ -390,14 +597,17 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
390
597
  }
391
598
  }
392
599
  const headers = (0, import_static_file_security.getStorageUploadSecurityHeaders)(`${pathname}${search || ""}`);
393
- for (const [key, value] of Object.entries(headers)) {
394
- res.setHeader(key, value);
395
- }
396
600
  req.url = req.url.substring(APP_PUBLIC_PATH.length + "storage".length);
397
601
  await compress(req, res);
398
602
  return (0, import_serve_handler.default)(req, res, {
399
603
  public: (0, import_utils.resolveStorageRoot)(),
400
- directoryListing: false
604
+ directoryListing: false,
605
+ headers: [
606
+ {
607
+ source: "**/*",
608
+ headers: Object.entries(headers).map(([key, value]) => ({ key, value }))
609
+ }
610
+ ]
401
611
  });
402
612
  }
403
613
  if (pathname.startsWith(APP_PUBLIC_PATH + "dist/")) {
@@ -437,6 +647,70 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
437
647
  }
438
648
  const isFilesRequest = Boolean(getFileAccessRestPath(pathname, APP_PUBLIC_PATH));
439
649
  if (!pathname.startsWith(import_node_process.default.env.API_BASE_PATH) && !isFilesRequest) {
650
+ if (this.isSettingsRequest(pathname)) {
651
+ if (handleApp !== "main") {
652
+ const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
653
+ if (isProxy) {
654
+ return;
655
+ }
656
+ }
657
+ if (this.isSettingsIndexRequest(pathname)) {
658
+ const settingsHtml = this.renderSettingsIndexHtml();
659
+ if (settingsHtml) {
660
+ res.setHeader("Cache-Control", "no-store");
661
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
662
+ res.end(settingsHtml);
663
+ return;
664
+ }
665
+ }
666
+ if (this.isSettingsAssetsRequest(pathname)) {
667
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
668
+ }
669
+ req.url = req.url.substring(APP_PUBLIC_PATH.length - 1);
670
+ await compress(req, res);
671
+ return (0, import_serve_handler.default)(req, res, {
672
+ public: `${import_node_process.default.env.APP_PACKAGE_ROOT}/dist/client`
673
+ });
674
+ }
675
+ const portalMatch = this.getPortalMatch(pathname);
676
+ if (portalMatch) {
677
+ if (handleApp !== "main" && handleApp !== portalMatch.appName) {
678
+ const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
679
+ if (isProxy) {
680
+ return;
681
+ }
682
+ }
683
+ if (!pathname.startsWith(portalMatch.publicPath)) {
684
+ res.statusCode = 302;
685
+ res.setHeader("Location", `${portalMatch.publicPath}${search || ""}`);
686
+ res.end();
687
+ return;
688
+ }
689
+ const portalDistRoot = this.getPortalDistRoot(portalMatch);
690
+ const portalIndex = (0, import_path.resolve)(portalDistRoot, "index.html");
691
+ if (!import_fs.default.existsSync(portalIndex)) {
692
+ res.statusCode = 404;
693
+ res.end();
694
+ return;
695
+ }
696
+ if (this.isPortalIndexRequest(pathname, portalMatch.publicPath)) {
697
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
698
+ res.end(import_fs.default.readFileSync(portalIndex, "utf-8"));
699
+ return;
700
+ }
701
+ req.url = req.url.substring(portalMatch.publicPath.length - 1);
702
+ await compress(req, res);
703
+ return (0, import_serve_handler.default)(req, res, {
704
+ public: portalDistRoot,
705
+ directoryListing: false
706
+ });
707
+ }
708
+ const portalRootPublicPath = this.getPortalRootPublicPath();
709
+ if (pathname === portalRootPublicPath.slice(0, -1) || pathname.startsWith(portalRootPublicPath)) {
710
+ res.statusCode = 404;
711
+ res.end();
712
+ return;
713
+ }
440
714
  if (this.isV2Request(pathname)) {
441
715
  if (handleApp !== "main") {
442
716
  const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
@@ -42,7 +42,18 @@ __export(static_file_security_exports, {
42
42
  });
43
43
  module.exports = __toCommonJS(static_file_security_exports);
44
44
  var import_node_path = __toESM(require("node:path"));
45
- const ACTIVE_CONTENT_EXTENSIONS = /* @__PURE__ */ new Set([".htm", ".html", ".pdf", ".svg", ".svgz", ".xhtml"]);
45
+ const ACTIVE_CONTENT_EXTENSIONS = /* @__PURE__ */ new Set([
46
+ ".htm",
47
+ ".html",
48
+ ".pdf",
49
+ ".svg",
50
+ ".svgz",
51
+ ".xht",
52
+ ".xhtml",
53
+ ".xml",
54
+ ".xsl",
55
+ ".xslt"
56
+ ]);
46
57
  function stripQueryAndHash(pathname = "") {
47
58
  return pathname.split("?")[0].split("#")[0];
48
59
  }
@@ -63,6 +74,7 @@ function hasActiveContentExtension(pathname = "") {
63
74
  __name(hasActiveContentExtension, "hasActiveContentExtension");
64
75
  function getStorageUploadSecurityHeaders(pathname = "") {
65
76
  const headers = {
77
+ "Content-Security-Policy": "sandbox",
66
78
  "X-Content-Type-Options": "nosniff"
67
79
  };
68
80
  if (hasActiveContentExtension(pathname) || shouldDownload(pathname)) {
@@ -10,10 +10,19 @@
10
10
  import { IncomingMessage } from 'http';
11
11
  import { IncomingRequest } from '.';
12
12
  export declare const MODERN_CLIENT_DIST_DIR = "v";
13
+ export declare const SETTINGS_CLIENT_DIST_DIR = "settings";
14
+ export declare const PORTAL_CLIENT_PREFIX = "x";
15
+ export declare const DEFAULT_PORTAL_APP_NAME = "main";
16
+ export declare const DEFAULT_PORTAL_NAME = "admin";
13
17
  export declare function resolvePublicPath(appPublicPath?: string): string;
14
18
  export declare function normalizeModernClientPrefix(value?: string): string;
15
19
  export declare function resolveV2PublicPath(appPublicPath?: string): string;
20
+ export declare function resolveSettingsPublicPath(appPublicPath?: string): string;
21
+ export declare function normalizePortalName(value?: string): string;
22
+ export declare function normalizePortalAppName(value?: string): string;
23
+ export declare function resolvePortalPublicPath(portalName: string, appPublicPath?: string): string;
16
24
  export declare function rewriteV2AssetPublicPath(html: string, assetPublicPath: string): string;
25
+ export declare function rewriteSettingsAssetPublicPath(html: string, assetPublicPath: string): string;
17
26
  export declare function injectRuntimeScript(html: string, runtimeScript: string): string;
18
27
  export declare function getHost(req: IncomingMessage | IncomingRequest): any;
19
28
  export declare function getHostname(req: IncomingMessage | IncomingRequest): any;
@@ -27,17 +27,30 @@ var __copyProps = (to, from, except, desc) => {
27
27
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
28
28
  var utils_exports = {};
29
29
  __export(utils_exports, {
30
+ DEFAULT_PORTAL_APP_NAME: () => DEFAULT_PORTAL_APP_NAME,
31
+ DEFAULT_PORTAL_NAME: () => DEFAULT_PORTAL_NAME,
30
32
  MODERN_CLIENT_DIST_DIR: () => MODERN_CLIENT_DIST_DIR,
33
+ PORTAL_CLIENT_PREFIX: () => PORTAL_CLIENT_PREFIX,
34
+ SETTINGS_CLIENT_DIST_DIR: () => SETTINGS_CLIENT_DIST_DIR,
31
35
  getHost: () => getHost,
32
36
  getHostname: () => getHostname,
33
37
  injectRuntimeScript: () => injectRuntimeScript,
34
38
  normalizeModernClientPrefix: () => normalizeModernClientPrefix,
39
+ normalizePortalAppName: () => normalizePortalAppName,
40
+ normalizePortalName: () => normalizePortalName,
41
+ resolvePortalPublicPath: () => resolvePortalPublicPath,
35
42
  resolvePublicPath: () => resolvePublicPath,
43
+ resolveSettingsPublicPath: () => resolveSettingsPublicPath,
36
44
  resolveV2PublicPath: () => resolveV2PublicPath,
45
+ rewriteSettingsAssetPublicPath: () => rewriteSettingsAssetPublicPath,
37
46
  rewriteV2AssetPublicPath: () => rewriteV2AssetPublicPath
38
47
  });
39
48
  module.exports = __toCommonJS(utils_exports);
40
49
  const MODERN_CLIENT_DIST_DIR = "v";
50
+ const SETTINGS_CLIENT_DIST_DIR = "settings";
51
+ const PORTAL_CLIENT_PREFIX = "x";
52
+ const DEFAULT_PORTAL_APP_NAME = "main";
53
+ const DEFAULT_PORTAL_NAME = "admin";
41
54
  function resolvePublicPath(appPublicPath = "/") {
42
55
  const normalized = String(appPublicPath || "/").trim() || "/";
43
56
  const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
@@ -46,7 +59,11 @@ function resolvePublicPath(appPublicPath = "/") {
46
59
  __name(resolvePublicPath, "resolvePublicPath");
47
60
  function normalizeModernClientPrefix(value) {
48
61
  const segment = String(value || "").trim().replace(/^\/+|\/+$/g, "");
49
- return segment || MODERN_CLIENT_DIST_DIR;
62
+ const normalized = segment || MODERN_CLIENT_DIST_DIR;
63
+ if (normalized === SETTINGS_CLIENT_DIST_DIR) {
64
+ throw new Error('APP_MODERN_CLIENT_PREFIX "settings" is reserved for the standalone Settings application.');
65
+ }
66
+ return normalized;
50
67
  }
51
68
  __name(normalizeModernClientPrefix, "normalizeModernClientPrefix");
52
69
  function resolveV2PublicPath(appPublicPath = "/") {
@@ -55,6 +72,26 @@ function resolveV2PublicPath(appPublicPath = "/") {
55
72
  return `${publicPath.replace(/\/$/, "")}/${prefix}/`;
56
73
  }
57
74
  __name(resolveV2PublicPath, "resolveV2PublicPath");
75
+ function resolveSettingsPublicPath(appPublicPath = "/") {
76
+ const publicPath = resolvePublicPath(appPublicPath);
77
+ return `${publicPath.replace(/\/$/, "")}/${SETTINGS_CLIENT_DIST_DIR}/`;
78
+ }
79
+ __name(resolveSettingsPublicPath, "resolveSettingsPublicPath");
80
+ function normalizePortalName(value) {
81
+ const segment = String(value || "").trim().replace(/^\/+|\/+$/g, "");
82
+ return segment || DEFAULT_PORTAL_NAME;
83
+ }
84
+ __name(normalizePortalName, "normalizePortalName");
85
+ function normalizePortalAppName(value) {
86
+ const segment = String(value || "").trim().replace(/^\/+|\/+$/g, "");
87
+ return segment || DEFAULT_PORTAL_APP_NAME;
88
+ }
89
+ __name(normalizePortalAppName, "normalizePortalAppName");
90
+ function resolvePortalPublicPath(portalName, appPublicPath = "/") {
91
+ const publicPath = resolvePublicPath(appPublicPath);
92
+ return `${publicPath.replace(/\/$/, "")}/${PORTAL_CLIENT_PREFIX}/${normalizePortalName(portalName)}/`;
93
+ }
94
+ __name(resolvePortalPublicPath, "resolvePortalPublicPath");
58
95
  function ensureTrailingSlash(value) {
59
96
  return value.endsWith("/") ? value : `${value}/`;
60
97
  }
@@ -69,6 +106,16 @@ function rewriteV2AssetPublicPath(html, assetPublicPath) {
69
106
  return html.replace(sentinelPattern, `$1${normalizedAssetPublicPath}`);
70
107
  }
71
108
  __name(rewriteV2AssetPublicPath, "rewriteV2AssetPublicPath");
109
+ function rewriteSettingsAssetPublicPath(html, assetPublicPath) {
110
+ const normalizedAssetPublicPath = ensureTrailingSlash(assetPublicPath);
111
+ const sentinel = `/${SETTINGS_CLIENT_DIST_DIR}/`;
112
+ if (normalizedAssetPublicPath === sentinel) {
113
+ return html;
114
+ }
115
+ const sentinelPattern = new RegExp(`((?:src|href)=["'])${sentinel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g");
116
+ return html.replace(sentinelPattern, `$1${normalizedAssetPublicPath}`);
117
+ }
118
+ __name(rewriteSettingsAssetPublicPath, "rewriteSettingsAssetPublicPath");
72
119
  function injectRuntimeScript(html, runtimeScript) {
73
120
  const browserCheckerScriptMatch = html.match(/<script\b[^>]*browser-checker\.js[^>]*><\/script>/i);
74
121
  if (browserCheckerScriptMatch == null ? void 0 : browserCheckerScriptMatch[0]) {
@@ -117,12 +164,21 @@ function getHostname(req) {
117
164
  __name(getHostname, "getHostname");
118
165
  // Annotate the CommonJS export names for ESM import in node:
119
166
  0 && (module.exports = {
167
+ DEFAULT_PORTAL_APP_NAME,
168
+ DEFAULT_PORTAL_NAME,
120
169
  MODERN_CLIENT_DIST_DIR,
170
+ PORTAL_CLIENT_PREFIX,
171
+ SETTINGS_CLIENT_DIST_DIR,
121
172
  getHost,
122
173
  getHostname,
123
174
  injectRuntimeScript,
124
175
  normalizeModernClientPrefix,
176
+ normalizePortalAppName,
177
+ normalizePortalName,
178
+ resolvePortalPublicPath,
125
179
  resolvePublicPath,
180
+ resolveSettingsPublicPath,
126
181
  resolveV2PublicPath,
182
+ rewriteSettingsAssetPublicPath,
127
183
  rewriteV2AssetPublicPath
128
184
  });
package/lib/helper.d.ts CHANGED
@@ -11,6 +11,7 @@ import { Command } from 'commander';
11
11
  import Application, { ApplicationOptions } from './application';
12
12
  export declare function createI18n(options: ApplicationOptions): import("i18next").i18n;
13
13
  export declare function createResourcer(options: ApplicationOptions): Resourcer;
14
+ export declare function resolveCorsOrigin(ctx: any): any;
14
15
  export declare function registerMiddlewares(app: Application, options: ApplicationOptions): void;
15
16
  export declare const createAppProxy: (app: Application) => Application<import("./application").DefaultState, import("./application").DefaultContext>;
16
17
  export declare const getCommandFullName: (command: Command) => string;
package/lib/helper.js CHANGED
@@ -45,6 +45,7 @@ __export(helper_exports, {
45
45
  getBodyLimit: () => getBodyLimit,
46
46
  getCommandFullName: () => getCommandFullName,
47
47
  registerMiddlewares: () => registerMiddlewares,
48
+ resolveCorsOrigin: () => resolveCorsOrigin,
48
49
  tsxRerunning: () => tsxRerunning
49
50
  });
50
51
  module.exports = __toCommonJS(helper_exports);
@@ -78,26 +79,23 @@ function createResourcer(options) {
78
79
  __name(createResourcer, "createResourcer");
79
80
  function isWhitelistedCorsOrigin(ctx) {
80
81
  const origin = ctx.get("origin");
81
- const whitelist = (0, import_utils.getCorsWhitelist)();
82
82
  if (!origin) {
83
83
  return false;
84
84
  }
85
- if (!whitelist) {
86
- return (0, import_utils.isTrustedOrigin)(ctx, origin);
87
- }
88
- return whitelist.has(origin);
85
+ return (0, import_utils.isTrustedOrigin)(ctx, origin);
89
86
  }
90
87
  __name(isWhitelistedCorsOrigin, "isWhitelistedCorsOrigin");
91
88
  function resolveCorsOrigin(ctx) {
92
89
  const origin = ctx.get("origin");
93
90
  const disallowNoOrigin = process.env.CORS_DISALLOW_NO_ORIGIN === "true";
91
+ const whitelist = (0, import_utils.getCorsWhitelist)();
94
92
  if (!origin && disallowNoOrigin) {
95
93
  return false;
96
94
  }
97
95
  if (isWhitelistedCorsOrigin(ctx)) {
98
96
  return origin;
99
97
  }
100
- return (0, import_utils.getCorsWhitelist)() ? false : origin;
98
+ return whitelist ? false : origin;
101
99
  }
102
100
  __name(resolveCorsOrigin, "resolveCorsOrigin");
103
101
  function registerMiddlewares(app, options) {
@@ -325,5 +323,6 @@ __name(createContextVariablesScope, "createContextVariablesScope");
325
323
  getBodyLimit,
326
324
  getCommandFullName,
327
325
  registerMiddlewares,
326
+ resolveCorsOrigin,
328
327
  tsxRerunning
329
328
  });
package/lib/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export * from './gateway/ws-server';
13
13
  export { Application as default } from './application';
14
14
  export * from './audit-manager';
15
15
  export * from './gateway';
16
+ export * from './gateway/static-file-security';
16
17
  export * as middlewares from './middlewares';
17
18
  export * from './migration';
18
19
  export * from './plugin';
package/lib/index.js CHANGED
@@ -55,6 +55,7 @@ __reExport(src_exports, require("./gateway/ws-server"), module.exports);
55
55
  var import_application = require("./application");
56
56
  __reExport(src_exports, require("./audit-manager"), module.exports);
57
57
  __reExport(src_exports, require("./gateway"), module.exports);
58
+ __reExport(src_exports, require("./gateway/static-file-security"), module.exports);
58
59
  var middlewares = __toESM(require("./middlewares"));
59
60
  __reExport(src_exports, require("./migration"), module.exports);
60
61
  __reExport(src_exports, require("./plugin"), module.exports);
@@ -85,6 +86,7 @@ var import_helper = require("./helper");
85
86
  ...require("./gateway/ws-server"),
86
87
  ...require("./audit-manager"),
87
88
  ...require("./gateway"),
89
+ ...require("./gateway/static-file-security"),
88
90
  ...require("./migration"),
89
91
  ...require("./plugin"),
90
92
  ...require("./plugin-manager"),
@@ -12,7 +12,23 @@ declare const _default: {
12
12
  readonly tags: readonly ["app"];
13
13
  readonly summary: "Get the current application language";
14
14
  readonly description: "Return the current locale used by the server.";
15
- readonly parameters: readonly [];
15
+ readonly parameters: readonly [{
16
+ readonly name: "locale";
17
+ readonly in: "query";
18
+ readonly required: false;
19
+ readonly schema: {
20
+ readonly type: "string";
21
+ };
22
+ readonly description: "Requested application locale. The server validates it against enabled languages.";
23
+ }, {
24
+ readonly name: "ns";
25
+ readonly in: "query";
26
+ readonly required: false;
27
+ readonly schema: {
28
+ readonly type: "string";
29
+ };
30
+ readonly description: "Comma-separated resource namespaces to return. Omit it to preserve the full legacy payload.";
31
+ }];
16
32
  readonly responses: {
17
33
  readonly 200: {
18
34
  readonly description: "OK";
@@ -35,7 +35,22 @@ var app_default = {
35
35
  tags: ["app"],
36
36
  summary: "Get the current application language",
37
37
  description: "Return the current locale used by the server.",
38
- parameters: [],
38
+ parameters: [
39
+ {
40
+ name: "locale",
41
+ in: "query",
42
+ required: false,
43
+ schema: { type: "string" },
44
+ description: "Requested application locale. The server validates it against enabled languages."
45
+ },
46
+ {
47
+ name: "ns",
48
+ in: "query",
49
+ required: false,
50
+ schema: { type: "string" },
51
+ description: "Comma-separated resource namespaces to return. Omit it to preserve the full legacy payload."
52
+ }
53
+ ],
39
54
  responses: {
40
55
  200: {
41
56
  description: "OK",
@@ -694,7 +694,23 @@ declare const _default: {
694
694
  readonly tags: readonly ["app"];
695
695
  readonly summary: "Get the current application language";
696
696
  readonly description: "Return the current locale used by the server.";
697
- readonly parameters: readonly [];
697
+ readonly parameters: readonly [{
698
+ readonly name: "locale";
699
+ readonly in: "query";
700
+ readonly required: false;
701
+ readonly schema: {
702
+ readonly type: "string";
703
+ };
704
+ readonly description: "Requested application locale. The server validates it against enabled languages.";
705
+ }, {
706
+ readonly name: "ns";
707
+ readonly in: "query";
708
+ readonly required: false;
709
+ readonly schema: {
710
+ readonly type: "string";
711
+ };
712
+ readonly description: "Comma-separated resource namespaces to return. Omit it to preserve the full legacy payload.";
713
+ }];
698
714
  readonly responses: {
699
715
  readonly 200: {
700
716
  readonly description: "OK";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/server",
3
- "version": "2.3.0-alpha.1",
3
+ "version": "3.0.0-alpha.1",
4
4
  "main": "lib/index.js",
5
5
  "types": "./lib/index.d.ts",
6
6
  "license": "Apache-2.0",
@@ -10,21 +10,21 @@
10
10
  "@koa/cors": "^5.0.0",
11
11
  "@koa/multer": "^3.1.0",
12
12
  "@koa/router": "^13.1.0",
13
- "@nocobase/acl": "2.3.0-alpha.1",
14
- "@nocobase/actions": "2.3.0-alpha.1",
15
- "@nocobase/ai": "2.3.0-alpha.1",
16
- "@nocobase/auth": "2.3.0-alpha.1",
17
- "@nocobase/cache": "2.3.0-alpha.1",
18
- "@nocobase/data-source-manager": "2.3.0-alpha.1",
19
- "@nocobase/database": "2.3.0-alpha.1",
20
- "@nocobase/evaluators": "2.3.0-alpha.1",
21
- "@nocobase/lock-manager": "2.3.0-alpha.1",
22
- "@nocobase/logger": "2.3.0-alpha.1",
23
- "@nocobase/resourcer": "2.3.0-alpha.1",
24
- "@nocobase/sdk": "2.3.0-alpha.1",
25
- "@nocobase/snowflake-id": "2.3.0-alpha.1",
26
- "@nocobase/telemetry": "2.3.0-alpha.1",
27
- "@nocobase/utils": "2.3.0-alpha.1",
13
+ "@nocobase/acl": "3.0.0-alpha.1",
14
+ "@nocobase/actions": "3.0.0-alpha.1",
15
+ "@nocobase/ai": "3.0.0-alpha.1",
16
+ "@nocobase/auth": "3.0.0-alpha.1",
17
+ "@nocobase/cache": "3.0.0-alpha.1",
18
+ "@nocobase/data-source-manager": "3.0.0-alpha.1",
19
+ "@nocobase/database": "3.0.0-alpha.1",
20
+ "@nocobase/evaluators": "3.0.0-alpha.1",
21
+ "@nocobase/lock-manager": "3.0.0-alpha.1",
22
+ "@nocobase/logger": "3.0.0-alpha.1",
23
+ "@nocobase/resourcer": "3.0.0-alpha.1",
24
+ "@nocobase/sdk": "3.0.0-alpha.1",
25
+ "@nocobase/snowflake-id": "3.0.0-alpha.1",
26
+ "@nocobase/telemetry": "3.0.0-alpha.1",
27
+ "@nocobase/utils": "3.0.0-alpha.1",
28
28
  "@types/decompress": "4.2.7",
29
29
  "@types/ini": "^1.3.31",
30
30
  "@types/koa-send": "^4.1.3",
@@ -61,5 +61,5 @@
61
61
  "@types/serve-handler": "^6.1.1",
62
62
  "@types/ws": "^8.5.5"
63
63
  },
64
- "gitHead": "2377df8ceb12549149017f7f14a61207bf6e49a2"
64
+ "gitHead": "22d8be8ece179cfa5c8a4ebb2c896c92bd418e03"
65
65
  }