@next-core/brick-container 2.98.23 → 2.99.0

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.
package/dist/preview.html CHANGED
@@ -1 +1 @@
1
- <!doctype html><html lang="zh-CN" data-theme="light" data-mode="default"><head><meta charset="utf-8"/><base href="<!--# echo var='base_href' default='/' -->"/><script>window.DEVELOPER_PREVIEW = true;</script><link href="preview.5fb551513edc8051ce6c.css" rel="stylesheet"></head><body><div id="preview-root"><div id="main-mount-point"></div><div id="bg-mount-point" style="display: none"></div><div id="portal-mount-point"></div></div><script src="dll.90ef3f2c.js"></script><script src="polyfill.3e88f145a0893497373b.js"></script><script src="preview.3d1b49a405ab0a4ae5f6.js"></script></body></html>
1
+ <!doctype html><html lang="zh-CN" data-theme="light" data-mode="default"><head><meta charset="utf-8"/><base href="<!--# echo var='base_href' default='/' -->"/><script>window.DEVELOPER_PREVIEW = true;</script><link href="preview.5fb551513edc8051ce6c.css" rel="stylesheet"></head><body><div id="preview-root"><div id="main-mount-point"></div><div id="bg-mount-point" style="display: none"></div><div id="portal-mount-point"></div></div><script src="dll.42523c4f.js"></script><script src="polyfill.3e88f145a0893497373b.js"></script><script src="preview.793b87ea20dd67a8a1e7.js"></script></body></html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@next-core/brick-container",
3
- "version": "2.98.23",
3
+ "version": "2.99.0",
4
4
  "description": "Brick Container Server",
5
5
  "homepage": "https://github.com/easyops-cn/next-core/tree/master/packages/brick-container",
6
6
  "license": "GPL-3.0",
@@ -73,5 +73,5 @@
73
73
  "webpack-dev-server": "^4.11.1",
74
74
  "webpack-merge": "^5.8.0"
75
75
  },
76
- "gitHead": "20369431686b418451a68783182d56514970f61f"
76
+ "gitHead": "dd3c7ae18c87b52d96c60732ff7f957dfd46862e"
77
77
  }
package/serve/getEnv.js CHANGED
@@ -2,6 +2,7 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const meow = require("meow");
4
4
  const chalk = require("chalk");
5
+ const yaml = require("js-yaml");
5
6
  const { getEasyopsConfig } = require("@next-core/repo-config");
