@nocobase/server 2.4.0-alpha.4 → 2.4.0-alpha.6

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,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
  }
@@ -604,24 +603,25 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
604
603
  if (!this.environmentName || typeof this.discoveryAdapter.registerEnvironment !== "function") {
605
604
  return;
606
605
  }
607
- 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 {
608
613
  name: this.environmentName,
609
614
  url: this.environmentUrl || "",
610
615
  proxyUrl: this.environmentProxyUrl || this.environmentUrl || "",
611
616
  appVersion: mainApp.getPackageVersion(),
612
617
  lastHeartbeatAt: Date.now()
613
- });
614
- if (registered) {
615
- this.heartbeatEnvironment();
616
- }
618
+ };
617
619
  }
618
620
  async unregisterEnvironment() {
621
+ this.stopEnvironmentHeartbeat();
619
622
  if (this.environmentName && typeof this.discoveryAdapter.unregisterEnvironment === "function") {
620
623
  await this.discoveryAdapter.unregisterEnvironment();
621
624
  }
622
- if (this.environmentHeartbeatTimer) {
623
- this.environmentHeartbeatTimer = null;
624
- }
625
625
  }
626
626
  normalizeEnvInfo(environment) {
627
627
  if (typeof environment.available === "boolean") {
@@ -634,7 +634,7 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
634
634
  available: false
635
635
  };
636
636
  }
637
- const available = Date.now() - lastHeartbeatAt <= this.environmentHeartbeatInterval;
637
+ const available = Date.now() - lastHeartbeatAt <= this.environmentHeartbeatTimeout;
638
638
  return {
639
639
  ...environment,
640
640
  available
@@ -657,17 +657,26 @@ const _AppSupervisor = class _AppSupervisor extends import_events.EventEmitter {
657
657
  }
658
658
  return this.normalizeEnvInfo(environment);
659
659
  }
660
- async heartbeatEnvironment() {
660
+ async heartbeatEnvironment(mainApp) {
661
661
  if (typeof this.discoveryAdapter.heartbeatEnvironment !== "function") {
662
662
  return;
663
663
  }
664
664
  if (this.environmentHeartbeatTimer) {
665
665
  return;
666
666
  }
667
- this.environmentHeartbeatTimer = setInterval(
668
- () => this.discoveryAdapter.heartbeatEnvironment(),
669
- this.environmentHeartbeatInterval
670
- );
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
+ }
671
680
  }
