@lark-apaas/express-core 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -35,6 +35,7 @@ __export(index_exports, {
35
35
  createCsrfValidationMiddleware: () => createCsrfValidationMiddleware,
36
36
  createViewContextMiddleware: () => createViewContextMiddleware,
37
37
  db: () => db,
38
+ getPlatformClient: () => getPlatformClient,
38
39
  safeEscape: () => safeEscape,
39
40
  safeStringify: () => safeStringify,
40
41
  setup: () => setup,
@@ -235,7 +236,7 @@ async function getAppInfo(appId) {
235
236
  return null;
236
237
  }
237
238
  try {
238
- const client = getPlatformClient();
239
+ const client = createPlatformClient();
239
240
  const resp = await client.get(`/b/${appId}/get_published_v2`);
240
241
  if (resp.status !== 200) {
241
242
  throw new Error(`Failed to get app info, status: ${resp.status}`);
@@ -330,8 +331,9 @@ function setup(app, config) {
330
331
  cookieKey: config?.csrf?.cookieKey
331
332
  }));
332
333
  }
333
- createPlatformClient();
334
- app.use(createViewContextMiddleware());
334
+ if (config?.viewContext?.enabled !== false) {
335
+ app.use(createViewContextMiddleware());
336
+ }
335
337
  return {
336
338
  close
337
339
  };
@@ -343,6 +345,7 @@ __name(setup, "setup");
343
345
  createCsrfValidationMiddleware,
344
346
  createViewContextMiddleware,
345
347
  db,
348
+ getPlatformClient,
346
349
  safeEscape,
347
350
  safeStringify,
348
351
  setup,
@@ -1 +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"]}
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';\nexport { getPlatformClient } from './utils/platform-client';\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';\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. ViewContext\n if (config?.viewContext?.enabled !== false) {\n app.use(createViewContextMiddleware());\n }\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 { createPlatformClient } 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 = createPlatformClient();\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","createPlatformClient","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","viewContext","createViewContextMiddleware"]}
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ import express, { Request, Response, NextFunction } from 'express';
2
2
  import { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
3
3
  export { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
4
4
  import { CreateDatabaseConfig } from '@lark-apaas/express-datapaas';
5
+ import { HttpClient } from '@lark-apaas/http-client';
5
6
 
6
7
  declare global {
7
8
  namespace Express {
@@ -40,6 +41,10 @@ interface SetupConfig {
40
41
  cookiePath?: string;
41
42
  routes?: string[];
42
43
  };
44
+ viewContext?: {
45
+ /** Whether to register viewContextMiddleware globally. Default: true */
46
+ enabled?: boolean;
47
+ };
43
48
  bodyLimit?: string;
44
49
  }
45
50
 
@@ -48,21 +53,17 @@ declare function setup(app: express.Application, config?: SetupConfig): {
48
53
  close: () => Promise<void>;
49
54
  };
50
55
 
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
56
  declare function safeStringify(obj: any): string;
57
57
  declare function safeEscape(s: string): string;
58
58
 
59
+ declare function getPlatformClient(): HttpClient;
60
+
59
61
  declare function userContextMiddleware(req: Request, _res: Response, next: NextFunction): void;
60
62
 
61
63
  interface CsrfTokenOptions {
62
64
  cookieKey?: string;
63
65
  cookieMaxAge?: number;
64
66
  cookiePath?: string;
65
- /** 排除的路由模式,匹配到则跳过 token 生成 */
66
67
  exclude?: string[];
67
68
  }
68
69
  declare function createCsrfTokenMiddleware(options?: CsrfTokenOptions): (req: Request, res: Response, next: NextFunction) => void;
@@ -75,4 +76,4 @@ declare function createCsrfValidationMiddleware(options?: CsrfValidationOptions)
75
76
 
76
77
  declare function createViewContextMiddleware(): (req: Request, res: Response, next: NextFunction) => Promise<void>;
77
78
 
78
- export { type CsrfTokenOptions, type CsrfValidationOptions, type SetupConfig, createCsrfTokenMiddleware, createCsrfValidationMiddleware, createViewContextMiddleware, db, safeEscape, safeStringify, setup, userContextMiddleware };
79
+ export { type CsrfTokenOptions, type CsrfValidationOptions, type SetupConfig, createCsrfTokenMiddleware, createCsrfValidationMiddleware, createViewContextMiddleware, db, getPlatformClient, safeEscape, safeStringify, setup, userContextMiddleware };
package/dist/index.js CHANGED
@@ -194,7 +194,7 @@ async function getAppInfo(appId) {
194
194
  return null;
195
195
  }
196
196
  try {
197
- const client = getPlatformClient();
197
+ const client = createPlatformClient();
198
198
  const resp = await client.get(`/b/${appId}/get_published_v2`);
199
199
  if (resp.status !== 200) {
200
200
  throw new Error(`Failed to get app info, status: ${resp.status}`);
@@ -289,8 +289,9 @@ function setup(app, config) {
289
289
  cookieKey: config?.csrf?.cookieKey
290
290
  }));
291
291
  }
292
- createPlatformClient();
293
- app.use(createViewContextMiddleware());
292
+ if (config?.viewContext?.enabled !== false) {
293
+ app.use(createViewContextMiddleware());
294
+ }
294
295
  return {
295
296
  close
296
297
  };
@@ -301,6 +302,7 @@ export {
301
302
  createCsrfValidationMiddleware,
302
303
  createViewContextMiddleware,
303
304
  db,
305
+ getPlatformClient,
304
306
  safeEscape,
305
307
  safeStringify,
306
308
  setup,
package/dist/index.js.map CHANGED
@@ -1 +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"]}
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';\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. ViewContext\n if (config?.viewContext?.enabled !== false) {\n app.use(createViewContextMiddleware());\n }\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 { createPlatformClient } 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 = createPlatformClient();\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","createPlatformClient","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","viewContext","createViewContextMiddleware"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/express-core",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Express core framework with platform middleware integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -41,7 +41,7 @@
41
41
  "prepublishOnly": "npm run build"
42
42
  },
43
43
  "dependencies": {
44
- "@lark-apaas/express-datapaas": "^0.0.1",
44
+ "@lark-apaas/express-datapaas": "^0.0.2",
45
45
  "@lark-apaas/http-client": "^0.1.2",
46
46
  "cookie-parser": "^1.4.7"
47
47
  },
package/dist/index.d.cts DELETED
@@ -1,78 +0,0 @@
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 };