@nocobase/server 2.3.0-alpha.1 → 2.3.0-beta.10

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.
@@ -34,7 +34,7 @@ const availableActions = {
34
34
  displayName: '{{t("Add new")}}',
35
35
  type: "new-data",
36
36
  onNewRecord: true,
37
- aliases: ["create", "firstOrCreate", "updateOrCreate"],
37
+ aliases: ["create"],
38
38
  allowConfigureFields: true
39
39
  },
40
40
  // import: {
@@ -53,6 +53,7 @@ export declare class AppSupervisor extends EventEmitter implements AsyncEmitter
53
53
  private appSsoIssuer?;
54
54
  appOptionsFactory: AppOptionsFactory;
55
55
  private environmentHeartbeatInterval;
56
+ private environmentHeartbeatTimeout;
56
57
  private environmentHeartbeatTimer;
57
58
  private constructor();
58
59
  private resolveDiscoveryAdapterName;
@@ -163,11 +164,13 @@ export declare class AppSupervisor extends EventEmitter implements AsyncEmitter
163
164
  proxyWeb(appName: string, req: IncomingMessage, res: ServerResponse): Promise<boolean>;
164
165
  proxyWs(req: IncomingMessage, socket: any, head: Buffer): Promise<boolean>;
165
166
  registerEnvironment(mainApp: Application): Promise<void>;
167
+ private getEnvironmentInfo;
166
168
  unregisterEnvironment(): Promise<void>;
167
169
  private normalizeEnvInfo;
168
170
  listEnvironments(): Promise<EnvironmentInfo[]>;
169
171
  getEnvironment(environmentName: string): Promise<EnvironmentInfo>;
170
- heartbeatEnvironment(): Promise<void>;
172
+ heartbeatEnvironment(mainApp: Application): Promise<void>;
173
+ private stopEnvironmentHeartbeat;
171
174
  dispatchCommand(command: ProcessCommand): Promise<void>;
172
175
  registerCommandHandler(mainApp: Application): void;
173
176
  sendSyncMessage(mainApp: Application, message: {
@@ -74,6 +74,7 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
74
74
  appSsoIssuer;
75
75
  appOptionsFactory = import_app_options_factory.appOptionsFactory;
76
76
  environmentHeartbeatInterval = 2 * 60 * 1e3;
77
+ environmentHeartbeatTimeout = 5 * 60 * 1e3;
77
78
  environmentHeartbeatTimer = null;
78
79
  constructor() {
79
80
  super();
@@ -282,13 +283,11 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
282
283
  }
283
284
  async reset() {
284
285
  var _a, _b, _c, _d;
286
+ this.stopEnvironmentHeartbeat();
285
287
  await this.processAdapter.removeAllApps();
286
288
  await ((_b = (_a = this.discoveryAdapter).dispose) == null ? void 0 : _b.call(_a));
287
289
  await ((_d = (_c = this.commandAdapter) == null ? void 0 : _c.dispose) == null ? void 0 : _d.call(_c));
288
290
  this.appManifests.clear();
289
- if (this.environmentHeartbeatTimer) {
290
- this.environmentHeartbeatTimer = null;
291
- }
292
291
  this.removeAllListeners();
293
292
  this.logger.close();
294
293
  }
@@ -389,6 +388,9 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
389
388
  if (!appOptions) {
390
389
  return;
391
390
  }
391
+ if (appModel.environments && !appModel.environments.includes(this.environmentName)) {
392
+ return;
393
+ }
392
394
  const newApp = this.registerApp({ appModel, mainApp: app2 });
393
395
  newApp.runCommand("start", "--quickstart");
394
396
  }
@@ -601,24 +603,25 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
601
603
  if (!this.environmentName || typeof this.discoveryAdapter.registerEnvironment !== "function") {
602
604
  return;
603
605
  }
604
- const registered = await this.discoveryAdapter.registerEnvironment({
606
+ const registered = await this.discoveryAdapter.registerEnvironment(this.getEnvironmentInfo(mainApp));
607
+ if (registered) {
608
+ this.heartbeatEnvironment(mainApp);
609
+ }
610
+ }
611
+ getEnvironmentInfo(mainApp) {
612
+ return {
605
613
  name: this.environmentName,
606
614
  url: this.environmentUrl || "",
607
615
  proxyUrl: this.environmentProxyUrl || this.environmentUrl || "",
608
616
  appVersion: mainApp.getPackageVersion(),
609
617
  lastHeartbeatAt: Date.now()
610
- });
611
- if (registered) {
612
- this.heartbeatEnvironment();
613
- }
618
+ };
614
619
  }
