@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 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"]}
@@ -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 };
@@ -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
+ }