@lark-apaas/express-core 0.0.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.
- package/LICENSE +13 -0
- package/README.md +153 -0
- package/dist/index.cjs +351 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +78 -0
- package/dist/index.d.ts +78 -0
- package/dist/index.js +309 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Lark Technologies Pte. Ltd. and/or its affiliates
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,provided that the above copyright notice and this permission notice appear in all copies.
|
|
6
|
+
|
|
7
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
8
|
+
IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE
|
|
9
|
+
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO
|
|
10
|
+
EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
|
11
|
+
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE,
|
|
12
|
+
DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
|
13
|
+
ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# @lark-apaas/express-core
|
|
2
|
+
|
|
3
|
+
Express 技术栈的平台核心框架,通过 `setup()` 一次性注册所有平台中间件,将平台逻辑与业务逻辑在框架层解耦。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 自动注册平台中间件链(UserContext、CSRF、RLS、ViewContext)
|
|
8
|
+
- 集成 DataPaaS 数据库连接管理与 RLS 支持
|
|
9
|
+
- CSRF Token 生成与验证
|
|
10
|
+
- 应用元数据自动注入到模板渲染上下文
|
|
11
|
+
- ESM live binding 导出 `db` 实例,使用体验与社区一致
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @lark-apaas/express-core
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Peer Dependencies
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install drizzle-orm@0.44.6 express
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
### 1. 初始化应用
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// app.ts
|
|
31
|
+
import express from 'express';
|
|
32
|
+
import { setup } from '@lark-apaas/express-core';
|
|
33
|
+
|
|
34
|
+
const app = express();
|
|
35
|
+
const { close } = setup(app);
|
|
36
|
+
|
|
37
|
+
// 业务路由
|
|
38
|
+
app.get('/api/users', async (req, res) => { ... });
|
|
39
|
+
|
|
40
|
+
app.listen(3000);
|
|
41
|
+
process.on('SIGTERM', () => close());
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2. 使用数据库
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// routes/users.ts
|
|
48
|
+
import { db } from '@lark-apaas/express-core';
|
|
49
|
+
import { users } from '../schema';
|
|
50
|
+
|
|
51
|
+
router.get('/users', async (req, res) => {
|
|
52
|
+
const result = await db.select().from(users);
|
|
53
|
+
res.json(result);
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### 3. 访问用户上下文
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
app.get('/api/profile', (req, res) => {
|
|
61
|
+
const { userId, tenantId, roles } = req.userContext;
|
|
62
|
+
res.json({ userId, tenantId, roles });
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### 4. 访问平台数据(模板渲染)
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
app.get('/', (req, res) => {
|
|
70
|
+
// res.locals 已自动注入 appName, csrfToken 等
|
|
71
|
+
res.render('index');
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API 参考
|
|
76
|
+
|
|
77
|
+
### setup(app, config?)
|
|
78
|
+
|
|
79
|
+
初始化 Express 应用,注册所有平台中间件。
|
|
80
|
+
|
|
81
|
+
**参数:**
|
|
82
|
+
|
|
83
|
+
| 参数 | 类型 | 说明 |
|
|
84
|
+
|------|------|------|
|
|
85
|
+
| `app` | `express.Application` | Express 实例 |
|
|
86
|
+
| `config` | `SetupConfig` | 可选配置 |
|
|
87
|
+
|
|
88
|
+
**返回值:** `{ close: () => Promise<void> }` — 用于优雅关闭数据库连接。
|
|
89
|
+
|
|
90
|
+
**SetupConfig:**
|
|
91
|
+
|
|
92
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
93
|
+
|------|------|--------|------|
|
|
94
|
+
| `database.connectionString` | `string` | `process.env.DATABASE_URL` | PostgreSQL 连接串 |
|
|
95
|
+
| `database.schema` | `object` | - | Drizzle schema |
|
|
96
|
+
| `database.maxConnections` | `number` | `1` | 最大连接数 |
|
|
97
|
+
| `csrf.enabled` | `boolean` | `true` | 是否启用 CSRF 保护 |
|
|
98
|
+
| `csrf.cookieKey` | `string` | `'suda-csrf-token'` | CSRF cookie 名称 |
|
|
99
|
+
| `csrf.headerKey` | `string` | `'x-suda-csrf-token'` | CSRF header 名称 |
|
|
100
|
+
| `csrf.routes` | `string[]` | `['/api/*']` | CSRF 验证路由 |
|
|
101
|
+
| `bodyLimit` | `string` | `'1mb'` | body parser 大小限制 |
|
|
102
|
+
|
|
103
|
+
### db
|
|
104
|
+
|
|
105
|
+
通过 ESM live binding 导出的 Drizzle 数据库实例。`setup()` 调用后可用。
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { db } from '@lark-apaas/express-core';
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 中间件(高级用法)
|
|
112
|
+
|
|
113
|
+
各中间件也单独导出,供高级用户自行组合:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import {
|
|
117
|
+
userContextMiddleware,
|
|
118
|
+
createCsrfTokenMiddleware,
|
|
119
|
+
createCsrfValidationMiddleware,
|
|
120
|
+
createViewContextMiddleware,
|
|
121
|
+
} from '@lark-apaas/express-core';
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## setup() 注册的中间件顺序
|
|
125
|
+
|
|
126
|
+
| 顺序 | 中间件 | 作用域 | 说明 |
|
|
127
|
+
|------|--------|--------|------|
|
|
128
|
+
| 1 | body parser | 所有路由 | JSON + URL-encoded 解析 |
|
|
129
|
+
| 2 | cookie-parser | 所有路由 | Cookie 解析 |
|
|
130
|
+
| 3 | userContextMiddleware | 所有路由 | 从请求头解析用户上下文 |
|
|
131
|
+
| 4 | DataPaaS + RLS | 所有路由 | 数据库连接 + 行级安全 |
|
|
132
|
+
| 5 | csrfTokenMiddleware | 所有路由 | 生成 CSRF Token |
|
|
133
|
+
| 6 | csrfValidationMiddleware | `/api/*` | 验证 CSRF Token |
|
|
134
|
+
| 7 | viewContextMiddleware | 所有路由 | 注入应用元数据到渲染上下文 |
|
|
135
|
+
|
|
136
|
+
## req 扩展字段
|
|
137
|
+
|
|
138
|
+
| 字段 | 类型 | 来源 |
|
|
139
|
+
|------|------|------|
|
|
140
|
+
| `req.userContext` | `UserContext` | userContextMiddleware |
|
|
141
|
+
| `req.csrfToken` | `string` | csrfTokenMiddleware |
|
|
142
|
+
| `req.__platform_data__` | `object` | viewContextMiddleware |
|
|
143
|
+
|
|
144
|
+
## 注意事项
|
|
145
|
+
|
|
146
|
+
1. `setup()` 必须在注册业务路由之前调用
|
|
147
|
+
2. `db` 在 `setup()` 调用前为 `undefined`
|
|
148
|
+
3. 不传 `database.connectionString` 时自动读取 `process.env.DATABASE_URL`
|
|
149
|
+
4. drizzle-orm 必须为 `0.44.6` 版本
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/index.ts
|
|
32
|
+
var index_exports = {};
|
|
33
|
+
__export(index_exports, {
|
|
34
|
+
createCsrfTokenMiddleware: () => createCsrfTokenMiddleware,
|
|
35
|
+
createCsrfValidationMiddleware: () => createCsrfValidationMiddleware,
|
|
36
|
+
createViewContextMiddleware: () => createViewContextMiddleware,
|
|
37
|
+
db: () => db,
|
|
38
|
+
safeEscape: () => safeEscape,
|
|
39
|
+
safeStringify: () => safeStringify,
|
|
40
|
+
setup: () => setup,
|
|
41
|
+
userContextMiddleware: () => userContextMiddleware
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/setup.ts
|
|
46
|
+
var import_express = __toESM(require("express"), 1);
|
|
47
|
+
var import_cookie_parser = __toESM(require("cookie-parser"), 1);
|
|
48
|
+
var import_express_datapaas = require("@lark-apaas/express-datapaas");
|
|
49
|
+
|
|
50
|
+
// src/middlewares/user-context.ts
|
|
51
|
+
var SUDA_WEB_USER_HEADER_KEY = "x-larkgw-suda-webuser";
|
|
52
|
+
function getWebUserFromHeader(req) {
|
|
53
|
+
const sudaWebUserContent = req.headers[SUDA_WEB_USER_HEADER_KEY];
|
|
54
|
+
if (!sudaWebUserContent) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const sudaWebUserJsonStr = decodeURIComponent(sudaWebUserContent);
|
|
59
|
+
return JSON.parse(sudaWebUserJsonStr);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error("parse suda webuser from header failed, err=%o", err);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
__name(getWebUserFromHeader, "getWebUserFromHeader");
|
|
66
|
+
function userContextMiddleware(req, _res, next) {
|
|
67
|
+
const webUser = getWebUserFromHeader(req);
|
|
68
|
+
req.userContext = {
|
|
69
|
+
userId: webUser?.user_id,
|
|
70
|
+
tenantId: webUser?.tenant_id,
|
|
71
|
+
appId: webUser?.app_id ?? "",
|
|
72
|
+
loginUrl: webUser?.login_url ?? "",
|
|
73
|
+
userType: webUser?.user_type ?? "",
|
|
74
|
+
env: webUser?.env ?? "runtime",
|
|
75
|
+
userName: webUser?.user_name?.zh_cn ?? "",
|
|
76
|
+
userNameEn: webUser?.user_name?.en_us ?? "",
|
|
77
|
+
userNameI18n: webUser?.user_name ?? {},
|
|
78
|
+
isSystemAccount: webUser?.is_system_account ?? false,
|
|
79
|
+
roles: webUser?.roles
|
|
80
|
+
};
|
|
81
|
+
next();
|
|
82
|
+
}
|
|
83
|
+
__name(userContextMiddleware, "userContextMiddleware");
|
|
84
|
+
|
|
85
|
+
// src/middlewares/csrf-token.ts
|
|
86
|
+
var import_crypto = __toESM(require("crypto"), 1);
|
|
87
|
+
function pathToRegex(pattern) {
|
|
88
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
89
|
+
return new RegExp(`^${escaped}$`);
|
|
90
|
+
}
|
|
91
|
+
__name(pathToRegex, "pathToRegex");
|
|
92
|
+
function resolveOptions(options) {
|
|
93
|
+
return {
|
|
94
|
+
cookieKey: options.cookieKey ?? "suda-csrf-token",
|
|
95
|
+
cookieMaxAge: options.cookieMaxAge ?? 1e3 * 60 * 60 * 24 * 30,
|
|
96
|
+
cookiePath: options.cookiePath ?? "/",
|
|
97
|
+
exclude: (options.exclude ?? []).map(pathToRegex)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
__name(resolveOptions, "resolveOptions");
|
|
101
|
+
function genToken() {
|
|
102
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
103
|
+
const randInt64 = BigInt("0x" + import_crypto.default.randomBytes(8).toString("hex")).toString();
|
|
104
|
+
const s = `${randInt64}.${ts}`;
|
|
105
|
+
const sha1 = import_crypto.default.createHash("sha1");
|
|
106
|
+
sha1.update(s);
|
|
107
|
+
return `${sha1.digest("hex")}-${ts}`;
|
|
108
|
+
}
|
|
109
|
+
__name(genToken, "genToken");
|
|
110
|
+
function createCsrfTokenMiddleware(options) {
|
|
111
|
+
const resolved = resolveOptions(options ?? {});
|
|
112
|
+
return /* @__PURE__ */ __name(function csrfTokenMiddleware(req, res, next) {
|
|
113
|
+
const pathname = req.path;
|
|
114
|
+
if (resolved.exclude.some((re) => re.test(pathname))) {
|
|
115
|
+
return next();
|
|
116
|
+
}
|
|
117
|
+
const { cookieKey, cookieMaxAge, cookiePath } = resolved;
|
|
118
|
+
const originToken = req.cookies[cookieKey.toLowerCase()];
|
|
119
|
+
if (originToken) {
|
|
120
|
+
req.csrfToken = originToken;
|
|
121
|
+
} else {
|
|
122
|
+
const token = genToken();
|
|
123
|
+
req.csrfToken = token;
|
|
124
|
+
res.cookie(cookieKey, token, {
|
|
125
|
+
maxAge: cookieMaxAge,
|
|
126
|
+
path: cookiePath,
|
|
127
|
+
httpOnly: false,
|
|
128
|
+
secure: true,
|
|
129
|
+
sameSite: "none",
|
|
130
|
+
partitioned: true
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
next();
|
|
134
|
+
}, "csrfTokenMiddleware");
|
|
135
|
+
}
|
|
136
|
+
__name(createCsrfTokenMiddleware, "createCsrfTokenMiddleware");
|
|
137
|
+
|
|
138
|
+
// src/middlewares/csrf-validation.ts
|
|
139
|
+
function resolveOptions2(options) {
|
|
140
|
+
return {
|
|
141
|
+
headerKey: options.headerKey ?? "x-suda-csrf-token",
|
|
142
|
+
cookieKey: options.cookieKey ?? "suda-csrf-token"
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
__name(resolveOptions2, "resolveOptions");
|
|
146
|
+
function sendForbidden(res, message) {
|
|
147
|
+
res.status(403).send(`Forbidden\uFF0C${message}`);
|
|
148
|
+
}
|
|
149
|
+
__name(sendForbidden, "sendForbidden");
|
|
150
|
+
function createCsrfValidationMiddleware(options) {
|
|
151
|
+
const resolved = resolveOptions2(options ?? {});
|
|
152
|
+
return /* @__PURE__ */ __name(function csrfValidationMiddleware(req, res, next) {
|
|
153
|
+
const { headerKey, cookieKey } = resolved;
|
|
154
|
+
const cookieCsrfToken = req.cookies[cookieKey.toLowerCase()];
|
|
155
|
+
if (!cookieCsrfToken) {
|
|
156
|
+
sendForbidden(res, "csrf token not found in cookie.");
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const headerCsrfToken = req.headers[headerKey.toLowerCase()];
|
|
160
|
+
if (!headerCsrfToken) {
|
|
161
|
+
sendForbidden(res, "csrf token not found in header.");
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (cookieCsrfToken !== headerCsrfToken) {
|
|
165
|
+
sendForbidden(res, "csrf token not match.");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
next();
|
|
169
|
+
}, "csrfValidationMiddleware");
|
|
170
|
+
}
|
|
171
|
+
__name(createCsrfValidationMiddleware, "createCsrfValidationMiddleware");
|
|
172
|
+
|
|
173
|
+
// src/utils/platform-client.ts
|
|
174
|
+
var import_http_client = require("@lark-apaas/http-client");
|
|
175
|
+
var _client = null;
|
|
176
|
+
function createPlatformClient() {
|
|
177
|
+
if (_client) return _client;
|
|
178
|
+
_client = new import_http_client.HttpClient({
|
|
179
|
+
timeout: 5e3,
|
|
180
|
+
platform: {
|
|
181
|
+
enabled: true
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
return _client;
|
|
185
|
+
}
|
|
186
|
+
__name(createPlatformClient, "createPlatformClient");
|
|
187
|
+
function getPlatformClient() {
|
|
188
|
+
if (!_client) {
|
|
189
|
+
throw new Error("Platform client not initialized. Call setup() first.");
|
|
190
|
+
}
|
|
191
|
+
return _client;
|
|
192
|
+
}
|
|
193
|
+
__name(getPlatformClient, "getPlatformClient");
|
|
194
|
+
|
|
195
|
+
// src/utils/safe-escape.ts
|
|
196
|
+
function safeStringify(obj) {
|
|
197
|
+
const json = JSON.stringify(obj);
|
|
198
|
+
return safeEscape(json);
|
|
199
|
+
}
|
|
200
|
+
__name(safeStringify, "safeStringify");
|
|
201
|
+
function safeEscape(s) {
|
|
202
|
+
return s.replace(/[<>&='"\n\r\u2028\u2029]/g, function(c) {
|
|
203
|
+
switch (c.charCodeAt(0)) {
|
|
204
|
+
case 60:
|
|
205
|
+
return "\\u003c";
|
|
206
|
+
case 62:
|
|
207
|
+
return "\\u003e";
|
|
208
|
+
case 38:
|
|
209
|
+
return "\\u0026";
|
|
210
|
+
case 61:
|
|
211
|
+
return "\\u003d";
|
|
212
|
+
case 39:
|
|
213
|
+
return "\\u0027";
|
|
214
|
+
case 34:
|
|
215
|
+
return "\\u0022";
|
|
216
|
+
case 10:
|
|
217
|
+
return "\\u000a";
|
|
218
|
+
case 13:
|
|
219
|
+
return "\\u000d";
|
|
220
|
+
case 8232:
|
|
221
|
+
return "\\u2028";
|
|
222
|
+
case 8233:
|
|
223
|
+
return "\\u2029";
|
|
224
|
+
default:
|
|
225
|
+
return c;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
__name(safeEscape, "safeEscape");
|
|
230
|
+
|
|
231
|
+
// src/middlewares/view-context.ts
|
|
232
|
+
async function getAppInfo(appId) {
|
|
233
|
+
if (!appId) {
|
|
234
|
+
console.warn("appId is empty, skip get app info");
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const client = getPlatformClient();
|
|
239
|
+
const resp = await client.get(`/b/${appId}/get_published_v2`);
|
|
240
|
+
if (resp.status !== 200) {
|
|
241
|
+
throw new Error(`Failed to get app info, status: ${resp.status}`);
|
|
242
|
+
}
|
|
243
|
+
const data = await resp.json();
|
|
244
|
+
if (data.status_code !== "0") {
|
|
245
|
+
throw new Error(`Failed to get app info, status_code: ${data.status_code}`);
|
|
246
|
+
}
|
|
247
|
+
return data.data.app_info ?? {};
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error("Failed to get app info", err);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
__name(getAppInfo, "getAppInfo");
|
|
254
|
+
function mapToWindowEnvironment(input) {
|
|
255
|
+
const value = (input || "").trim().toLowerCase();
|
|
256
|
+
if (value === "boe") return "staging";
|
|
257
|
+
if (value === "pre") return "gray";
|
|
258
|
+
return "online";
|
|
259
|
+
}
|
|
260
|
+
__name(mapToWindowEnvironment, "mapToWindowEnvironment");
|
|
261
|
+
function createViewContextMiddleware() {
|
|
262
|
+
return /* @__PURE__ */ __name(async function viewContextMiddleware(req, res, next) {
|
|
263
|
+
const { userId, tenantId, appId, loginUrl, userType } = req.userContext;
|
|
264
|
+
const csrfToken = req.csrfToken;
|
|
265
|
+
const appInfo = await getAppInfo(appId);
|
|
266
|
+
const environment = mapToWindowEnvironment(process.env.FORCE_FRAMEWORK_ENVIRONMENT);
|
|
267
|
+
req.__platform_data__ = {
|
|
268
|
+
csrfToken: csrfToken ?? "",
|
|
269
|
+
userId: userId ?? "",
|
|
270
|
+
appId: appId ?? "",
|
|
271
|
+
appName: safeEscape(appInfo?.app_name ?? ""),
|
|
272
|
+
appAvatar: appInfo?.app_avatar ?? "",
|
|
273
|
+
appDescription: safeEscape(appInfo?.app_description ?? ""),
|
|
274
|
+
loginUrl: loginUrl ?? "",
|
|
275
|
+
userType: userType ?? "",
|
|
276
|
+
tenantId,
|
|
277
|
+
environment
|
|
278
|
+
};
|
|
279
|
+
res.locals = {
|
|
280
|
+
...res.locals ?? {},
|
|
281
|
+
csrfToken: csrfToken ?? "",
|
|
282
|
+
userId: userId ?? "",
|
|
283
|
+
tenantId: tenantId ?? "",
|
|
284
|
+
loginUrl: loginUrl ?? "",
|
|
285
|
+
userType: userType ?? "",
|
|
286
|
+
appId: appId ?? "",
|
|
287
|
+
appName: safeEscape(appInfo?.app_name ?? ""),
|
|
288
|
+
appAvatar: appInfo?.app_avatar ?? "",
|
|
289
|
+
appDescription: safeEscape(appInfo?.app_description ?? ""),
|
|
290
|
+
environment
|
|
291
|
+
};
|
|
292
|
+
next();
|
|
293
|
+
}, "viewContextMiddleware");
|
|
294
|
+
}
|
|
295
|
+
__name(createViewContextMiddleware, "createViewContextMiddleware");
|
|
296
|
+
|
|
297
|
+
// src/setup.ts
|
|
298
|
+
var db;
|
|
299
|
+
function setup(app, config) {
|
|
300
|
+
const bodyLimit = config?.bodyLimit ?? process.env.BODY_SIZE_LIMIT ?? "1mb";
|
|
301
|
+
app.use(import_express.default.json({
|
|
302
|
+
limit: bodyLimit
|
|
303
|
+
}));
|
|
304
|
+
app.use(import_express.default.urlencoded({
|
|
305
|
+
limit: bodyLimit,
|
|
306
|
+
extended: true
|
|
307
|
+
}));
|
|
308
|
+
app.use((0, import_cookie_parser.default)());
|
|
309
|
+
app.use(userContextMiddleware);
|
|
310
|
+
const connectionString = config?.database?.connectionString ?? process.env.DATABASE_URL ?? "";
|
|
311
|
+
const { db: database, close } = (0, import_express_datapaas.createDatabase)({
|
|
312
|
+
connectionString,
|
|
313
|
+
...config?.database
|
|
314
|
+
});
|
|
315
|
+
db = database;
|
|
316
|
+
app.use((0, import_express_datapaas.createRlsMiddleware)());
|
|
317
|
+
const csrfEnabled = config?.csrf?.enabled ?? true;
|
|
318
|
+
if (csrfEnabled) {
|
|
319
|
+
const csrfRoutes = config?.csrf?.routes ?? [
|
|
320
|
+
"/api/*"
|
|
321
|
+
];
|
|
322
|
+
app.use(createCsrfTokenMiddleware({
|
|
323
|
+
cookieKey: config?.csrf?.cookieKey,
|
|
324
|
+
cookieMaxAge: config?.csrf?.cookieMaxAge,
|
|
325
|
+
cookiePath: config?.csrf?.cookiePath,
|
|
326
|
+
exclude: csrfRoutes
|
|
327
|
+
}));
|
|
328
|
+
app.use(csrfRoutes, createCsrfValidationMiddleware({
|
|
329
|
+
headerKey: config?.csrf?.headerKey,
|
|
330
|
+
cookieKey: config?.csrf?.cookieKey
|
|
331
|
+
}));
|
|
332
|
+
}
|
|
333
|
+
createPlatformClient();
|
|
334
|
+
app.use(createViewContextMiddleware());
|
|
335
|
+
return {
|
|
336
|
+
close
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
__name(setup, "setup");
|
|
340
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
341
|
+
0 && (module.exports = {
|
|
342
|
+
createCsrfTokenMiddleware,
|
|
343
|
+
createCsrfValidationMiddleware,
|
|
344
|
+
createViewContextMiddleware,
|
|
345
|
+
db,
|
|
346
|
+
safeEscape,
|
|
347
|
+
safeStringify,
|
|
348
|
+
setup,
|
|
349
|
+
userContextMiddleware
|
|
350
|
+
});
|
|
351
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/setup.ts","../src/middlewares/user-context.ts","../src/middlewares/csrf-token.ts","../src/middlewares/csrf-validation.ts","../src/utils/platform-client.ts","../src/utils/safe-escape.ts","../src/middlewares/view-context.ts"],"sourcesContent":["import './types/express.d';\n\nexport { setup, db } from './setup';\nexport type { SetupConfig } from './types/config';\nexport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';\n\nexport { safeEscape, safeStringify } from './utils/safe-escape';\n\nexport { userContextMiddleware } from './middlewares/user-context';\nexport { createCsrfTokenMiddleware } from './middlewares/csrf-token';\nexport { createCsrfValidationMiddleware } from './middlewares/csrf-validation';\nexport { createViewContextMiddleware } from './middlewares/view-context';\nexport type { CsrfTokenOptions } from './middlewares/csrf-token';\nexport type { CsrfValidationOptions } from './middlewares/csrf-validation';\n","import express from 'express';\nimport cookieParser from 'cookie-parser';\nimport { createDatabase, createRlsMiddleware } from '@lark-apaas/express-datapaas';\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';\n\nimport type { SetupConfig } from './types/config';\nimport { userContextMiddleware } from './middlewares/user-context';\nimport { createCsrfTokenMiddleware } from './middlewares/csrf-token';\nimport { createCsrfValidationMiddleware } from './middlewares/csrf-validation';\nimport { createViewContextMiddleware } from './middlewares/view-context';\nimport { createPlatformClient } from './utils/platform-client';\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let db: PostgresJsDatabase;\n\nexport function setup(app: express.Application, config?: SetupConfig): { close: () => Promise<void> } {\n const bodyLimit = config?.bodyLimit ?? process.env.BODY_SIZE_LIMIT ?? '1mb';\n\n // 1. Body parsers\n app.use(express.json({ limit: bodyLimit }));\n app.use(express.urlencoded({ limit: bodyLimit, extended: true }));\n\n // 2. Cookie parser\n app.use(cookieParser());\n\n // 3. UserContext (all routes)\n app.use(userContextMiddleware);\n\n // 4. Database + RLS\n const connectionString = config?.database?.connectionString ?? process.env.DATABASE_URL ?? '';\n const { db: database, close } = createDatabase({\n connectionString,\n ...config?.database,\n });\n db = database;\n app.use(createRlsMiddleware());\n\n // 5. CSRF\n const csrfEnabled = config?.csrf?.enabled ?? true;\n if (csrfEnabled) {\n const csrfRoutes = config?.csrf?.routes ?? ['/api/*'];\n\n // 页面路由生成 token(排除 API 路由),为后续 API 请求准备 cookie\n app.use(createCsrfTokenMiddleware({\n cookieKey: config?.csrf?.cookieKey,\n cookieMaxAge: config?.csrf?.cookieMaxAge,\n cookiePath: config?.csrf?.cookiePath,\n exclude: csrfRoutes,\n }));\n\n // API 路由验证 token(cookie vs header)\n app.use(csrfRoutes, createCsrfValidationMiddleware({\n headerKey: config?.csrf?.headerKey,\n cookieKey: config?.csrf?.cookieKey,\n }));\n }\n\n // 6. Platform client + ViewContext\n createPlatformClient();\n app.use(createViewContextMiddleware());\n\n return { close };\n}\n","import type { Request, Response, NextFunction } from 'express';\n\nconst SUDA_WEB_USER_HEADER_KEY = 'x-larkgw-suda-webuser';\n\ninterface SudaWebUser {\n user_id: string;\n tenant_id: number;\n app_id: string;\n login_url: string;\n user_type: string;\n env: 'preview' | 'runtime';\n user_name: Record<string, string>;\n is_system_account?: boolean;\n roles?: string[];\n}\n\nfunction getWebUserFromHeader(req: Request): SudaWebUser | null {\n const sudaWebUserContent = req.headers[SUDA_WEB_USER_HEADER_KEY] as string | undefined;\n if (!sudaWebUserContent) {\n return null;\n }\n try {\n const sudaWebUserJsonStr = decodeURIComponent(sudaWebUserContent);\n return JSON.parse(sudaWebUserJsonStr) as SudaWebUser;\n } catch (err) {\n console.error('parse suda webuser from header failed, err=%o', err);\n return null;\n }\n}\n\nexport function userContextMiddleware(req: Request, _res: Response, next: NextFunction) {\n const webUser = getWebUserFromHeader(req);\n req.userContext = {\n userId: webUser?.user_id,\n tenantId: webUser?.tenant_id,\n appId: webUser?.app_id ?? '',\n loginUrl: webUser?.login_url ?? '',\n userType: webUser?.user_type ?? '',\n env: webUser?.env ?? 'runtime',\n userName: webUser?.user_name?.zh_cn ?? '',\n userNameEn: webUser?.user_name?.en_us ?? '',\n userNameI18n: webUser?.user_name ?? {},\n isSystemAccount: webUser?.is_system_account ?? false,\n roles: webUser?.roles,\n };\n next();\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport crypto from 'crypto';\n\nexport interface CsrfTokenOptions {\n cookieKey?: string;\n cookieMaxAge?: number;\n cookiePath?: string;\n /** 排除的路由模式,匹配到则跳过 token 生成 */\n exclude?: string[];\n}\n\ninterface ResolvedCsrfTokenOptions {\n cookieKey: string;\n cookieMaxAge: number;\n cookiePath: string;\n exclude: RegExp[];\n}\n\nfunction pathToRegex(pattern: string): RegExp {\n // 将 Express 风格的路径模式转为正则:/api/* → /^\\/api\\/.*$/\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`);\n}\n\nfunction resolveOptions(options: CsrfTokenOptions): ResolvedCsrfTokenOptions {\n return {\n cookieKey: options.cookieKey ?? 'suda-csrf-token',\n cookieMaxAge: options.cookieMaxAge ?? 1000 * 60 * 60 * 24 * 30,\n cookiePath: options.cookiePath ?? '/',\n exclude: (options.exclude ?? []).map(pathToRegex),\n };\n}\n\nfunction genToken(): string {\n const ts = Math.floor(Date.now() / 1000);\n const randInt64 = BigInt('0x' + crypto.randomBytes(8).toString('hex')).toString();\n const s = `${randInt64}.${ts}`;\n const sha1 = crypto.createHash('sha1');\n sha1.update(s);\n return `${sha1.digest('hex')}-${ts}`;\n}\n\nexport function createCsrfTokenMiddleware(options?: CsrfTokenOptions) {\n const resolved = resolveOptions(options ?? {});\n\n return function csrfTokenMiddleware(req: Request, res: Response, next: NextFunction) {\n // 匹配排除路由则跳过\n const pathname = req.path;\n if (resolved.exclude.some(re => re.test(pathname))) {\n return next();\n }\n\n const { cookieKey, cookieMaxAge, cookiePath } = resolved;\n const originToken = req.cookies[cookieKey.toLowerCase()];\n\n if (originToken) {\n req.csrfToken = originToken;\n } else {\n const token = genToken();\n req.csrfToken = token;\n res.cookie(cookieKey, token, {\n maxAge: cookieMaxAge,\n path: cookiePath,\n httpOnly: false,\n secure: true,\n sameSite: 'none',\n partitioned: true,\n });\n }\n next();\n };\n}\n","import type { Request, Response, NextFunction } from 'express';\n\nexport interface CsrfValidationOptions {\n headerKey?: string;\n cookieKey?: string;\n}\n\ninterface ResolvedCsrfValidationOptions {\n headerKey: string;\n cookieKey: string;\n}\n\nfunction resolveOptions(options: CsrfValidationOptions): ResolvedCsrfValidationOptions {\n return {\n headerKey: options.headerKey ?? 'x-suda-csrf-token',\n cookieKey: options.cookieKey ?? 'suda-csrf-token',\n };\n}\n\nfunction sendForbidden(res: Response, message: string) {\n res.status(403).send(`Forbidden,${message}`);\n}\n\nexport function createCsrfValidationMiddleware(options?: CsrfValidationOptions) {\n const resolved = resolveOptions(options ?? {});\n\n return function csrfValidationMiddleware(req: Request, res: Response, next: NextFunction) {\n const { headerKey, cookieKey } = resolved;\n\n const cookieCsrfToken = req.cookies[cookieKey.toLowerCase()];\n if (!cookieCsrfToken) {\n sendForbidden(res, 'csrf token not found in cookie.');\n return;\n }\n\n const headerCsrfToken = req.headers[headerKey.toLowerCase()];\n if (!headerCsrfToken) {\n sendForbidden(res, 'csrf token not found in header.');\n return;\n }\n\n if (cookieCsrfToken !== headerCsrfToken) {\n sendForbidden(res, 'csrf token not match.');\n return;\n }\n\n next();\n };\n}\n","import { HttpClient } from '@lark-apaas/http-client';\n\nlet _client: HttpClient | null = null;\n\nexport function createPlatformClient(): HttpClient {\n if (_client) return _client;\n _client = new HttpClient({\n timeout: 5000,\n platform: { enabled: true },\n });\n return _client;\n}\n\nexport function getPlatformClient(): HttpClient {\n if (!_client) {\n throw new Error('Platform client not initialized. Call setup() first.');\n }\n return _client;\n}\n","/**\n * 安全转义的 Json Stringify,模版渲染的 JSON 对象需要调用 safeStringify,而不是用原生的 JSON.stringify。\n * 抄 gson 的实现: https://github.com/google/gson/blob/49ddab9eeb6cbac686711deca6001d40e8d8500d/gson/src/main/java/com/google/gson/stream/JsonWriter.java\n * 将 json 的敏感字符转换为 unicode, 避免 xss\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function safeStringify(obj: any) {\n const json = JSON.stringify(obj);\n return safeEscape(json);\n}\n\nenum EscapedChar {\n LT = 0x3c, // <\n GT = 0x3e, // >\n AMP = 0x26, // &\n APO = 0x27, // '\n QUOT = 0x22, // \"\n EQUAL = 0x3d, // =\n LF = 0x0a, // \\n\n CR = 0x0d, // \\r\n LS = 0x2028, // 行分隔符\n PS = 0x2029, // 段落分隔符\n}\n\nexport function safeEscape(s: string) {\n return s.replace(/[<>&='\"\\n\\r\\u2028\\u2029]/g, function (c) {\n switch (c.charCodeAt(0)) {\n case EscapedChar.LT:\n return '\\\\u003c';\n case EscapedChar.GT:\n return '\\\\u003e';\n case EscapedChar.AMP:\n return '\\\\u0026';\n case EscapedChar.EQUAL:\n return '\\\\u003d';\n case EscapedChar.APO:\n return '\\\\u0027';\n case EscapedChar.QUOT:\n return '\\\\u0022';\n case EscapedChar.LF:\n return '\\\\u000a';\n case EscapedChar.CR:\n return '\\\\u000d';\n case EscapedChar.LS:\n return '\\\\u2028';\n case EscapedChar.PS:\n return '\\\\u2029';\n default:\n return c;\n }\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { getPlatformClient } from '../utils/platform-client';\nimport { safeEscape } from '../utils/safe-escape';\n\ninterface GetAppInfoResponse {\n status_code: string;\n data: {\n status_code: number;\n app_info: {\n app_name: string;\n app_description: string;\n app_avatar: string;\n };\n app_runtime_extra: {\n url: string;\n bucket: string;\n };\n };\n}\n\nasync function getAppInfo(appId?: string) {\n if (!appId) {\n console.warn('appId is empty, skip get app info');\n return null;\n }\n try {\n const client = getPlatformClient();\n const resp = await client.get(`/b/${appId}/get_published_v2`);\n if (resp.status !== 200) {\n throw new Error(`Failed to get app info, status: ${resp.status}`);\n }\n const data = (await resp.json()) as GetAppInfoResponse;\n if (data.status_code !== '0') {\n throw new Error(`Failed to get app info, status_code: ${data.status_code}`);\n }\n return data.data.app_info ?? {};\n } catch (err) {\n console.error('Failed to get app info', err);\n return null;\n }\n}\n\nfunction mapToWindowEnvironment(input?: string): string {\n const value = (input || '').trim().toLowerCase();\n if (value === 'boe') return 'staging';\n if (value === 'pre') return 'gray';\n return 'online';\n}\n\nexport function createViewContextMiddleware() {\n return async function viewContextMiddleware(req: Request, res: Response, next: NextFunction) {\n const { userId, tenantId, appId, loginUrl, userType } = req.userContext;\n const csrfToken = req.csrfToken;\n const appInfo = await getAppInfo(appId);\n const environment = mapToWindowEnvironment(process.env.FORCE_FRAMEWORK_ENVIRONMENT);\n\n req.__platform_data__ = {\n csrfToken: csrfToken ?? '',\n userId: userId ?? '',\n appId: appId ?? '',\n appName: safeEscape(appInfo?.app_name ?? ''),\n appAvatar: appInfo?.app_avatar ?? '',\n appDescription: safeEscape(appInfo?.app_description ?? ''),\n loginUrl: loginUrl ?? '',\n userType: userType ?? '',\n tenantId,\n environment,\n };\n\n res.locals = {\n ...(res.locals ?? {}),\n csrfToken: csrfToken ?? '',\n userId: userId ?? '',\n tenantId: tenantId ?? '',\n loginUrl: loginUrl ?? '',\n userType: userType ?? '',\n appId: appId ?? '',\n appName: safeEscape(appInfo?.app_name ?? ''),\n appAvatar: appInfo?.app_avatar ?? '',\n appDescription: safeEscape(appInfo?.app_description ?? ''),\n environment,\n };\n\n next();\n };\n}\n"],"mappings":"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","names":["SUDA_WEB_USER_HEADER_KEY","getWebUserFromHeader","req","sudaWebUserContent","headers","sudaWebUserJsonStr","decodeURIComponent","JSON","parse","err","console","error","userContextMiddleware","_res","next","webUser","userContext","userId","user_id","tenantId","tenant_id","appId","app_id","loginUrl","login_url","userType","user_type","env","userName","user_name","zh_cn","userNameEn","en_us","userNameI18n","isSystemAccount","is_system_account","roles","pathToRegex","pattern","escaped","replace","RegExp","resolveOptions","options","cookieKey","cookieMaxAge","cookiePath","exclude","map","genToken","ts","Math","floor","Date","now","randInt64","BigInt","crypto","randomBytes","toString","s","sha1","createHash","update","digest","createCsrfTokenMiddleware","resolved","csrfTokenMiddleware","req","res","next","pathname","path","some","re","test","originToken","cookies","toLowerCase","csrfToken","token","cookie","maxAge","httpOnly","secure","sameSite","partitioned","resolveOptions","options","headerKey","cookieKey","sendForbidden","res","message","status","send","createCsrfValidationMiddleware","resolved","csrfValidationMiddleware","req","next","cookieCsrfToken","cookies","toLowerCase","headerCsrfToken","headers","_client","createPlatformClient","HttpClient","timeout","platform","enabled","getPlatformClient","Error","safeStringify","obj","json","JSON","stringify","safeEscape","safeEscape","s","replace","c","charCodeAt","getAppInfo","appId","console","warn","client","getPlatformClient","resp","get","status","Error","data","json","status_code","app_info","err","error","mapToWindowEnvironment","input","value","trim","toLowerCase","createViewContextMiddleware","viewContextMiddleware","req","res","next","userId","tenantId","loginUrl","userType","userContext","csrfToken","appInfo","environment","process","env","FORCE_FRAMEWORK_ENVIRONMENT","__platform_data__","appName","safeEscape","app_name","appAvatar","app_avatar","appDescription","app_description","locals","db","setup","app","config","bodyLimit","process","env","BODY_SIZE_LIMIT","use","express","json","limit","urlencoded","extended","cookieParser","userContextMiddleware","connectionString","database","DATABASE_URL","close","createDatabase","createRlsMiddleware","csrfEnabled","csrf","enabled","csrfRoutes","routes","createCsrfTokenMiddleware","cookieKey","cookieMaxAge","cookiePath","exclude","createCsrfValidationMiddleware","headerKey","createPlatformClient","createViewContextMiddleware"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
3
|
+
export { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
4
|
+
import { CreateDatabaseConfig } from '@lark-apaas/express-datapaas';
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
namespace Express {
|
|
8
|
+
interface Request {
|
|
9
|
+
__platform_data__?: {
|
|
10
|
+
userId?: string;
|
|
11
|
+
tenantId?: number;
|
|
12
|
+
csrfToken?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
userContext: {
|
|
16
|
+
userId?: string;
|
|
17
|
+
tenantId?: number;
|
|
18
|
+
appId?: string;
|
|
19
|
+
loginUrl?: string;
|
|
20
|
+
userType?: string;
|
|
21
|
+
env?: 'preview' | 'runtime';
|
|
22
|
+
userName?: string;
|
|
23
|
+
userNameEn?: string;
|
|
24
|
+
userNameI18n?: Record<string, string>;
|
|
25
|
+
isSystemAccount?: boolean;
|
|
26
|
+
roles?: string[];
|
|
27
|
+
};
|
|
28
|
+
csrfToken?: string;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SetupConfig {
|
|
34
|
+
database?: Partial<CreateDatabaseConfig>;
|
|
35
|
+
csrf?: {
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
cookieKey?: string;
|
|
38
|
+
headerKey?: string;
|
|
39
|
+
cookieMaxAge?: number;
|
|
40
|
+
cookiePath?: string;
|
|
41
|
+
routes?: string[];
|
|
42
|
+
};
|
|
43
|
+
bodyLimit?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare let db: PostgresJsDatabase;
|
|
47
|
+
declare function setup(app: express.Application, config?: SetupConfig): {
|
|
48
|
+
close: () => Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 安全转义的 Json Stringify,模版渲染的 JSON 对象需要调用 safeStringify,而不是用原生的 JSON.stringify。
|
|
53
|
+
* 抄 gson 的实现: https://github.com/google/gson/blob/49ddab9eeb6cbac686711deca6001d40e8d8500d/gson/src/main/java/com/google/gson/stream/JsonWriter.java
|
|
54
|
+
* 将 json 的敏感字符转换为 unicode, 避免 xss
|
|
55
|
+
*/
|
|
56
|
+
declare function safeStringify(obj: any): string;
|
|
57
|
+
declare function safeEscape(s: string): string;
|
|
58
|
+
|
|
59
|
+
declare function userContextMiddleware(req: Request, _res: Response, next: NextFunction): void;
|
|
60
|
+
|
|
61
|
+
interface CsrfTokenOptions {
|
|
62
|
+
cookieKey?: string;
|
|
63
|
+
cookieMaxAge?: number;
|
|
64
|
+
cookiePath?: string;
|
|
65
|
+
/** 排除的路由模式,匹配到则跳过 token 生成 */
|
|
66
|
+
exclude?: string[];
|
|
67
|
+
}
|
|
68
|
+
declare function createCsrfTokenMiddleware(options?: CsrfTokenOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
69
|
+
|
|
70
|
+
interface CsrfValidationOptions {
|
|
71
|
+
headerKey?: string;
|
|
72
|
+
cookieKey?: string;
|
|
73
|
+
}
|
|
74
|
+
declare function createCsrfValidationMiddleware(options?: CsrfValidationOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
75
|
+
|
|
76
|
+
declare function createViewContextMiddleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
77
|
+
|
|
78
|
+
export { type CsrfTokenOptions, type CsrfValidationOptions, type SetupConfig, createCsrfTokenMiddleware, createCsrfValidationMiddleware, createViewContextMiddleware, db, safeEscape, safeStringify, setup, userContextMiddleware };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import express, { Request, Response, NextFunction } from 'express';
|
|
2
|
+
import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
3
|
+
export { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
|
|
4
|
+
import { CreateDatabaseConfig } from '@lark-apaas/express-datapaas';
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
namespace Express {
|
|
8
|
+
interface Request {
|
|
9
|
+
__platform_data__?: {
|
|
10
|
+
userId?: string;
|
|
11
|
+
tenantId?: number;
|
|
12
|
+
csrfToken?: string;
|
|
13
|
+
[key: string]: unknown;
|
|
14
|
+
};
|
|
15
|
+
userContext: {
|
|
16
|
+
userId?: string;
|
|
17
|
+
tenantId?: number;
|
|
18
|
+
appId?: string;
|
|
19
|
+
loginUrl?: string;
|
|
20
|
+
userType?: string;
|
|
21
|
+
env?: 'preview' | 'runtime';
|
|
22
|
+
userName?: string;
|
|
23
|
+
userNameEn?: string;
|
|
24
|
+
userNameI18n?: Record<string, string>;
|
|
25
|
+
isSystemAccount?: boolean;
|
|
26
|
+
roles?: string[];
|
|
27
|
+
};
|
|
28
|
+
csrfToken?: string;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface SetupConfig {
|
|
34
|
+
database?: Partial<CreateDatabaseConfig>;
|
|
35
|
+
csrf?: {
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
cookieKey?: string;
|
|
38
|
+
headerKey?: string;
|
|
39
|
+
cookieMaxAge?: number;
|
|
40
|
+
cookiePath?: string;
|
|
41
|
+
routes?: string[];
|
|
42
|
+
};
|
|
43
|
+
bodyLimit?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
declare let db: PostgresJsDatabase;
|
|
47
|
+
declare function setup(app: express.Application, config?: SetupConfig): {
|
|
48
|
+
close: () => Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 安全转义的 Json Stringify,模版渲染的 JSON 对象需要调用 safeStringify,而不是用原生的 JSON.stringify。
|
|
53
|
+
* 抄 gson 的实现: https://github.com/google/gson/blob/49ddab9eeb6cbac686711deca6001d40e8d8500d/gson/src/main/java/com/google/gson/stream/JsonWriter.java
|
|
54
|
+
* 将 json 的敏感字符转换为 unicode, 避免 xss
|
|
55
|
+
*/
|
|
56
|
+
declare function safeStringify(obj: any): string;
|
|
57
|
+
declare function safeEscape(s: string): string;
|
|
58
|
+
|
|
59
|
+
declare function userContextMiddleware(req: Request, _res: Response, next: NextFunction): void;
|
|
60
|
+
|
|
61
|
+
interface CsrfTokenOptions {
|
|
62
|
+
cookieKey?: string;
|
|
63
|
+
cookieMaxAge?: number;
|
|
64
|
+
cookiePath?: string;
|
|
65
|
+
/** 排除的路由模式,匹配到则跳过 token 生成 */
|
|
66
|
+
exclude?: string[];
|
|
67
|
+
}
|
|
68
|
+
declare function createCsrfTokenMiddleware(options?: CsrfTokenOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
69
|
+
|
|
70
|
+
interface CsrfValidationOptions {
|
|
71
|
+
headerKey?: string;
|
|
72
|
+
cookieKey?: string;
|
|
73
|
+
}
|
|
74
|
+
declare function createCsrfValidationMiddleware(options?: CsrfValidationOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
75
|
+
|
|
76
|
+
declare function createViewContextMiddleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
|
77
|
+
|
|
78
|
+
export { type CsrfTokenOptions, type CsrfValidationOptions, type SetupConfig, createCsrfTokenMiddleware, createCsrfValidationMiddleware, createViewContextMiddleware, db, safeEscape, safeStringify, setup, userContextMiddleware };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/setup.ts
|
|
5
|
+
import express from "express";
|
|
6
|
+
import cookieParser from "cookie-parser";
|
|
7
|
+
import { createDatabase, createRlsMiddleware } from "@lark-apaas/express-datapaas";
|
|
8
|
+
|
|
9
|
+
// src/middlewares/user-context.ts
|
|
10
|
+
var SUDA_WEB_USER_HEADER_KEY = "x-larkgw-suda-webuser";
|
|
11
|
+
function getWebUserFromHeader(req) {
|
|
12
|
+
const sudaWebUserContent = req.headers[SUDA_WEB_USER_HEADER_KEY];
|
|
13
|
+
if (!sudaWebUserContent) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const sudaWebUserJsonStr = decodeURIComponent(sudaWebUserContent);
|
|
18
|
+
return JSON.parse(sudaWebUserJsonStr);
|
|
19
|
+
} catch (err) {
|
|
20
|
+
console.error("parse suda webuser from header failed, err=%o", err);
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
__name(getWebUserFromHeader, "getWebUserFromHeader");
|
|
25
|
+
function userContextMiddleware(req, _res, next) {
|
|
26
|
+
const webUser = getWebUserFromHeader(req);
|
|
27
|
+
req.userContext = {
|
|
28
|
+
userId: webUser?.user_id,
|
|
29
|
+
tenantId: webUser?.tenant_id,
|
|
30
|
+
appId: webUser?.app_id ?? "",
|
|
31
|
+
loginUrl: webUser?.login_url ?? "",
|
|
32
|
+
userType: webUser?.user_type ?? "",
|
|
33
|
+
env: webUser?.env ?? "runtime",
|
|
34
|
+
userName: webUser?.user_name?.zh_cn ?? "",
|
|
35
|
+
userNameEn: webUser?.user_name?.en_us ?? "",
|
|
36
|
+
userNameI18n: webUser?.user_name ?? {},
|
|
37
|
+
isSystemAccount: webUser?.is_system_account ?? false,
|
|
38
|
+
roles: webUser?.roles
|
|
39
|
+
};
|
|
40
|
+
next();
|
|
41
|
+
}
|
|
42
|
+
__name(userContextMiddleware, "userContextMiddleware");
|
|
43
|
+
|
|
44
|
+
// src/middlewares/csrf-token.ts
|
|
45
|
+
import crypto from "crypto";
|
|
46
|
+
function pathToRegex(pattern) {
|
|
47
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
48
|
+
return new RegExp(`^${escaped}$`);
|
|
49
|
+
}
|
|
50
|
+
__name(pathToRegex, "pathToRegex");
|
|
51
|
+
function resolveOptions(options) {
|
|
52
|
+
return {
|
|
53
|
+
cookieKey: options.cookieKey ?? "suda-csrf-token",
|
|
54
|
+
cookieMaxAge: options.cookieMaxAge ?? 1e3 * 60 * 60 * 24 * 30,
|
|
55
|
+
cookiePath: options.cookiePath ?? "/",
|
|
56
|
+
exclude: (options.exclude ?? []).map(pathToRegex)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
__name(resolveOptions, "resolveOptions");
|
|
60
|
+
function genToken() {
|
|
61
|
+
const ts = Math.floor(Date.now() / 1e3);
|
|
62
|
+
const randInt64 = BigInt("0x" + crypto.randomBytes(8).toString("hex")).toString();
|
|
63
|
+
const s = `${randInt64}.${ts}`;
|
|
64
|
+
const sha1 = crypto.createHash("sha1");
|
|
65
|
+
sha1.update(s);
|
|
66
|
+
return `${sha1.digest("hex")}-${ts}`;
|
|
67
|
+
}
|
|
68
|
+
__name(genToken, "genToken");
|
|
69
|
+
function createCsrfTokenMiddleware(options) {
|
|
70
|
+
const resolved = resolveOptions(options ?? {});
|
|
71
|
+
return /* @__PURE__ */ __name(function csrfTokenMiddleware(req, res, next) {
|
|
72
|
+
const pathname = req.path;
|
|
73
|
+
if (resolved.exclude.some((re) => re.test(pathname))) {
|
|
74
|
+
return next();
|
|
75
|
+
}
|
|
76
|
+
const { cookieKey, cookieMaxAge, cookiePath } = resolved;
|
|
77
|
+
const originToken = req.cookies[cookieKey.toLowerCase()];
|
|
78
|
+
if (originToken) {
|
|
79
|
+
req.csrfToken = originToken;
|
|
80
|
+
} else {
|
|
81
|
+
const token = genToken();
|
|
82
|
+
req.csrfToken = token;
|
|
83
|
+
res.cookie(cookieKey, token, {
|
|
84
|
+
maxAge: cookieMaxAge,
|
|
85
|
+
path: cookiePath,
|
|
86
|
+
httpOnly: false,
|
|
87
|
+
secure: true,
|
|
88
|
+
sameSite: "none",
|
|
89
|
+
partitioned: true
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
next();
|
|
93
|
+
}, "csrfTokenMiddleware");
|
|
94
|
+
}
|
|
95
|
+
__name(createCsrfTokenMiddleware, "createCsrfTokenMiddleware");
|
|
96
|
+
|
|
97
|
+
// src/middlewares/csrf-validation.ts
|
|
98
|
+
function resolveOptions2(options) {
|
|
99
|
+
return {
|
|
100
|
+
headerKey: options.headerKey ?? "x-suda-csrf-token",
|
|
101
|
+
cookieKey: options.cookieKey ?? "suda-csrf-token"
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
__name(resolveOptions2, "resolveOptions");
|
|
105
|
+
function sendForbidden(res, message) {
|
|
106
|
+
res.status(403).send(`Forbidden\uFF0C${message}`);
|
|
107
|
+
}
|
|
108
|
+
__name(sendForbidden, "sendForbidden");
|
|
109
|
+
function createCsrfValidationMiddleware(options) {
|
|
110
|
+
const resolved = resolveOptions2(options ?? {});
|
|
111
|
+
return /* @__PURE__ */ __name(function csrfValidationMiddleware(req, res, next) {
|
|
112
|
+
const { headerKey, cookieKey } = resolved;
|
|
113
|
+
const cookieCsrfToken = req.cookies[cookieKey.toLowerCase()];
|
|
114
|
+
if (!cookieCsrfToken) {
|
|
115
|
+
sendForbidden(res, "csrf token not found in cookie.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const headerCsrfToken = req.headers[headerKey.toLowerCase()];
|
|
119
|
+
if (!headerCsrfToken) {
|
|
120
|
+
sendForbidden(res, "csrf token not found in header.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (cookieCsrfToken !== headerCsrfToken) {
|
|
124
|
+
sendForbidden(res, "csrf token not match.");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
next();
|
|
128
|
+
}, "csrfValidationMiddleware");
|
|
129
|
+
}
|
|
130
|
+
__name(createCsrfValidationMiddleware, "createCsrfValidationMiddleware");
|
|
131
|
+
|
|
132
|
+
// src/utils/platform-client.ts
|
|
133
|
+
import { HttpClient } from "@lark-apaas/http-client";
|
|
134
|
+
var _client = null;
|
|
135
|
+
function createPlatformClient() {
|
|
136
|
+
if (_client) return _client;
|
|
137
|
+
_client = new HttpClient({
|
|
138
|
+
timeout: 5e3,
|
|
139
|
+
platform: {
|
|
140
|
+
enabled: true
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
return _client;
|
|
144
|
+
}
|
|
145
|
+
__name(createPlatformClient, "createPlatformClient");
|
|
146
|
+
function getPlatformClient() {
|
|
147
|
+
if (!_client) {
|
|
148
|
+
throw new Error("Platform client not initialized. Call setup() first.");
|
|
149
|
+
}
|
|
150
|
+
return _client;
|
|
151
|
+
}
|
|
152
|
+
__name(getPlatformClient, "getPlatformClient");
|
|
153
|
+
|
|
154
|
+
// src/utils/safe-escape.ts
|
|
155
|
+
function safeStringify(obj) {
|
|
156
|
+
const json = JSON.stringify(obj);
|
|
157
|
+
return safeEscape(json);
|
|
158
|
+
}
|
|
159
|
+
__name(safeStringify, "safeStringify");
|
|
160
|
+
function safeEscape(s) {
|
|
161
|
+
return s.replace(/[<>&='"\n\r\u2028\u2029]/g, function(c) {
|
|
162
|
+
switch (c.charCodeAt(0)) {
|
|
163
|
+
case 60:
|
|
164
|
+
return "\\u003c";
|
|
165
|
+
case 62:
|
|
166
|
+
return "\\u003e";
|
|
167
|
+
case 38:
|
|
168
|
+
return "\\u0026";
|
|
169
|
+
case 61:
|
|
170
|
+
return "\\u003d";
|
|
171
|
+
case 39:
|
|
172
|
+
return "\\u0027";
|
|
173
|
+
case 34:
|
|
174
|
+
return "\\u0022";
|
|
175
|
+
case 10:
|
|
176
|
+
return "\\u000a";
|
|
177
|
+
case 13:
|
|
178
|
+
return "\\u000d";
|
|
179
|
+
case 8232:
|
|
180
|
+
return "\\u2028";
|
|
181
|
+
case 8233:
|
|
182
|
+
return "\\u2029";
|
|
183
|
+
default:
|
|
184
|
+
return c;
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
__name(safeEscape, "safeEscape");
|
|
189
|
+
|
|
190
|
+
// src/middlewares/view-context.ts
|
|
191
|
+
async function getAppInfo(appId) {
|
|
192
|
+
if (!appId) {
|
|
193
|
+
console.warn("appId is empty, skip get app info");
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const client = getPlatformClient();
|
|
198
|
+
const resp = await client.get(`/b/${appId}/get_published_v2`);
|
|
199
|
+
if (resp.status !== 200) {
|
|
200
|
+
throw new Error(`Failed to get app info, status: ${resp.status}`);
|
|
201
|
+
}
|
|
202
|
+
const data = await resp.json();
|
|
203
|
+
if (data.status_code !== "0") {
|
|
204
|
+
throw new Error(`Failed to get app info, status_code: ${data.status_code}`);
|
|
205
|
+
}
|
|
206
|
+
return data.data.app_info ?? {};
|
|
207
|
+
} catch (err) {
|
|
208
|
+
console.error("Failed to get app info", err);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
__name(getAppInfo, "getAppInfo");
|
|
213
|
+
function mapToWindowEnvironment(input) {
|
|
214
|
+
const value = (input || "").trim().toLowerCase();
|
|
215
|
+
if (value === "boe") return "staging";
|
|
216
|
+
if (value === "pre") return "gray";
|
|
217
|
+
return "online";
|
|
218
|
+
}
|
|
219
|
+
__name(mapToWindowEnvironment, "mapToWindowEnvironment");
|
|
220
|
+
function createViewContextMiddleware() {
|
|
221
|
+
return /* @__PURE__ */ __name(async function viewContextMiddleware(req, res, next) {
|
|
222
|
+
const { userId, tenantId, appId, loginUrl, userType } = req.userContext;
|
|
223
|
+
const csrfToken = req.csrfToken;
|
|
224
|
+
const appInfo = await getAppInfo(appId);
|
|
225
|
+
const environment = mapToWindowEnvironment(process.env.FORCE_FRAMEWORK_ENVIRONMENT);
|
|
226
|
+
req.__platform_data__ = {
|
|
227
|
+
csrfToken: csrfToken ?? "",
|
|
228
|
+
userId: userId ?? "",
|
|
229
|
+
appId: appId ?? "",
|
|
230
|
+
appName: safeEscape(appInfo?.app_name ?? ""),
|
|
231
|
+
appAvatar: appInfo?.app_avatar ?? "",
|
|
232
|
+
appDescription: safeEscape(appInfo?.app_description ?? ""),
|
|
233
|
+
loginUrl: loginUrl ?? "",
|
|
234
|
+
userType: userType ?? "",
|
|
235
|
+
tenantId,
|
|
236
|
+
environment
|
|
237
|
+
};
|
|
238
|
+
res.locals = {
|
|
239
|
+
...res.locals ?? {},
|
|
240
|
+
csrfToken: csrfToken ?? "",
|
|
241
|
+
userId: userId ?? "",
|
|
242
|
+
tenantId: tenantId ?? "",
|
|
243
|
+
loginUrl: loginUrl ?? "",
|
|
244
|
+
userType: userType ?? "",
|
|
245
|
+
appId: appId ?? "",
|
|
246
|
+
appName: safeEscape(appInfo?.app_name ?? ""),
|
|
247
|
+
appAvatar: appInfo?.app_avatar ?? "",
|
|
248
|
+
appDescription: safeEscape(appInfo?.app_description ?? ""),
|
|
249
|
+
environment
|
|
250
|
+
};
|
|
251
|
+
next();
|
|
252
|
+
}, "viewContextMiddleware");
|
|
253
|
+
}
|
|
254
|
+
__name(createViewContextMiddleware, "createViewContextMiddleware");
|
|
255
|
+
|
|
256
|
+
// src/setup.ts
|
|
257
|
+
var db;
|
|
258
|
+
function setup(app, config) {
|
|
259
|
+
const bodyLimit = config?.bodyLimit ?? process.env.BODY_SIZE_LIMIT ?? "1mb";
|
|
260
|
+
app.use(express.json({
|
|
261
|
+
limit: bodyLimit
|
|
262
|
+
}));
|
|
263
|
+
app.use(express.urlencoded({
|
|
264
|
+
limit: bodyLimit,
|
|
265
|
+
extended: true
|
|
266
|
+
}));
|
|
267
|
+
app.use(cookieParser());
|
|
268
|
+
app.use(userContextMiddleware);
|
|
269
|
+
const connectionString = config?.database?.connectionString ?? process.env.DATABASE_URL ?? "";
|
|
270
|
+
const { db: database, close } = createDatabase({
|
|
271
|
+
connectionString,
|
|
272
|
+
...config?.database
|
|
273
|
+
});
|
|
274
|
+
db = database;
|
|
275
|
+
app.use(createRlsMiddleware());
|
|
276
|
+
const csrfEnabled = config?.csrf?.enabled ?? true;
|
|
277
|
+
if (csrfEnabled) {
|
|
278
|
+
const csrfRoutes = config?.csrf?.routes ?? [
|
|
279
|
+
"/api/*"
|
|
280
|
+
];
|
|
281
|
+
app.use(createCsrfTokenMiddleware({
|
|
282
|
+
cookieKey: config?.csrf?.cookieKey,
|
|
283
|
+
cookieMaxAge: config?.csrf?.cookieMaxAge,
|
|
284
|
+
cookiePath: config?.csrf?.cookiePath,
|
|
285
|
+
exclude: csrfRoutes
|
|
286
|
+
}));
|
|
287
|
+
app.use(csrfRoutes, createCsrfValidationMiddleware({
|
|
288
|
+
headerKey: config?.csrf?.headerKey,
|
|
289
|
+
cookieKey: config?.csrf?.cookieKey
|
|
290
|
+
}));
|
|
291
|
+
}
|
|
292
|
+
createPlatformClient();
|
|
293
|
+
app.use(createViewContextMiddleware());
|
|
294
|
+
return {
|
|
295
|
+
close
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
__name(setup, "setup");
|
|
299
|
+
export {
|
|
300
|
+
createCsrfTokenMiddleware,
|
|
301
|
+
createCsrfValidationMiddleware,
|
|
302
|
+
createViewContextMiddleware,
|
|
303
|
+
db,
|
|
304
|
+
safeEscape,
|
|
305
|
+
safeStringify,
|
|
306
|
+
setup,
|
|
307
|
+
userContextMiddleware
|
|
308
|
+
};
|
|
309
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/setup.ts","../src/middlewares/user-context.ts","../src/middlewares/csrf-token.ts","../src/middlewares/csrf-validation.ts","../src/utils/platform-client.ts","../src/utils/safe-escape.ts","../src/middlewares/view-context.ts"],"sourcesContent":["import express from 'express';\nimport cookieParser from 'cookie-parser';\nimport { createDatabase, createRlsMiddleware } from '@lark-apaas/express-datapaas';\nimport type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';\n\nimport type { SetupConfig } from './types/config';\nimport { userContextMiddleware } from './middlewares/user-context';\nimport { createCsrfTokenMiddleware } from './middlewares/csrf-token';\nimport { createCsrfValidationMiddleware } from './middlewares/csrf-validation';\nimport { createViewContextMiddleware } from './middlewares/view-context';\nimport { createPlatformClient } from './utils/platform-client';\n\n// eslint-disable-next-line import/no-mutable-exports\nexport let db: PostgresJsDatabase;\n\nexport function setup(app: express.Application, config?: SetupConfig): { close: () => Promise<void> } {\n const bodyLimit = config?.bodyLimit ?? process.env.BODY_SIZE_LIMIT ?? '1mb';\n\n // 1. Body parsers\n app.use(express.json({ limit: bodyLimit }));\n app.use(express.urlencoded({ limit: bodyLimit, extended: true }));\n\n // 2. Cookie parser\n app.use(cookieParser());\n\n // 3. UserContext (all routes)\n app.use(userContextMiddleware);\n\n // 4. Database + RLS\n const connectionString = config?.database?.connectionString ?? process.env.DATABASE_URL ?? '';\n const { db: database, close } = createDatabase({\n connectionString,\n ...config?.database,\n });\n db = database;\n app.use(createRlsMiddleware());\n\n // 5. CSRF\n const csrfEnabled = config?.csrf?.enabled ?? true;\n if (csrfEnabled) {\n const csrfRoutes = config?.csrf?.routes ?? ['/api/*'];\n\n // 页面路由生成 token(排除 API 路由),为后续 API 请求准备 cookie\n app.use(createCsrfTokenMiddleware({\n cookieKey: config?.csrf?.cookieKey,\n cookieMaxAge: config?.csrf?.cookieMaxAge,\n cookiePath: config?.csrf?.cookiePath,\n exclude: csrfRoutes,\n }));\n\n // API 路由验证 token(cookie vs header)\n app.use(csrfRoutes, createCsrfValidationMiddleware({\n headerKey: config?.csrf?.headerKey,\n cookieKey: config?.csrf?.cookieKey,\n }));\n }\n\n // 6. Platform client + ViewContext\n createPlatformClient();\n app.use(createViewContextMiddleware());\n\n return { close };\n}\n","import type { Request, Response, NextFunction } from 'express';\n\nconst SUDA_WEB_USER_HEADER_KEY = 'x-larkgw-suda-webuser';\n\ninterface SudaWebUser {\n user_id: string;\n tenant_id: number;\n app_id: string;\n login_url: string;\n user_type: string;\n env: 'preview' | 'runtime';\n user_name: Record<string, string>;\n is_system_account?: boolean;\n roles?: string[];\n}\n\nfunction getWebUserFromHeader(req: Request): SudaWebUser | null {\n const sudaWebUserContent = req.headers[SUDA_WEB_USER_HEADER_KEY] as string | undefined;\n if (!sudaWebUserContent) {\n return null;\n }\n try {\n const sudaWebUserJsonStr = decodeURIComponent(sudaWebUserContent);\n return JSON.parse(sudaWebUserJsonStr) as SudaWebUser;\n } catch (err) {\n console.error('parse suda webuser from header failed, err=%o', err);\n return null;\n }\n}\n\nexport function userContextMiddleware(req: Request, _res: Response, next: NextFunction) {\n const webUser = getWebUserFromHeader(req);\n req.userContext = {\n userId: webUser?.user_id,\n tenantId: webUser?.tenant_id,\n appId: webUser?.app_id ?? '',\n loginUrl: webUser?.login_url ?? '',\n userType: webUser?.user_type ?? '',\n env: webUser?.env ?? 'runtime',\n userName: webUser?.user_name?.zh_cn ?? '',\n userNameEn: webUser?.user_name?.en_us ?? '',\n userNameI18n: webUser?.user_name ?? {},\n isSystemAccount: webUser?.is_system_account ?? false,\n roles: webUser?.roles,\n };\n next();\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport crypto from 'crypto';\n\nexport interface CsrfTokenOptions {\n cookieKey?: string;\n cookieMaxAge?: number;\n cookiePath?: string;\n /** 排除的路由模式,匹配到则跳过 token 生成 */\n exclude?: string[];\n}\n\ninterface ResolvedCsrfTokenOptions {\n cookieKey: string;\n cookieMaxAge: number;\n cookiePath: string;\n exclude: RegExp[];\n}\n\nfunction pathToRegex(pattern: string): RegExp {\n // 将 Express 风格的路径模式转为正则:/api/* → /^\\/api\\/.*$/\n const escaped = pattern\n .replace(/[.+?^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*');\n return new RegExp(`^${escaped}$`);\n}\n\nfunction resolveOptions(options: CsrfTokenOptions): ResolvedCsrfTokenOptions {\n return {\n cookieKey: options.cookieKey ?? 'suda-csrf-token',\n cookieMaxAge: options.cookieMaxAge ?? 1000 * 60 * 60 * 24 * 30,\n cookiePath: options.cookiePath ?? '/',\n exclude: (options.exclude ?? []).map(pathToRegex),\n };\n}\n\nfunction genToken(): string {\n const ts = Math.floor(Date.now() / 1000);\n const randInt64 = BigInt('0x' + crypto.randomBytes(8).toString('hex')).toString();\n const s = `${randInt64}.${ts}`;\n const sha1 = crypto.createHash('sha1');\n sha1.update(s);\n return `${sha1.digest('hex')}-${ts}`;\n}\n\nexport function createCsrfTokenMiddleware(options?: CsrfTokenOptions) {\n const resolved = resolveOptions(options ?? {});\n\n return function csrfTokenMiddleware(req: Request, res: Response, next: NextFunction) {\n // 匹配排除路由则跳过\n const pathname = req.path;\n if (resolved.exclude.some(re => re.test(pathname))) {\n return next();\n }\n\n const { cookieKey, cookieMaxAge, cookiePath } = resolved;\n const originToken = req.cookies[cookieKey.toLowerCase()];\n\n if (originToken) {\n req.csrfToken = originToken;\n } else {\n const token = genToken();\n req.csrfToken = token;\n res.cookie(cookieKey, token, {\n maxAge: cookieMaxAge,\n path: cookiePath,\n httpOnly: false,\n secure: true,\n sameSite: 'none',\n partitioned: true,\n });\n }\n next();\n };\n}\n","import type { Request, Response, NextFunction } from 'express';\n\nexport interface CsrfValidationOptions {\n headerKey?: string;\n cookieKey?: string;\n}\n\ninterface ResolvedCsrfValidationOptions {\n headerKey: string;\n cookieKey: string;\n}\n\nfunction resolveOptions(options: CsrfValidationOptions): ResolvedCsrfValidationOptions {\n return {\n headerKey: options.headerKey ?? 'x-suda-csrf-token',\n cookieKey: options.cookieKey ?? 'suda-csrf-token',\n };\n}\n\nfunction sendForbidden(res: Response, message: string) {\n res.status(403).send(`Forbidden,${message}`);\n}\n\nexport function createCsrfValidationMiddleware(options?: CsrfValidationOptions) {\n const resolved = resolveOptions(options ?? {});\n\n return function csrfValidationMiddleware(req: Request, res: Response, next: NextFunction) {\n const { headerKey, cookieKey } = resolved;\n\n const cookieCsrfToken = req.cookies[cookieKey.toLowerCase()];\n if (!cookieCsrfToken) {\n sendForbidden(res, 'csrf token not found in cookie.');\n return;\n }\n\n const headerCsrfToken = req.headers[headerKey.toLowerCase()];\n if (!headerCsrfToken) {\n sendForbidden(res, 'csrf token not found in header.');\n return;\n }\n\n if (cookieCsrfToken !== headerCsrfToken) {\n sendForbidden(res, 'csrf token not match.');\n return;\n }\n\n next();\n };\n}\n","import { HttpClient } from '@lark-apaas/http-client';\n\nlet _client: HttpClient | null = null;\n\nexport function createPlatformClient(): HttpClient {\n if (_client) return _client;\n _client = new HttpClient({\n timeout: 5000,\n platform: { enabled: true },\n });\n return _client;\n}\n\nexport function getPlatformClient(): HttpClient {\n if (!_client) {\n throw new Error('Platform client not initialized. Call setup() first.');\n }\n return _client;\n}\n","/**\n * 安全转义的 Json Stringify,模版渲染的 JSON 对象需要调用 safeStringify,而不是用原生的 JSON.stringify。\n * 抄 gson 的实现: https://github.com/google/gson/blob/49ddab9eeb6cbac686711deca6001d40e8d8500d/gson/src/main/java/com/google/gson/stream/JsonWriter.java\n * 将 json 的敏感字符转换为 unicode, 避免 xss\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function safeStringify(obj: any) {\n const json = JSON.stringify(obj);\n return safeEscape(json);\n}\n\nenum EscapedChar {\n LT = 0x3c, // <\n GT = 0x3e, // >\n AMP = 0x26, // &\n APO = 0x27, // '\n QUOT = 0x22, // \"\n EQUAL = 0x3d, // =\n LF = 0x0a, // \\n\n CR = 0x0d, // \\r\n LS = 0x2028, // 行分隔符\n PS = 0x2029, // 段落分隔符\n}\n\nexport function safeEscape(s: string) {\n return s.replace(/[<>&='\"\\n\\r\\u2028\\u2029]/g, function (c) {\n switch (c.charCodeAt(0)) {\n case EscapedChar.LT:\n return '\\\\u003c';\n case EscapedChar.GT:\n return '\\\\u003e';\n case EscapedChar.AMP:\n return '\\\\u0026';\n case EscapedChar.EQUAL:\n return '\\\\u003d';\n case EscapedChar.APO:\n return '\\\\u0027';\n case EscapedChar.QUOT:\n return '\\\\u0022';\n case EscapedChar.LF:\n return '\\\\u000a';\n case EscapedChar.CR:\n return '\\\\u000d';\n case EscapedChar.LS:\n return '\\\\u2028';\n case EscapedChar.PS:\n return '\\\\u2029';\n default:\n return c;\n }\n });\n}\n","import type { Request, Response, NextFunction } from 'express';\nimport { getPlatformClient } from '../utils/platform-client';\nimport { safeEscape } from '../utils/safe-escape';\n\ninterface GetAppInfoResponse {\n status_code: string;\n data: {\n status_code: number;\n app_info: {\n app_name: string;\n app_description: string;\n app_avatar: string;\n };\n app_runtime_extra: {\n url: string;\n bucket: string;\n };\n };\n}\n\nasync function getAppInfo(appId?: string) {\n if (!appId) {\n console.warn('appId is empty, skip get app info');\n return null;\n }\n try {\n const client = getPlatformClient();\n const resp = await client.get(`/b/${appId}/get_published_v2`);\n if (resp.status !== 200) {\n throw new Error(`Failed to get app info, status: ${resp.status}`);\n }\n const data = (await resp.json()) as GetAppInfoResponse;\n if (data.status_code !== '0') {\n throw new Error(`Failed to get app info, status_code: ${data.status_code}`);\n }\n return data.data.app_info ?? {};\n } catch (err) {\n console.error('Failed to get app info', err);\n return null;\n }\n}\n\nfunction mapToWindowEnvironment(input?: string): string {\n const value = (input || '').trim().toLowerCase();\n if (value === 'boe') return 'staging';\n if (value === 'pre') return 'gray';\n return 'online';\n}\n\nexport function createViewContextMiddleware() {\n return async function viewContextMiddleware(req: Request, res: Response, next: NextFunction) {\n const { userId, tenantId, appId, loginUrl, userType } = req.userContext;\n const csrfToken = req.csrfToken;\n const appInfo = await getAppInfo(appId);\n const environment = mapToWindowEnvironment(process.env.FORCE_FRAMEWORK_ENVIRONMENT);\n\n req.__platform_data__ = {\n csrfToken: csrfToken ?? '',\n userId: userId ?? '',\n appId: appId ?? '',\n appName: safeEscape(appInfo?.app_name ?? ''),\n appAvatar: appInfo?.app_avatar ?? '',\n appDescription: safeEscape(appInfo?.app_description ?? ''),\n loginUrl: loginUrl ?? '',\n userType: userType ?? '',\n tenantId,\n environment,\n };\n\n res.locals = {\n ...(res.locals ?? {}),\n csrfToken: csrfToken ?? '',\n userId: userId ?? '',\n tenantId: tenantId ?? '',\n loginUrl: loginUrl ?? '',\n userType: userType ?? '',\n appId: appId ?? '',\n appName: safeEscape(appInfo?.app_name ?? ''),\n appAvatar: appInfo?.app_avatar ?? '',\n appDescription: safeEscape(appInfo?.app_description ?? ''),\n environment,\n };\n\n next();\n };\n}\n"],"mappings":"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","names":["express","cookieParser","createDatabase","createRlsMiddleware","SUDA_WEB_USER_HEADER_KEY","getWebUserFromHeader","req","sudaWebUserContent","headers","sudaWebUserJsonStr","decodeURIComponent","JSON","parse","err","console","error","userContextMiddleware","_res","next","webUser","userContext","userId","user_id","tenantId","tenant_id","appId","app_id","loginUrl","login_url","userType","user_type","env","userName","user_name","zh_cn","userNameEn","en_us","userNameI18n","isSystemAccount","is_system_account","roles","crypto","pathToRegex","pattern","escaped","replace","RegExp","resolveOptions","options","cookieKey","cookieMaxAge","cookiePath","exclude","map","genToken","ts","Math","floor","Date","now","randInt64","BigInt","crypto","randomBytes","toString","s","sha1","createHash","update","digest","createCsrfTokenMiddleware","resolved","csrfTokenMiddleware","req","res","next","pathname","path","some","re","test","originToken","cookies","toLowerCase","csrfToken","token","cookie","maxAge","httpOnly","secure","sameSite","partitioned","resolveOptions","options","headerKey","cookieKey","sendForbidden","res","message","status","send","createCsrfValidationMiddleware","resolved","csrfValidationMiddleware","req","next","cookieCsrfToken","cookies","toLowerCase","headerCsrfToken","headers","HttpClient","_client","createPlatformClient","HttpClient","timeout","platform","enabled","getPlatformClient","Error","safeStringify","obj","json","JSON","stringify","safeEscape","safeEscape","s","replace","c","charCodeAt","getAppInfo","appId","console","warn","client","getPlatformClient","resp","get","status","Error","data","json","status_code","app_info","err","error","mapToWindowEnvironment","input","value","trim","toLowerCase","createViewContextMiddleware","viewContextMiddleware","req","res","next","userId","tenantId","loginUrl","userType","userContext","csrfToken","appInfo","environment","process","env","FORCE_FRAMEWORK_ENVIRONMENT","__platform_data__","appName","safeEscape","app_name","appAvatar","app_avatar","appDescription","app_description","locals","db","setup","app","config","bodyLimit","process","env","BODY_SIZE_LIMIT","use","express","json","limit","urlencoded","extended","cookieParser","userContextMiddleware","connectionString","database","DATABASE_URL","close","createDatabase","createRlsMiddleware","csrfEnabled","csrf","enabled","csrfRoutes","routes","createCsrfTokenMiddleware","cookieKey","cookieMaxAge","cookiePath","exclude","createCsrfValidationMiddleware","headerKey","createPlatformClient","createViewContextMiddleware"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lark-apaas/express-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Express core framework with platform middleware integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"publishConfig": {
|
|
9
|
+
"access": "public"
|
|
10
|
+
},
|
|
11
|
+
"license": "MIT",
|
|
12
|
+
"keywords": [
|
|
13
|
+
"plugin",
|
|
14
|
+
"express",
|
|
15
|
+
"server",
|
|
16
|
+
"typescript",
|
|
17
|
+
"middleware",
|
|
18
|
+
"platform"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"require": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"clean": "rm -rf dist",
|
|
37
|
+
"lint": "eslint src/**/*.ts",
|
|
38
|
+
"type-check": "tsc --noEmit",
|
|
39
|
+
"test": "vitest run",
|
|
40
|
+
"test:watch": "vitest --watch",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@lark-apaas/express-datapaas": "^0.0.1",
|
|
45
|
+
"@lark-apaas/http-client": "^0.1.2",
|
|
46
|
+
"cookie-parser": "^1.4.7"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/cookie-parser": "^1.4.9",
|
|
50
|
+
"@types/express": "^5.0.0",
|
|
51
|
+
"@types/node": "^20.0.0",
|
|
52
|
+
"tsup": "^8.0.0",
|
|
53
|
+
"typescript": "^5.0.0",
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
|
+
},
|
|
56
|
+
"peerDependencies": {
|
|
57
|
+
"drizzle-orm": "0.44.6",
|
|
58
|
+
"express": "^4.0.0 || ^5.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|