615
620
  async unregisterEnvironment() {
621
+ this.stopEnvironmentHeartbeat();
616
622
  if (this.environmentName && typeof this.discoveryAdapter.unregisterEnvironment === "function") {
617
623
  await this.discoveryAdapter.unregisterEnvironment();
618
624
  }
619
- if (this.environmentHeartbeatTimer) {
620
- this.environmentHeartbeatTimer = null;
621
- }
622
625
  }
623
626
  normalizeEnvInfo(environment) {
624
627
  if (typeof environment.available === "boolean") {
@@ -631,7 +634,7 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
631
634
  available: false
632
635
  };
633
636
  }
634
- const available = Date.now() - lastHeartbeatAt <= this.environmentHeartbeatInterval;
637
+ const available = Date.now() - lastHeartbeatAt <= this.environmentHeartbeatTimeout;
635
638
  return {
636
639
  ...environment,
637
640
  available
@@ -654,17 +657,26 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
654
657
  }
655
658
  return this.normalizeEnvInfo(environment);
656
659
  }
657
- async heartbeatEnvironment() {
660
+ async heartbeatEnvironment(mainApp) {
658
661
  if (typeof this.discoveryAdapter.heartbeatEnvironment !== "function") {
659
662
  return;
660
663
  }
661
664
  if (this.environmentHeartbeatTimer) {
662
665
  return;
663
666
  }
664
- this.environmentHeartbeatTimer = setInterval(
665
- () => this.discoveryAdapter.heartbeatEnvironment(),
666
- this.environmentHeartbeatInterval
667
- );
667
+ this.environmentHeartbeatTimer = setInterval(async () => {
668
+ try {
669
+ await this.discoveryAdapter.heartbeatEnvironment(this.getEnvironmentInfo(mainApp));
670
+ } catch (error) {
671
+ this.logger.error(error instanceof Error ? error.message : String(error), { method: "heartbeatEnvironment" });
672
+ }
673
+ }, this.environmentHeartbeatInterval);
674
+ }
675
+ stopEnvironmentHeartbeat() {
676
+ if (this.environmentHeartbeatTimer) {
677
+ clearInterval(this.environmentHeartbeatTimer);
678
+ this.environmentHeartbeatTimer = null;
679
+ }
668
680
  }
669
681
  async dispatchCommand(command) {
670
682
  var _a;
@@ -132,7 +132,7 @@ export interface AppDiscoveryAdapter {
132
132
  unregisterEnvironment?(): Promise<void>;
133
133
  listEnvironments?(): Promise<EnvironmentInfo[]>;
134
134
  getEnvironment?(environmentName: string): Promise<EnvironmentInfo | null>;
135
- heartbeatEnvironment?(): Promise<void>;
135
+ heartbeatEnvironment?(environment: EnvironmentInfo): Promise<void>;
136
136
  getBootstrapLock?(appName: string): Promise<BootstrapLock | null> | BootstrapLock | null;
137
137
  proxyWeb?(appName: string, req: IncomingMessage, res: ServerResponse): Promise<boolean>;
138
138
  proxyWs?(req: IncomingMessage, socket: any, head: Buffer): Promise<boolean>;
@@ -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,14 +78,15 @@ 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()) {
87
84
  return;
88
85
  }
89
86
  const reading = this.reading.get(channel) || [];
90
- const count = (event.concurrency || QUEUE_DEFAULT_CONCURRENCY) - reading.length;
87
+ const queue = this.queues.get(channel);
88
+ const concurrency = event.concurrency ?? QUEUE_DEFAULT_CONCURRENCY;
89
+ const count = concurrency <= 0 ? (queue == null ? void 0 : queue.length) || 0 : concurrency - reading.length;
91
90
  if (count <= 0) {
92
91
  return;
93
92
  }
@@ -99,10 +98,24 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
99
98
  if (index > -1) {
100
99
  reading.splice(index, 1);
101
100
  }
101
+ this.scheduleListen(channel);
102
102
  });