6
7
  const {
7
8
  getNamesOfMicroApps,
@@ -163,6 +164,9 @@ module.exports = (runtimeFlags) => {
163
164
  useSkywalkingAnalysis: {
164
165
  type: "string",
165
166
  },
167
+ proxyConfig: {
168
+ type: "string",
169
+ },
166
170
  };
167
171
  const cli = meow(
168
172
  `
@@ -201,6 +205,7 @@ module.exports = (runtimeFlags) => {
201
205
  --help Show help message
202
206
  --version Show brick container version
203
207
  --use-skywalking-analysis Use skywalking analysis
208
+ --proxy-config Specify custom proxy config file path (defaults to "dev.proxy.yaml" in project root)
204
209
  `,
205
210
  {
206
211
  flags: flagOptions,
@@ -247,6 +252,37 @@ module.exports = (runtimeFlags) => {
247
252
  );
248
253
  const appConfig = (devConfig && devConfig.appConfig) || {};
249
254
 
255
+ function getLocalProxies() {
256
+ const customPath = flags.proxyConfig;
257
+ const proxyConfigPath = customPath
258
+ ? path.resolve(customPath)
259
+ : path.join(rootDir, "dev.proxy.yaml");
260
+ if (fs.existsSync(proxyConfigPath)) {
261
+ console.log(chalk.cyan("proxy config:"), proxyConfigPath);
262
+ let content;
263
+ try {
264
+ content = yaml.safeLoad(fs.readFileSync(proxyConfigPath, "utf8"));
265
+ } catch (e) {
266
+ console.error(
267
+ chalk.red("Failed to parse proxy config:"),
268
+ proxyConfigPath,
269
+ e.message
270
+ );
271
+ return {};
272
+ }
273
+ if (!content) return {};
274
+ if (content.proxies) {
275
+ return content;
276
+ }
277
+ return { proxies: content };
278
+ }
279
+ if (customPath) {
280
+ console.error(chalk.red("proxy config not found:"), proxyConfigPath);
281
+ }
282
+ return {};
283
+ }
284
+ const localProxies = getLocalProxies();
285
+
250
286
  const { usePublicScope, standalone: confStandalone } =
251
287
  getEasyopsConfig(nextRepoDir);
252
288
 
@@ -412,6 +448,7 @@ module.exports = (runtimeFlags) => {
412
448
  publicCdn: flags.publicCdn,
413
449
  asCdn: flags.asCdn,
414
450
  useSkywalkingAnalysis,
451
+ localProxies,
415
452
  isWebpackServe,
416
453
  };
417
454
 
@@ -482,6 +519,22 @@ module.exports = (runtimeFlags) => {
482
519
  console.log("local templates:", env.localTemplates);
483
520
  }
484
521
 
522
+ if (
523
+ env.localProxies.proxies &&
524
+ Object.keys(env.localProxies.proxies).length > 0
525
+ ) {
526
+ console.log();
527
+ console.log("local proxies:", env.localProxies.proxies);
528
+ if (env.localProxies.auth) {
529
+ console.log(
530
+ "local proxy auth: appId=%s, org=%s, user=%s",
531
+ env.localProxies.auth.appId || "api_gateway",
532
+ env.localProxies.auth.org,
533
+ env.localProxies.auth.user
534
+ );
535
+ }
536
+ }
537
+
485
538
  console.log();
486
539
  console.log(
487
540
  chalk.bold.cyan("mode:"),
@@ -4,7 +4,6 @@ const { throttle } = require("lodash");
4
4
  const { getPatternsToWatch } = require("./utils");
5
5
 
6
6
  module.exports = function liveReload(env) {
7
- // 建立 websocket 连接支持自动刷新
8
7
  if (env.liveReload) {
9
8
  const wss = new WebSocket.Server({ port: env.wsPort });
10
9
 
@@ -0,0 +1,76 @@
1
+ const { createProxyMiddleware } = require("http-proxy-middleware");
2
+ const { escapeRegExp } = require("lodash");
3
+ const chalk = require("chalk");
4
+ const { getToken } = require("./tokenManager");
5
+
6
+ function setupLocalProxies(app, env) {
7
+ const config = env.localProxies;
8
+ if (!config || !config.proxies || Object.keys(config.proxies).length === 0) {
9
+ return;
10
+ }
11
+
12
+ const { proxies, auth } = config;
13
+ const org = auth && auth.org;
14
+ const user = auth && auth.user;
15
+ const serverUrl = env.server;
16
+
17
+ // 预热 token,避免首次请求 401
18
+ if (auth && !auth.token) {
19
+ getToken(serverUrl, auth).catch((e) =>
20
+ console.error(
21
+ chalk.red("[local-proxy]"),
22
+ "Token pre-warm failed:",
23
+ e.message
24
+ )
25
+ );
26
+ }
27
+
28
+ for (const [pathPattern, target] of Object.entries(proxies)) {
29
+ const gatewayPath = pathPattern.startsWith("/")
30
+ ? pathPattern
31
+ : `/api/gateway/${pathPattern}`;
32
+ const fullPath = `${env.baseHref}${gatewayPath.slice(1)}`;
33
+
34
+ const proxy = createProxyMiddleware({
35
+ target,
36
+ secure: false,
37
+ changeOrigin: true,
38
+ logLevel: "debug",
39
+ pathRewrite: { [`^${escapeRegExp(fullPath)}`]: "" },
40
+ onProxyReq: (proxyReq) => {
41
+ if (org && !proxyReq.getHeader("org")) {
42
+ proxyReq.setHeader("org", String(org));
43
+ }
44
+ if (user && !proxyReq.getHeader("user")) {
45
+ proxyReq.setHeader("user", user);
46
+ }
47
+ },
48
+ });
49
+
50
+ app.use(fullPath, async (req, res, next) => {
51
+ console.log(
52
+ chalk.cyan("[local-proxy]"),
53
+ chalk.green(pathPattern) + chalk.white(":"),
54
+ chalk.yellow(req.method),
55
+ chalk.white(req.url)
56
+ );
57
+ if (auth) {
58
+ try {
59
+ const token = await getToken(serverUrl, auth);
60
+ if (token) {
61
+ req.headers["authorization"] = `Bearer ${token}`;
62
+ }
63
+ } catch (e) {
64
+ console.error(
65
+ chalk.red("[local-proxy]"),
66
+ "Failed to get token:",
67
+ e.message
68
+ );
69
+ }
70
+ }
71
+ proxy(req, res, next);
72
+ });
73
+ }
74
+ }
75
+
76
+ module.exports = { setupLocalProxies };
package/serve/serve.js CHANGED
@@ -11,6 +11,7 @@ const serveLocal = require("./serveLocal");
11
11
  const getProxies = require("./getProxies");
12
12
  const { getIndexHtml, distDir, getRawIndexHtml } = require("./getIndexHtml");
13
13
  const liveReload = require("./liveReload");
14
+ const { setupLocalProxies } = require("./localProxy");
14
15
 
15
16
  module.exports = function serve(runtimeFlags) {
16
17
  const env = getEnv(runtimeFlags);
@@ -86,6 +87,9 @@ module.exports = function serve(runtimeFlags) {
86
87
  app.use(`${env.baseHref}:appId/-/core/`, express.static(distDir));
87
88
  }
88
89
 
90
+ // Register local service proxies before default proxies.
91
+ setupLocalProxies(app, env);
92
+
89
93
  // Using proxies.
90
94
  const proxies = getProxies(env, getRawIndexHtml);
91
95
  if (proxies) {
@@ -0,0 +1,224 @@
1
+ const crypto = require("crypto");
2
+ const https = require("https");
3
+ const http = require("http");
4
+ const { URL } = require("url");
5
+ const chalk = require("chalk");
6
+
7
+ const TOKEN_CACHE_TTL = 1800 * 1000;
8
+ const CREDENTIALS_CACHE_TTL = 3600 * 1000;
9
+ const DEFAULT_APP_ID = "api_gateway";
10
+ const GATEWAY_INNER_PORT = 8107;
11
+ const GATEWAY_SERVICE_PATH = "/api/gateway/user_service.apikey";
12
+
13
+ const _tokenCache = {};
14
+ const _credentialsCache = {};
15
+
16
+ function generateSignature(
17
+ method,
18
+ urlPath,
19
+ query,
20
+ contentType,
21
+ clientId,
22
+ secret,
23
+ timestamp
24
+ ) {
25
+ const keyList = Object.keys(query)
26
+ .filter((k) => k !== "signature")
27
+ .sort();
28
+
29
+ const queryParts = [];
30
+ for (const key of keyList) {
31
+ const val = query[key];
32
+ if (Array.isArray(val)) {
33
+ for (const v of val) {
34
+ queryParts.push(`${key}${v}`);
35
+ }
36
+ } else {
37
+ queryParts.push(`${key}${val}`);
38
+ }
39
+ }
40
+
41
+ const data = [
42
+ method,
43
+ urlPath,
44
+ queryParts.join(""),
45
+ contentType,
46
+ clientId,
47
+ secret,
48
+ timestamp,
49
+ ].join("\n");
50
+ return crypto.createHmac("sha256", secret).update(data).digest("hex");
51
+ }
52
+
53
+ function httpRequest(fullUrl, options, body) {
54
+ return new Promise((resolve, reject) => {
55
+ const parsed = new URL(fullUrl);
56
+ const mod = parsed.protocol === "https:" ? https : http;
57
+
58
+ if (body) {
59
+ options.headers = options.headers || {};
60
+ options.headers["Content-Length"] = Buffer.byteLength(body);
61
+ }
62
+
63
+ const req = mod.request(
64
+ fullUrl,
65
+ { ...options, timeout: 30000 },
66
+ (res) => {
67
+ let data = "";
68
+ res.on("data", (chunk) => (data += chunk));
69
+ res.on("end", () => {
70
+ if (res.statusCode !== 200) {
71
+ return reject(new Error(`HTTP ${res.statusCode}: ${data}`));
72
+ }
73
+ try {
74
+ resolve(JSON.parse(data));
75
+ } catch (e) {
76
+ reject(new Error(`JSON parse error: ${e.message}`));
77
+ }
78
+ });
79
+ }
80
+ );
81
+ req.on("error", (e) => reject(new Error(`network error: ${e.message}`)));
82
+ req.on("timeout", () => {
83
+ req.destroy();
84
+ reject(new Error("request timeout"));
85
+ });
86
+ req.end(body || undefined);
87
+ });
88
+ }
89
+
90
+ function getInnerGatewayUrl(serverUrl) {
91
+ const parsed = new URL(serverUrl);
92
+ return `http://${parsed.hostname}:${GATEWAY_INNER_PORT}${GATEWAY_SERVICE_PATH}`;
93
+ }
94
+
95
+ async function createServerApiKey(serverUrl, clientId) {
96
+ const cacheKey = `${serverUrl}:${clientId}`;
97
+ const now = Date.now();
98
+ if (
99
+ _credentialsCache[cacheKey] &&
100
+ now < _credentialsCache[cacheKey].expireAt
101
+ ) {
102
+ return _credentialsCache[cacheKey].value;
103
+ }
104
+
105
+ const timestamp = String(Math.floor(now / 1000));
106
+ const servicePath = "/api/v1/apikey/server";
107
+ const signature = generateSignature(
108
+ "POST",
109
+ servicePath,
110
+ {},
111
+ "application/json",
112
+ clientId,
113
+ clientId,
114
+ timestamp
115
+ );
116
+
117
+ const baseUrl = getInnerGatewayUrl(serverUrl);
118
+ const fullUrl = `${baseUrl}${servicePath}`;
119
+ console.log(
120
+ chalk.cyan("[token-manager]"),
121
+ "Creating server apikey for",
122
+ clientId,
123
+ "(via inner port)"
124
+ );
125
+
126
+ const data = await httpRequest(
127
+ fullUrl,
128
+ {
129
+ method: "POST",
130
+ headers: {
131
+ "Content-Type": "application/json",
132
+ "X-Timestamp": timestamp,
133
+ },
134
+ },
135
+ JSON.stringify({ clientId, signature })
136
+ );
137
+
138
+ const secret = data.data?.secret;
139
+ if (!secret) {
140
+ throw new Error(
141
+ `CreateServerApiKey: unexpected response: ${JSON.stringify(data)}`
142
+ );
143
+ }
144
+
145
+ const value = { clientId, secret };
146
+ _credentialsCache[cacheKey] = {
147
+ value,
148
+ expireAt: now + CREDENTIALS_CACHE_TTL,
149
+ };
150
+ return value;
151
+ }
152
+
153
+ async function requestToken(serverUrl, clientId, secret, user, org) {
154
+ const timestamp = String(Math.floor(Date.now() / 1000));
155
+ const servicePath = `/api/v1/apikey/request_token/client_id/${encodeURIComponent(
156
+ clientId
157
+ )}`;
158
+
159
+ const query = {
160
+ user: user || "defaultUser",
161
+ org: org != null ? String(org) : "0",
162
+ };
163
+ query.signature = generateSignature(
164
+ "GET",
165
+ servicePath,
166
+ query,
167
+ "",
168
+ clientId,
169
+ secret,
170
+ timestamp
171
+ );
172
+
173
+ const qs = Object.entries(query)
174
+ .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
175
+ .join("&");
176
+
177
+ const baseUrl = getInnerGatewayUrl(serverUrl);
178
+ const fullUrl = `${baseUrl}${servicePath}?${qs}`;
179
+ const data = await httpRequest(fullUrl, {
180
+ method: "GET",
181
+ headers: { "X-Timestamp": timestamp },
182
+ });
183
+
184
+ if (data.code !== 0 || !data.data) {
185
+ throw new Error(
186
+ `RequestToken: unexpected response: ${JSON.stringify(data)}`
187
+ );
188
+ }
189
+ return data.data.token || "";
190
+ }
191
+
192
+ async function getToken(serverUrl, auth) {
193
+ if (auth.token) {
194
+ return auth.token;
195
+ }
196
+
197
+ const appId = auth.appId || DEFAULT_APP_ID;
198
+ const clientId = `easyops_server_${appId}`;
199
+ const cacheKey = `${serverUrl}:${clientId}:${auth.org || ""}:${
200
+ auth.user || ""
201
+ }`;
202
+
203
+ const now = Date.now();
204
+ if (_tokenCache[cacheKey] && now < _tokenCache[cacheKey].expireAt) {
205
+ return _tokenCache[cacheKey].token;
206
+ }
207
+
208
+ const creds = await createServerApiKey(serverUrl, clientId);
209
+ const token = await requestToken(
210
+ serverUrl,
211
+ creds.clientId,
212
+ creds.secret,
213
+ auth.user,
214
+ auth.org
215
+ );
216
+ _tokenCache[cacheKey] = { token, expireAt: now + TOKEN_CACHE_TTL };
217
+ console.log(
218
+ chalk.green("[token-manager]"),
219
+ "Token acquired, cached for 30 minutes"
220
+ );
221
+ return token;
222
+ }
223
+
224
+ module.exports = { getToken };