672
681
  async dispatchCommand(command) {
673
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>;
@@ -8,10 +8,14 @@
8
8
  */
9
9
  export declare class Environment {
10
10
  private vars;
11
- setVariable(key: string, value: string): void;
11
+ private secretKeys;
12
+ setVariable(key: string, value: string, options?: {
13
+ isSecret?: boolean;
14
+ }): void;
12
15
  removeVariable(key: string): void;
13
16
  getVariablesAndSecrets(): {};
14
17
  getVariables(): {};
18
+ getNonSecretVariables(): {};
15
19
  renderJsonTemplate(template: any, options?: {
16
20
  omit?: string[];
17
21
  }): any;
@@ -44,11 +44,18 @@ var import_utils = require("@nocobase/utils");
44
44
  var import_lodash = __toESM(require("lodash"));
45
45
  const _Environment = class _Environment {
46
46
  vars = {};
47
- setVariable(key, value) {
47
+ secretKeys = /* @__PURE__ */ new Set();
48
+ setVariable(key, value, options) {
48
49
  this.vars[key] = value;
50
+ if ((options == null ? void 0 : options.isSecret) === true) {
51
+ this.secretKeys.add(key);
52
+ } else if ((options == null ? void 0 : options.isSecret) === false) {
53
+ this.secretKeys.delete(key);
54
+ }
49
55
  }
50
56
  removeVariable(key) {
51
57
  delete this.vars[key];
58
+ this.secretKeys.delete(key);
52
59
  }
53
60
  getVariablesAndSecrets() {
54
61
  return this.vars;
@@ -56,6 +63,15 @@ const _Environment = class _Environment {
56
63
  getVariables() {
57
64
  return this.vars;
58
65
  }
66
+ getNonSecretVariables() {
67
+ const result = {};
68
+ for (const key of Object.keys(this.vars)) {
69
+ if (!this.secretKeys.has(key)) {
70
+ result[key] = this.vars[key];
71
+ }
72
+ }
73
+ return result;
74
+ }
59
75
  renderJsonTemplate(template, options) {
60
76
  if (options == null ? void 0 : options.omit) {
61
77
  const omitTemplate = import_lodash.default.omit(template, options.omit);
@@ -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");
@@ -123,7 +122,17 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
123
122
  const internalUrl = req.url;
124
123
  req.url = this.getOriginalRequestUrl(req);
125
124
  try {
126
- return await supervisor.proxyWeb(appName, req, res);
125
+ const proxied = await supervisor.proxyWeb(appName, req, res);
126
+ const adapter = supervisor.getDiscoveryAdapter();
127
+ if (!proxied && import_node_process.default.env.APP_MODE === "supervisor" && typeof adapter.proxyWeb === "function") {
128
+ let code = "APP_ENVIRONMENT_UNAVAILABLE";
129
+ if (typeof adapter.getAppModel === "function" && !await supervisor.getAppModel(appName)) {
130
+ code = "APP_NOT_FOUND";
131
+ }
132
+ this.responseErrorWithCode(code, res, { appName });
133
+ return true;
134
+ }
135
+ return proxied;
127
136
  } finally {
128
137
  req.url = internalUrl;
129
138
  }
@@ -608,27 +617,6 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
608
617
  this.responseErrorWithCode("APP_INITIALIZING", res, { appName: handleApp });
609
618
  return;
610
619
  }
611
- if (pathname.startsWith(APP_PUBLIC_PATH + "storage/uploads/")) {
612
- if (handleApp !== "main") {
613
- const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
614
- if (isProxy) {
615
- return;
616
- }
617
- }
618
- const headers = (0, import_static_file_security.getStorageUploadSecurityHeaders)(`${pathname}${search || ""}`);
619
- req.url = req.url.substring(APP_PUBLIC_PATH.length + "storage".length);
620
- await compress(req, res);
621
- return (0, import_serve_handler.default)(req, res, {
622
- public: (0, import_utils.resolveStorageRoot)(),
623
- directoryListing: false,
624
- headers: [
625
- {
626
- source: "**/*",
627
- headers: Object.entries(headers).map(([key, value]) => ({ key, value }))
628
- }
629
- ]
630
- });
631
- }
632
620
  if (pathname.startsWith(APP_PUBLIC_PATH + "dist/")) {
633
621
  if (handleApp !== "main") {
634
622
  const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
@@ -665,7 +653,8 @@ const _Gateway = class _Gateway extends import_events.EventEmitter {
665
653
  });
666
654
  }
667
655
  const isFilesRequest = Boolean(getFileAccessRestPath(pathname, APP_PUBLIC_PATH));
668
- if (!pathname.startsWith(import_node_process.default.env.API_BASE_PATH) && !isFilesRequest) {
656
+ const isLegacyUploadRequest = pathname.startsWith(APP_PUBLIC_PATH + "storage/uploads/");
657
+ if (!pathname.startsWith(import_node_process.default.env.API_BASE_PATH) && !isFilesRequest && !isLegacyUploadRequest) {
669
658
  if (this.isSettingsRequest(pathname)) {
670
659
  if (handleApp !== "main") {
671
660
  const isProxy = await this.proxyRequestToSubApp(supervisor, handleApp, req, res);
@@ -74,6 +74,7 @@ function hasActiveContentExtension(pathname = "") {
74
74
  __name(hasActiveContentExtension, "hasActiveContentExtension");
75
75
  function getStorageUploadSecurityHeaders(pathname = "") {
76
76
  const headers = {
77
+ "Cache-Control": "private, no-store",
77
78
  "Content-Security-Policy": "sandbox",
78
79
  "X-Content-Type-Options": "nosniff"
79
80
  };
package/lib/helper.js CHANGED
@@ -57,10 +57,24 @@ var import_crypto = require("crypto");
57
57
  var import_fs = __toESM(require("fs"));
58
58
  var import_i18next = __toESM(require("i18next"));
59
59
  var import_koa_bodyparser = __toESM(require("koa-bodyparser"));
60
+ var import_koa_send = __toESM(require("koa-send"));
60
61
  var import_perf_hooks = require("perf_hooks");
61
62
  var import_data_wrapping = require("./middlewares/data-wrapping");
62
63
  var import_extract_client_ip = require("./middlewares/extract-client-ip");
64
+ var import_static_file_security = require("./gateway/static-file-security");
63
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");
64
78
  function createI18n(options) {
65
79
  const instance = import_i18next.default.createInstance();
66
80
  instance.init({
@@ -138,7 +152,7 @@ function registerMiddlewares(app, options) {
138
152
  }
139
153
  app.use(/* @__PURE__ */ __name(async function getBearerToken(ctx, next) {
140
154
  ctx.getBearerToken = () => {
141
- var _a2;
155
+ var _a2, _b;
142
156
  const authorization = ctx.get("Authorization");
143
157
  if (authorization) {
144
158
  ctx.state.pendingAuthTokenSource = "authorization";
@@ -148,7 +162,7 @@ function registerMiddlewares(app, options) {
148
162
  ctx.state.pendingAuthTokenSource = "query";
149
163
  return ctx.query.token;
150
164
  }
151
- const canUseAuthCookie = Boolean((_a2 = ctx.state) == null ? void 0 : _a2.fileAccess) && ["GET", "HEAD"].includes(ctx.method);
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);
152
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;
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nocobase/server",
3
- "version": "2.4.0-alpha.4",
3
+ "version": "2.4.0-alpha.6",
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.4.0-alpha.4",
14
- "@nocobase/actions": "2.4.0-alpha.4",
15
- "@nocobase/ai": "2.4.0-alpha.4",
16
- "@nocobase/auth": "2.4.0-alpha.4",
17
- "@nocobase/cache": "2.4.0-alpha.4",
18
- "@nocobase/data-source-manager": "2.4.0-alpha.4",
19
- "@nocobase/database": "2.4.0-alpha.4",
20
- "@nocobase/evaluators": "2.4.0-alpha.4",
21
- "@nocobase/lock-manager": "2.4.0-alpha.4",
22
- "@nocobase/logger": "2.4.0-alpha.4",
23
- "@nocobase/resourcer": "2.4.0-alpha.4",
24
- "@nocobase/sdk": "2.4.0-alpha.4",
25
- "@nocobase/snowflake-id": "2.4.0-alpha.4",
26
- "@nocobase/telemetry": "2.4.0-alpha.4",
27
- "@nocobase/utils": "2.4.0-alpha.4",
13
+ "@nocobase/acl": "2.4.0-alpha.6",
14
+ "@nocobase/actions": "2.4.0-alpha.6",
15
+ "@nocobase/ai": "2.4.0-alpha.6",
16
+ "@nocobase/auth": "2.4.0-alpha.6",
17
+ "@nocobase/cache": "2.4.0-alpha.6",
18
+ "@nocobase/data-source-manager": "2.4.0-alpha.6",
19
+ "@nocobase/database": "2.4.0-alpha.6",
20
+ "@nocobase/evaluators": "2.4.0-alpha.6",
21
+ "@nocobase/lock-manager": "2.4.0-alpha.6",
22
+ "@nocobase/logger": "2.4.0-alpha.6",
23
+ "@nocobase/resourcer": "2.4.0-alpha.6",
24
+ "@nocobase/sdk": "2.4.0-alpha.6",
25
+ "@nocobase/snowflake-id": "2.4.0-alpha.6",
26
+ "@nocobase/telemetry": "2.4.0-alpha.6",
27
+ "@nocobase/utils": "2.4.0-alpha.6",
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": "527910882037ce1558ee6b0ef748208b8d6996bb"
64
+ "gitHead": "1fc4fcd17a0cd79912a642600bdc2103e2104bdb"
65
65
  }