103
103
  });
104
104
  this.reading.set(channel, reading);
105
105
  }, "listen");
106
+ scheduleListen(channel) {
107
+ if (this.scheduledChannels.has(channel)) {
108
+ return;
109
+ }
110
+ this.scheduledChannels.add(channel);
111
+ setImmediate(() => {
112
+ this.scheduledChannels.delete(channel);
113
+ if (!this.events.has(channel)) {
114
+ return;
115
+ }
116
+ this.listen(channel);
117
+ });
118
+ }
106
119
  isConnected() {
107
120
  return this.connected;
108
121
  }
@@ -181,7 +194,6 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
181
194
  if (!this.queues.has(channel)) {
182
195
  this.queues.set(channel, []);
183
196
  }
184
- this.emitter.on(channel, this.listen);
185
197
  if (this.connected) {
186
198
  this.consume(channel);
187
199
  }
@@ -191,12 +203,12 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
191
203
  return;
192
204
  }
193
205
  this.events.delete(channel);
194
- this.emitter.off(channel, this.listen);
195
206
  }
196
207
  publish(channel, content, options = { timestamp: Date.now() }) {
208
+ const { logger } = this.options;
197
209
  const event = this.events.get(channel);
198
210
  if (!event) {
199
- console.debug(`memory queue (${channel}) not subscribed, skip`);
211
+ logger.debug(`memory queue (${channel}) not subscribed, skip`);
200
212
  return;
201
213
  }
202
214
  if (!this.queues.get(channel)) {
@@ -205,11 +217,8 @@ const _MemoryEventQueueAdapter = class _MemoryEventQueueAdapter {
205
217
  const queue = this.queues.get(channel);
206
218
  const message = { id: (0, import_crypto.randomUUID)(), content, options };
207
219
  queue.push(message);
208
- const { logger } = this.options;
209
220
  logger.debug(`memory queue (${channel}) published message`, content);
210
- setImmediate(() => {
211
- this.emitter.emit(channel, channel);
212
- });
221
+ this.scheduleListen(channel);
213
222
  }
214
223
  async consume(channel, once = false) {
215
224
  while (this.connected && this.events.get(channel)) {
@@ -54,6 +54,11 @@ function getAppName(app) {
54
54
  }
55
55
  __name(getAppName, "getAppName");
56
56
  const errors = {
57
+ APP_ENVIRONMENT_UNAVAILABLE: {
58
+ status: 503,
59
+ message: /* @__PURE__ */ __name(({ appName }) => `deployment environment for application ${appName} is unavailable`, "message"),
60
+ maintaining: true
61
+ },
57
62
  APP_NOT_FOUND: {
58
63
  status: 404,
59
64
  message: /* @__PURE__ */ __name(({ appName }) => `application ${appName} not found`, "message"),
@@ -66,7 +66,6 @@ var import_plugin_manager = require("../plugin-manager");
66
66
  var import_errors = require("./errors");
67
67
  var import_ipc_socket_client = require("./ipc-socket-client");
68
68
  var import_ipc_socket_server = require("./ipc-socket-server");
69
- var import_static_file_security = require("./static-file-security");
70
69
  var import_utils3 = require("./utils");
71
70
  var import_ws_server = require("./ws-server");
72
71
  var import_node_worker_threads = require("node:worker_threads");
@@ -122,7 +121,17 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
122
121
  const internalUrl = req.url;
123
122
  req.url = this.getOriginalRequestUrl(req);
124
123
  try {
125
- return await supervisor.proxyWeb(appName, req, res);
124
+ const proxied = await supervisor.proxyWeb(appName, req, res);
125
+ const adapter = supervisor.getDiscoveryAdapter();
126
+ if (!proxied && import_node_process.default.env.APP_MODE === "supervisor" && typeof adapter.proxyWeb === "function") {
127
+ let code = "APP_ENVIRONMENT_UNAVAILABLE";
128
+ if (typeof adapter.getAppModel === "function" && !await supervisor.getAppModel(appName)) {
129
+ code = "APP_NOT_FOUND";
130
+ }
131
+ this.responseErrorWithCode(code, res, { appName });
132
+ return true;
133
+ }
134
+ return proxied;
126
135
  } finally {
127
136
  req.url = internalUrl;
128
137
  }
@@ -382,24 +391,6 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
382
391
  this.responseErrorWithCode("APP_INITIALIZING", res, { appName: handleApp });
383
392
  return;
384
393
  }
385
- if (pathname.startsWith(APP_PUBLIC_PATH + "storage/uploads/")) {
386
- if (handleApp !== "main") {
387
- const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
388
- if (isProxy) {
389
- return;
390
- }
391
- }
392
- 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
- req.url = req.url.substring(APP_PUBLIC_PATH.length + "storage".length);
397
- await compress(req, res);
398
- return (0, import_serve_handler.default)(req, res, {
399
- public: (0, import_utils.resolveStorageRoot)(),
400
- directoryListing: false
401
- });
402
- }
403
394
  if (pathname.startsWith(APP_PUBLIC_PATH + "dist/")) {
404
395
  if (handleApp !== "main") {
405
396
  const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
@@ -436,7 +427,8 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
436
427
  });
437
428
  }
438
429
  const isFilesRequest = Boolean(getFileAccessRestPath(pathname, APP_PUBLIC_PATH));
439
- if (!pathname.startsWith(import_node_process.default.env.API_BASE_PATH) && !isFilesRequest) {
430
+ const isLegacyUploadRequest = pathname.startsWith(APP_PUBLIC_PATH + "storage/uploads/");
431
+ if (!pathname.startsWith(import_node_process.default.env.API_BASE_PATH) && !isFilesRequest && !isLegacyUploadRequest) {
440
432
  if (this.isV2Request(pathname)) {
441
433
  if (handleApp !== "main") {
442
434
  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,8 @@ function hasActiveContentExtension(pathname = "") {
63
74
  __name(hasActiveContentExtension, "hasActiveContentExtension");
64
75
  function getStorageUploadSecurityHeaders(pathname = "") {
65
76
  const headers = {
77
+ "Cache-Control": "private, no-store",
78
+ "Content-Security-Policy": "sandbox",
66
79
  "X-Content-Type-Options": "nosniff"
67
80
  };
68
81
  if (hasActiveContentExtension(pathname) || shouldDownload(pathname)) {
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);
@@ -56,10 +57,24 @@ var import_crypto = require("crypto");
56
57
  var import_fs = __toESM(require("fs"));
57
58
  var import_i18next = __toESM(require("i18next"));
58
59
  var import_koa_bodyparser = __toESM(require("koa-bodyparser"));
60
+ var import_koa_send = __toESM(require("koa-send"));
59
61
  var import_perf_hooks = require("perf_hooks");
60
62
  var import_data_wrapping = require("./middlewares/data-wrapping");
61
63
  var import_extract_client_ip = require("./middlewares/extract-client-ip");
64
+ var import_static_file_security = require("./gateway/static-file-security");
62
65
  var import_i18n = require("./middlewares/i18n");
66
+ function resolveAppPublicPath(value = "/") {
67
+ const normalized = String(value || "/").trim() || "/";
68
+ const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
69
+ return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
70
+ }
71
+ __name(resolveAppPublicPath, "resolveAppPublicPath");
72
+ function resolveApiBasePath(value = "/api") {
73
+ const normalized = String(value || "/api").trim() || "/api";
74
+ const withLeadingSlash = normalized.startsWith("/") ? normalized : `/${normalized}`;
75
+ return withLeadingSlash.replace(/\/+$/g, "") || "/";
76
+ }
77
+ __name(resolveApiBasePath, "resolveApiBasePath");
63
78
  function createI18n(options) {
64
79
  const instance = import_i18next.default.createInstance();
65
80
  instance.init({
@@ -78,26 +93,23 @@ function createResourcer(options) {
78
93
  __name(createResourcer, "createResourcer");
79
94
  function isWhitelistedCorsOrigin(ctx) {
80
95
  const origin = ctx.get("origin");
81
- const whitelist = (0, import_utils.getCorsWhitelist)();
82
96
  if (!origin) {
83
97
  return false;
84
98
  }
85
- if (!whitelist) {
86
- return (0, import_utils.isTrustedOrigin)(ctx, origin);
87
- }
88
- return whitelist.has(origin);
99
+ return (0, import_utils.isTrustedOrigin)(ctx, origin);
89
100
  }
90
101
  __name(isWhitelistedCorsOrigin, "isWhitelistedCorsOrigin");
91
102
  function resolveCorsOrigin(ctx) {
92
103
  const origin = ctx.get("origin");
93
104
  const disallowNoOrigin = process.env.CORS_DISALLOW_NO_ORIGIN === "true";
105
+ const whitelist = (0, import_utils.getCorsWhitelist)();
94
106
  if (!origin && disallowNoOrigin) {
95
107
  return false;
96
108
  }
97
109
  if (isWhitelistedCorsOrigin(ctx)) {
98
110
  return origin;
99
111
  }
100
- return (0, import_utils.getCorsWhitelist)() ? false : origin;
112
+ return whitelist ? false : origin;
101
113
  }
102
114
  __name(resolveCorsOrigin, "resolveCorsOrigin");
103
115
  function registerMiddlewares(app, options) {
@@ -114,7 +126,7 @@ function registerMiddlewares(app, options) {
114
126
  app.use(
115
127
  (0, import_cors.default)({
116
128
  credentials: isWhitelistedCorsOrigin,
117
- exposeHeaders: ["content-disposition"],
129
+ exposeHeaders: ["content-disposition", "x-new-token"],
118
130
  origin: resolveCorsOrigin,
119
131
  ...options.cors
120
132
  }),
@@ -140,6 +152,7 @@ function registerMiddlewares(app, options) {
140
152
  }
141
153
  app.use(/* @__PURE__ */ __name(async function getBearerToken(ctx, next) {
142
154
  ctx.getBearerToken = () => {
155
+ var _a2, _b;
143
156
  const authorization = ctx.get("Authorization");
144
157
  if (authorization) {
145
158
  ctx.state.pendingAuthTokenSource = "authorization";
@@ -149,7 +162,8 @@ function registerMiddlewares(app, options) {
149
162
  ctx.state.pendingAuthTokenSource = "query";
150
163
  return ctx.query.token;
151
164
  }
152
- const cookieToken = ctx.cookies.get((0, import_utils.getAuthCookieName)("authToken", app.name));
165
+ const canUseAuthCookie = Boolean(((_a2 = ctx.state) == null ? void 0 : _a2.fileAccess) || ((_b = ctx.state) == null ? void 0 : _b.legacyFileAccess)) && ["GET", "HEAD"].includes(ctx.method);
166
+ const cookieToken = canUseAuthCookie ? ctx.cookies.get((0, import_utils.getAuthCookieName)("authToken", app.name)) : void 0;
153
167
  ctx.state.pendingAuthTokenSource = cookieToken ? "cookie" : void 0;
154
168
  return cookieToken;
155
169
  };
@@ -159,6 +173,47 @@ function registerMiddlewares(app, options) {
159
173
  if (options.dataWrapping !== false) {
160
174
  app.use((0, import_data_wrapping.dataWrapping)(), { tag: "dataWrapping", after: "cors" });
161
175
  }
176
+ app.use(
177
+ /* @__PURE__ */ __name(async function legacyFileAccess(ctx, next) {
178
+ const publicPath = resolveAppPublicPath(process.env.APP_PUBLIC_PATH);
179
+ const uploadsPrefix = `${publicPath}storage/uploads/`;
180
+ const authCheckPath = `${resolveApiBasePath(process.env.API_BASE_PATH)}/auth:checkLegacyFileAccess`;
181
+ const isUploadRequest = ctx.path.startsWith(uploadsPrefix);
182
+ const isAuthCheckRequest = ctx.path === authCheckPath || ctx.path === "/auth:checkLegacyFileAccess";
183
+ if (!isUploadRequest && !isAuthCheckRequest) {
184
+ return next();
185
+ }
186
+ if (!["GET", "HEAD"].includes(ctx.method)) {
187
+ return ctx.throw(405);
188
+ }
189
+ ctx.state.legacyFileAccess = true;
190
+ ctx.state.legacyLocalStoragePublicAccess = process.env.LEGACY_LOCAL_STORAGE_PUBLIC_ACCESS === "true";
191
+ if (ctx.state.legacyLocalStoragePublicAccess) {
192
+ ctx.skipAuthCheck = true;
193
+ }
194
+ if (isAuthCheckRequest) {
195
+ return next();
196
+ }
197
+ const originalPath = ctx.path;
198
+ ctx.path = authCheckPath;
199
+ try {
200
+ await next();
201
+ } finally {
202
+ ctx.path = originalPath;
203
+ }
204
+ const relativePath = `uploads/${originalPath.slice(uploadsPrefix.length)}`;
205
+ const headers = (0, import_static_file_security.getStorageUploadSecurityHeaders)(`${originalPath}${ctx.search || ""}`);
206
+ if (!headers["Content-Disposition"]) {
207
+ headers["Content-Disposition"] = "inline";
208
+ }
209
+ for (const [name, value] of Object.entries(headers)) {
210
+ ctx.set(name, value);
211
+ }
212
+ ctx.status = 200;
213
+ await (0, import_koa_send.default)(ctx, relativePath, { root: (0, import_utils.resolveStorageRoot)() });
214
+ }, "legacyFileAccess"),
215
+ { tag: "legacyFileAccess", before: "dataSource", after: "dataWrapping" }
216
+ );
162
217
  app.use(app.dataSourceManager.middleware(), { tag: "dataSource", after: "dataWrapping" });
163
218
  app.use((0, import_extract_client_ip.extractClientIp)(), { tag: "extractClientIp", before: "cors" });
164
219
  }
@@ -325,5 +380,6 @@ __name(createContextVariablesScope, "createContextVariablesScope");
325
380
  getBodyLimit,
326
381
  getCommandFullName,
327
382
  registerMiddlewares,
383
+ resolveCorsOrigin,
328
384
  tsxRerunning
329
385
  });
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": "2.3.0-beta.10",
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": "2.3.0-beta.10",
14
+ "@nocobase/actions": "2.3.0-beta.10",
15
+ "@nocobase/ai": "2.3.0-beta.10",
16
+ "@nocobase/auth": "2.3.0-beta.10",
17
+ "@nocobase/cache": "2.3.0-beta.10",
18
+ "@nocobase/data-source-manager": "2.3.0-beta.10",
19
+ "@nocobase/database": "2.3.0-beta.10",
20
+ "@nocobase/evaluators": "2.3.0-beta.10",
21
+ "@nocobase/lock-manager": "2.3.0-beta.10",
22
+ "@nocobase/logger": "2.3.0-beta.10",
23
+ "@nocobase/resourcer": "2.3.0-beta.10",
24
+ "@nocobase/sdk": "2.3.0-beta.10",
25
+ "@nocobase/snowflake-id": "2.3.0-beta.10",
26
+ "@nocobase/telemetry": "2.3.0-beta.10",
27
+ "@nocobase/utils": "2.3.0-beta.10",
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": "7aa3dce6cf3201752882f7012c72997898015d89"
65
65
  }