create-skweb 1.1.4 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { execSync } from 'child_process';
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
7
  async function createProject(options) {
8
8
  const { name, description = 'A SKWeb project', template, dest } = options;
9
+ const auth = options.auth ?? 'static';
9
10
  const templateDir = path.join(__dirname, '..', 'templates', template);
10
11
  const baseDir = path.resolve(dest);
11
12
  const targetDir = path.join(baseDir, name);
@@ -29,7 +30,8 @@ async function createProject(options) {
29
30
  let content = await fs.readFile(srcPath, 'utf-8');
30
31
  content = content
31
32
  .replace(/\{\{name\}\}/g, name)
32
- .replace(/\{\{description\}\}/g, description);
33
+ .replace(/\{\{description\}\}/g, description)
34
+ .replace(/\{\{auth\}\}/g, auth);
33
35
  const destPath = path.join(targetDir, fileName);
34
36
  await fs.writeFile(destPath, content);
35
37
  }
@@ -73,20 +75,23 @@ function printSuccess(name, dest) {
73
75
  接下来可以执行:
74
76
 
75
77
  cd ${name}
76
-
78
+
79
+ # 复制环境变量模板并按需修改
80
+ cp .env.example .env
81
+
77
82
  # 开发模式运行
78
83
  npm run start:dev
79
-
84
+
80
85
  # 生产模式运行 (需要先构建)
81
86
  npm run build
82
87
  npm run start
83
-
88
+
84
89
  # 查看日志
85
90
  npm run logs
86
-
91
+
87
92
  # 停止服务
88
93
  npm run stop
89
-
94
+
90
95
  # 重启服务
91
96
  npm run restart
92
97
 
@@ -95,6 +100,7 @@ function printSuccess(name, dest) {
95
100
  ✅ @dotenvx/dotenvx 环境变量管理
96
101
  ✅ PM2 进程管理
97
102
  ✅ SKWeb Express 集成
103
+ ✅ Keycloak/JWKS 鉴权 (可选)
98
104
  ✅ ESLint 代码检查
99
105
  ✅ Mocha 测试框架
100
106
  `);
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":[null],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-skweb",
3
- "version": "1.1.4",
3
+ "version": "2.0.0",
4
4
  "description": "Create a new SKWeb project with TypeScript, CJS, or MJS support",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/cli.ts CHANGED
@@ -20,14 +20,31 @@ async function main() {
20
20
  .argument('[name]', 'Project name')
21
21
  .option('--template <type>', 'Template type: ts, mjs, cjs', 'ts')
22
22
  .option('--dest <path>', 'Destination directory', '.')
23
+ .option('--auth <type>', 'Auth provider: static (HS256) or jwks (Keycloak/Auth0)', 'static')
23
24
  .option('--skip-install', 'Skip dependency installation')
24
25
  .option('--skip-git', 'Skip git initialization')
25
26
  .action(async (name, options) => {
26
27
  printBanner()
27
-
28
- if (!name) {
29
- const answers = await inquirer.prompt([
30
- {
28
+
29
+ // 只要没有传 --no-interactive 且任何一个关键参数缺失,就走交互式 prompt
30
+ const providedTemplate = process.argv.includes('--template')
31
+ const providedDest = process.argv.includes('--dest')
32
+ const providedAuth = process.argv.includes('--auth')
33
+ const providedSkipInstall = process.argv.includes('--skip-install')
34
+ const providedSkipGit = process.argv.includes('--skip-git')
35
+
36
+ // 如果有任何一个关键参数未通过命令行提供,就走交互式 prompt
37
+ const needsPrompt = !name
38
+ || !providedTemplate
39
+ || !providedDest
40
+ || !providedAuth
41
+ || !providedSkipInstall
42
+ || !providedSkipGit
43
+
44
+ if (needsPrompt) {
45
+ const questions: any[] = []
46
+ if (!name) {
47
+ questions.push({
31
48
  type: 'input',
32
49
  name: 'name',
33
50
  message: 'Enter project name:',
@@ -38,8 +55,10 @@ async function main() {
38
55
  }
39
56
  return true
40
57
  }
41
- },
42
- {
58
+ })
59
+ }
60
+ if (!providedTemplate) {
61
+ questions.push({
43
62
  type: 'list',
44
63
  name: 'template',
45
64
  message: 'Select project template:',
@@ -49,44 +68,66 @@ async function main() {
49
68
  { name: 'CommonJS (CJS)', value: 'cjs' }
50
69
  ],
51
70
  default: 'ts'
52
- },
53
- {
71
+ })
72
+ }
73
+ if (!providedAuth) {
74
+ questions.push({
75
+ type: 'list',
76
+ name: 'auth',
77
+ message: 'Select auth provider:',
78
+ choices: [
79
+ { name: 'Static (HS256 shared secret)', value: 'static' },
80
+ { name: 'JWKS (Keycloak / Auth0 / OIDC)', value: 'jwks' }
81
+ ],
82
+ default: 'static'
83
+ })
84
+ }
85
+ if (!providedDest) {
86
+ questions.push({
54
87
  type: 'input',
55
88
  name: 'dest',
56
89
  message: 'Enter destination directory:',
57
90
  default: '.'
58
- },
59
- {
91
+ })
92
+ }
93
+ if (!providedSkipInstall) {
94
+ questions.push({
60
95
  type: 'confirm',
61
96
  name: 'install',
62
97
  message: 'Install dependencies?',
63
98
  default: true
64
- },
65
- {
99
+ })
100
+ }
101
+ if (!providedSkipGit) {
102
+ questions.push({
66
103
  type: 'confirm',
67
104
  name: 'git',
68
105
  message: 'Initialize git repository?',
69
106
  default: true
70
- }
71
- ])
72
-
73
- name = answers.name
74
- options.template = answers.template
75
- options.dest = answers.dest
76
- options.skipInstall = !answers.install
77
- options.skipGit = !answers.git
107
+ })
108
+ }
109
+
110
+ const answers: any = await inquirer.prompt(questions)
111
+
112
+ if (answers.name) name = answers.name
113
+ if (answers.template) options.template = answers.template
114
+ if (answers.auth) options.auth = answers.auth
115
+ if (answers.dest) options.dest = answers.dest
116
+ if (typeof answers.install === 'boolean') options.skipInstall = !answers.install
117
+ if (typeof answers.git === 'boolean') options.skipGit = !answers.git
78
118
  }
79
-
119
+
80
120
  const spinner = ora('Creating project...').start()
81
-
121
+
82
122
  try {
83
123
  await createProject({
84
124
  name,
85
125
  template: options.template as 'ts' | 'mjs' | 'cjs',
86
- dest: options.dest
126
+ dest: options.dest,
127
+ auth: (options.auth || 'static') as 'static' | 'jwks'
87
128
  })
88
129
  spinner.succeed('Project files created')
89
-
130
+
90
131
  if (!options.skipInstall) {
91
132
  spinner.text = 'Installing dependencies...'
92
133
  spinner.start()
@@ -100,11 +141,11 @@ async function main() {
100
141
  await initGit(options.dest, name)
101
142
  spinner.succeed('Git repository initialized')
102
143
  }
103
-
144
+
104
145
  spinner.stop()
105
-
146
+
106
147
  printSuccess(name, options.dest)
107
-
148
+
108
149
  } catch (error) {
109
150
  spinner.fail('Failed to create project')
110
151
  console.error(error)
package/src/index.ts CHANGED
@@ -10,10 +10,13 @@ export interface CreateOptions {
10
10
  description?: string
11
11
  template: 'ts' | 'mjs' | 'cjs'
12
12
  dest: string
13
+ /** Auth provider: 'static' (HS256, default) or 'jwks' (Keycloak/Auth0) */
14
+ auth?: 'static' | 'jwks'
13
15
  }
14
16
 
15
17
  export async function createProject(options: CreateOptions): Promise<void> {
16
18
  const { name, description = 'A SKWeb project', template, dest } = options
19
+ const auth = options.auth ?? 'static'
17
20
 
18
21
  const templateDir = path.join(__dirname, '..', 'templates', template)
19
22
  const baseDir = path.resolve(dest)
@@ -44,6 +47,7 @@ export async function createProject(options: CreateOptions): Promise<void> {
44
47
  content = content
45
48
  .replace(/\{\{name\}\}/g, name)
46
49
  .replace(/\{\{description\}\}/g, description)
50
+ .replace(/\{\{auth\}\}/g, auth)
47
51
 
48
52
  const destPath = path.join(targetDir, fileName)
49
53
  await fs.writeFile(destPath, content)
@@ -96,20 +100,23 @@ export function printSuccess(name: string, dest: string): void {
96
100
  接下来可以执行:
97
101
 
98
102
  cd ${name}
99
-
103
+
104
+ # 复制环境变量模板并按需修改
105
+ cp .env.example .env
106
+
100
107
  # 开发模式运行
101
108
  npm run start:dev
102
-
109
+
103
110
  # 生产模式运行 (需要先构建)
104
111
  npm run build
105
112
  npm run start
106
-
113
+
107
114
  # 查看日志
108
115
  npm run logs
109
-
116
+
110
117
  # 停止服务
111
118
  npm run stop
112
-
119
+
113
120
  # 重启服务
114
121
  npm run restart
115
122
 
@@ -118,6 +125,7 @@ export function printSuccess(name: string, dest: string): void {
118
125
  ✅ @dotenvx/dotenvx 环境变量管理
119
126
  ✅ PM2 进程管理
120
127
  ✅ SKWeb Express 集成
128
+ ✅ Keycloak/JWKS 鉴权 (可选)
121
129
  ✅ ESLint 代码检查
122
130
  ✅ Mocha 测试框架
123
131
  `)
@@ -0,0 +1,73 @@
1
+ # ============================================
2
+ # SKWeb Environment Variables Template
3
+ # ============================================
4
+ # Copy this file to .env and fill in your values:
5
+ # cp .env.example .env
6
+ #
7
+ # Variables with default values can be omitted.
8
+ # Variables marked [REQUIRED] must be set in production.
9
+
10
+ # ---------- Node Environment ----------
11
+ # Development | Production | Test
12
+ NODE_ENV=development
13
+
14
+ # ---------- Server ----------
15
+ PORT=3000
16
+ HOST=0.0.0.0
17
+
18
+ # ---------- Database (MySQL / MariaDB) ----------
19
+ DB_HOST=127.0.0.1
20
+ DB_PORT=3306
21
+ DB_USER=root
22
+ DB_PASS=
23
+ DB_NAME={{name}}
24
+ DB_DIALECT=mysql
25
+ DB_CHARSET=utf8mb4
26
+ DEBUG_DB=false
27
+
28
+ # ---------- Redis ----------
29
+ REDIS_HOST=127.0.0.1
30
+ REDIS_PORT=6379
31
+ REDIS_PASSWORD=
32
+ REDIS_DB=0
33
+
34
+ # ---------- Swagger / OpenAPI ----------
35
+ # Mounts Swagger UI at /api-docs and exposes raw spec at /api-docs.json.
36
+ # Only enabled when NODE_ENV is 'development' or 'test'.
37
+ # Set SWAGGER_ENABLED=false to force-disable in dev/test as well.
38
+ # (Production is always disabled to avoid exposing internal API details.)
39
+ SWAGGER_ENABLED=true
40
+
41
+ # ---------- Auth Provider ----------
42
+ # 'static' (default): use a shared secret (HS256)
43
+ # 'jwks': verify via JWKS endpoint (Keycloak / Auth0 / generic OIDC)
44
+ AUTH_PROVIDER={{auth}}
45
+
46
+ # ---------- JWT (static mode) ----------
47
+ # [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
48
+ JWT_SECRET=please-change-this-in-production
49
+ JWT_EXPIRES_IN=7d
50
+
51
+ # ---------- JWT (jwks mode, OIDC compatible) ----------
52
+ # Works with any OIDC-compliant IdP. Common issuer formats:
53
+ # Keycloak: https://kc.example.com/realms/myrealm
54
+ # Auth0: https://your-tenant.auth0.com/
55
+ # Okta: https://your-domain.okta.com/oauth2/default
56
+ # Authing: https://your-app.authing.cn/oauth2/default
57
+ # Keycloak Realm: https://kc.example.com/auth/realms/myrealm
58
+ # `audience` is the client id of this API in the IdP (recommended).
59
+ # JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
60
+ # unless OIDC_JWKS_URI is set explicitly.
61
+ OIDC_ISSUER=
62
+ OIDC_AUDIENCE=
63
+ OIDC_JWKS_URI=
64
+
65
+ # ---------- Logging ----------
66
+ # error | warn | info | http | verbose | debug | silly
67
+ LOG_LEVEL=info
68
+ LOG_DIR=./logs
69
+ LOG_CONSOLE=true
70
+
71
+ # ---------- CORS ----------
72
+ # Comma-separated origins, or * for all
73
+ CORS_ORIGIN=*
@@ -23,7 +23,7 @@
23
23
  "@dotenvx/dotenvx": "^1.71.0",
24
24
  "http-errors": "^2.0.0",
25
25
  "skweb-express": "^1.0.0",
26
- "skweb-express-jwt": "^1.0.0",
26
+ "skweb-express-jwt": "^1.2.0",
27
27
  "skweb-express-logger": "^1.0.0",
28
28
  "skweb-sequelize": "^1.0.0",
29
29
  "skweb-redis": "^1.0.0",
@@ -32,7 +32,9 @@
32
32
  "skweb-plugin": "^1.0.0",
33
33
  "skweb-util": "^1.0.0",
34
34
  "winston": "^3.17.0",
35
- "sequelize": "^6.37.7"
35
+ "sequelize": "^6.37.7",
36
+ "swagger-jsdoc": "^6.2.8",
37
+ "swagger-ui-express": "^5.0.1"
36
38
  },
37
39
  "devDependencies": {
38
40
  "chai": "^6.2.1",
@@ -3,9 +3,55 @@ require('dotenv/config')
3
3
  const path = require('path')
4
4
  const express = require('skweb-express')
5
5
  const expressLogger = require('skweb-express-logger').default || require('skweb-express-logger')
6
+ const swaggerUi = require('swagger-ui-express')
6
7
  const { WebFramework } = require('skweb-framework')
7
8
  const { logger } = require('./utils/logger.js')
8
9
  const { checkRequiredEnv } = require('./utils/config.js')
10
+ const { buildOpenApiSpec } = require('./swagger.js')
11
+
12
+ /**
13
+ * 构建 JWT 配置,支持两种模式:
14
+ * - 'static' (默认):使用共享密钥(HS256/HS384/HS512),适用于自签 token
15
+ * - 'jwks':通过 JWKS 端点动态获取公钥(RS256 等),适用于 Keycloak/Auth0 等 IdP
16
+ */
17
+ function buildJwtOptions() {
18
+ const provider = process.env.AUTH_PROVIDER || 'static'
19
+
20
+ if (provider === 'jwks') {
21
+ const issuer = process.env.OIDC_ISSUER
22
+ const jwksUri = process.env.OIDC_JWKS_URI
23
+ const audience = process.env.OIDC_AUDIENCE
24
+ if (!issuer && !jwksUri) {
25
+ throw new Error('AUTH_PROVIDER=jwks requires OIDC_ISSUER or OIDC_JWKS_URI')
26
+ }
27
+ return {
28
+ provider: 'jwks',
29
+ algorithms: ['RS256'],
30
+ jwks: { jwksUri, issuer, audience },
31
+ verify: { issuer, audience },
32
+ requestProperty: 'resolvedToken'
33
+ }
34
+ }
35
+
36
+ return {
37
+ provider: 'static',
38
+ secret: process.env.JWT_SECRET || 'please-change-this-in-production',
39
+ algorithms: ['HS256'],
40
+ sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
41
+ requestProperty: 'resolvedToken'
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Swagger UI 仅在 NODE_ENV 为 development / test,且 SWAGGER_ENABLED !== 'false' 时启用。
47
+ * 生产环境(production / unset)默认禁用,避免暴露内部 API 细节。
48
+ */
49
+ function shouldEnableSwagger() {
50
+ const env = (process.env.NODE_ENV || '').toLowerCase()
51
+ if (env !== 'development' && env !== 'test') return false
52
+ if ((process.env.SWAGGER_ENABLED || '').toLowerCase() === 'false') return false
53
+ return true
54
+ }
9
55
 
10
56
  async function bootstrap() {
11
57
  logger.info('[app] Initializing web process...')
@@ -50,17 +96,33 @@ async function bootstrap() {
50
96
  app.setOkCode(0)
51
97
  app.setOkMessage('success')
52
98
 
53
- app.setJWT('default', {
54
- secret: process.env.JWT_SECRET || 'please-change-this-in-production',
55
- algorithms: ['HS256'],
56
- sign: { expiresIn: process.env.JWT_EXPIRES_IN || '7d' },
57
- requestProperty: 'resolvedToken'
58
- })
99
+ // JWT 配置(按 AUTH_PROVIDER 选择 static 或 jwks 模式)
100
+ const jwtOptions = buildJwtOptions()
101
+ app.setJWT('default', jwtOptions)
59
102
  app.setJwtHintHeader('JwtHint')
60
103
 
104
+ if (jwtOptions.provider === 'jwks') {
105
+ logger.info(`[app] JWT auth provider: jwks (issuer: ${jwtOptions.jwks.issuer || jwtOptions.jwks.jwksUri})`)
106
+ } else {
107
+ logger.info('[app] JWT auth provider: static (HS256)')
108
+ }
109
+
61
110
  app.use(expressLogger())
62
111
  app.use(express.static(path.join(__dirname, '..', 'public')))
63
112
 
113
+ // 仅在 dev / test 环境挂载 Swagger UI(默认关闭,需显式 SWAGGER_ENABLED=true)
114
+ if (shouldEnableSwagger()) {
115
+ const openapiSpec = buildOpenApiSpec()
116
+ app.get('/api-docs.json', (_req, res) => res.json(openapiSpec))
117
+ app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(openapiSpec, {
118
+ customSiteTitle: `${process.env.APP_NAME || 'SKWeb'} API Docs`,
119
+ swaggerOptions: { persistAuthorization: true }
120
+ }))
121
+ logger.info(`[app] Swagger UI mounted at http://localhost:${process.env.PORT || 3000}/api-docs (env: ${process.env.NODE_ENV})`)
122
+ } else {
123
+ logger.info(`[app] Swagger UI disabled (env: ${process.env.NODE_ENV || 'unset'})`)
124
+ }
125
+
64
126
  await framework.loadControllers(path.join(__dirname, './controllers'))
65
127
  await framework.loadPluginControllers()
66
128
 
@@ -88,4 +150,4 @@ if (require.main === module) {
88
150
  })
89
151
  }
90
152
 
91
- module.exports = { bootstrap }
153
+ module.exports = { bootstrap }
@@ -1,3 +1,49 @@
1
+ /**
2
+ * @swagger
3
+ * /:
4
+ * get:
5
+ * summary: 欢迎页
6
+ * description: 返回项目基本信息。
7
+ * tags: [Hello]
8
+ * responses:
9
+ * 200:
10
+ * description: OK
11
+ * content:
12
+ * application/json:
13
+ * schema:
14
+ * type: object
15
+ * properties:
16
+ * message: { type: string, example: 'Welcome to SKWeb!' }
17
+ * version: { type: string, example: '1.0.0' }
18
+ * timestamp: { type: string, format: date-time }
19
+ * /health:
20
+ * get:
21
+ * summary: 健康检查
22
+ * tags: [Hello]
23
+ * responses:
24
+ * 200:
25
+ * description: OK
26
+ * content:
27
+ * application/json:
28
+ * schema:
29
+ * type: object
30
+ * properties:
31
+ * status: { type: string, example: 'ok' }
32
+ * timestamp: { type: string, format: date-time }
33
+ * /echo:
34
+ * post:
35
+ * summary: 回显请求体
36
+ * tags: [Hello]
37
+ * requestBody:
38
+ * required: true
39
+ * content:
40
+ * application/json:
41
+ * schema:
42
+ * type: object
43
+ * responses:
44
+ * 200:
45
+ * description: OK
46
+ */
1
47
  module.exports = [
2
48
  {
3
49
  method: 'get',
@@ -0,0 +1,52 @@
1
+ 'use strict'
2
+
3
+ const path = require('path')
4
+ const swaggerJSDoc = require('swagger-jsdoc')
5
+
6
+ /**
7
+ * 构建 OpenAPI 3.0 规范对象。
8
+ *
9
+ * 控制器中应使用 @swagger JSDoc 注释描述端点,例如:
10
+ *
11
+ * /**
12
+ * * @swagger
13
+ * * /api/users:
14
+ * * get:
15
+ * * summary: 获取用户列表
16
+ * * tags: [Users]
17
+ * * responses:
18
+ * * 200:
19
+ * * description: OK
20
+ * *\/
21
+ */
22
+ function buildOpenApiSpec() {
23
+ return swaggerJSDoc({
24
+ definition: {
25
+ openapi: '3.0.3',
26
+ info: {
27
+ title: process.env.APP_NAME || 'SKWeb API',
28
+ version: process.env.API_VERSION || '1.0.0',
29
+ description: process.env.APP_DESCRIPTION || 'SKWeb project API documentation',
30
+ contact: { name: 'API Support' }
31
+ },
32
+ servers: [
33
+ { url: `http://localhost:${process.env.PORT || 3000}`, description: 'Local development' }
34
+ ],
35
+ components: {
36
+ securitySchemes: {
37
+ bearerAuth: {
38
+ type: 'http',
39
+ scheme: 'bearer',
40
+ bearerFormat: 'JWT',
41
+ description: 'Paste a JWT token here (only needed when AUTH_PROVIDER=static)'
42
+ }
43
+ }
44
+ }
45
+ },
46
+ apis: [
47
+ path.join(__dirname, 'controllers', '*.js')
48
+ ]
49
+ })
50
+ }
51
+
52
+ module.exports = { buildOpenApiSpec }
@@ -0,0 +1,73 @@
1
+ # ============================================
2
+ # SKWeb Environment Variables Template
3
+ # ============================================
4
+ # Copy this file to .env and fill in your values:
5
+ # cp .env.example .env
6
+ #
7
+ # Variables with default values can be omitted.
8
+ # Variables marked [REQUIRED] must be set in production.
9
+
10
+ # ---------- Node Environment ----------
11
+ # Development | Production | Test
12
+ NODE_ENV=development
13
+
14
+ # ---------- Server ----------
15
+ PORT=3000
16
+ HOST=0.0.0.0
17
+
18
+ # ---------- Database (MySQL / MariaDB) ----------
19
+ DB_HOST=127.0.0.1
20
+ DB_PORT=3306
21
+ DB_USER=root
22
+ DB_PASS=
23
+ DB_NAME={{name}}
24
+ DB_DIALECT=mysql
25
+ DB_CHARSET=utf8mb4
26
+ DEBUG_DB=false
27
+
28
+ # ---------- Redis ----------
29
+ REDIS_HOST=127.0.0.1
30
+ REDIS_PORT=6379
31
+ REDIS_PASSWORD=
32
+ REDIS_DB=0
33
+
34
+ # ---------- Swagger / OpenAPI ----------
35
+ # Mounts Swagger UI at /api-docs and exposes raw spec at /api-docs.json.
36
+ # Only enabled when NODE_ENV is 'development' or 'test'.
37
+ # Set SWAGGER_ENABLED=false to force-disable in dev/test as well.
38
+ # (Production is always disabled to avoid exposing internal API details.)
39
+ SWAGGER_ENABLED=true
40
+
41
+ # ---------- Auth Provider ----------
42
+ # 'static' (default): use a shared secret (HS256)
43
+ # 'jwks': verify via JWKS endpoint (Keycloak / Auth0 / generic OIDC)
44
+ AUTH_PROVIDER={{auth}}
45
+
46
+ # ---------- JWT (static mode) ----------
47
+ # [REQUIRED in production] Use a strong random string, e.g. `openssl rand -hex 32`
48
+ JWT_SECRET=please-change-this-in-production
49
+ JWT_EXPIRES_IN=7d
50
+
51
+ # ---------- JWT (jwks mode, OIDC compatible) ----------
52
+ # Works with any OIDC-compliant IdP. Common issuer formats:
53
+ # Keycloak: https://kc.example.com/realms/myrealm
54
+ # Auth0: https://your-tenant.auth0.com/
55
+ # Okta: https://your-domain.okta.com/oauth2/default
56
+ # Authing: https://your-app.authing.cn/oauth2/default
57
+ # Keycloak Realm: https://kc.example.com/auth/realms/myrealm
58
+ # `audience` is the client id of this API in the IdP (recommended).
59
+ # JWKS URI is auto-derived as ${OIDC_ISSUER}/.well-known/jwks.json
60
+ # unless OIDC_JWKS_URI is set explicitly.
61
+ OIDC_ISSUER=
62
+ OIDC_AUDIENCE=
63
+ OIDC_JWKS_URI=
64
+
65
+ # ---------- Logging ----------
66
+ # error | warn | info | http | verbose | debug | silly
67
+ LOG_LEVEL=info
68
+ LOG_DIR=./logs
69
+ LOG_CONSOLE=true
70
+
71
+ # ---------- CORS ----------
72
+ # Comma-separated origins, or * for all
73
+ CORS_ORIGIN=*
@@ -23,7 +23,7 @@
23
23
  "@dotenvx/dotenvx": "^1.71.0",
24
24
  "http-errors": "^2.0.0",
25
25
  "skweb-express": "^1.0.0",
26
- "skweb-express-jwt": "^1.0.0",
26
+ "skweb-express-jwt": "^1.2.0",
27
27
  "skweb-express-logger": "^1.0.0",
28
28
  "skweb-sequelize": "^1.0.0",
29
29
  "skweb-redis": "^1.0.0",
@@ -32,7 +32,9 @@
32
32
  "skweb-plugin": "^1.0.0",
33
33
  "skweb-util": "^1.0.0",
34
34
  "winston": "^3.17.0",
35
- "sequelize": "^6.37.7"
35
+ "sequelize": "^6.37.7",
36
+ "swagger-jsdoc": "^6.2.8",
37
+ "swagger-ui-express": "^5.0.1"
36
38
  },
37
39
  "devDependencies": {
38
40
  "chai": "^6.2.1",