docnest 0.1.12 → 0.1.13

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/README.md CHANGED
@@ -78,6 +78,8 @@ export default defineConfig({
78
78
  auth: {
79
79
  enabled: true,
80
80
  passphrase: '请替换为项目自己的长口令',
81
+ // 服务通过反向代理挂载在子路径时填写,例如 /documents;默认跳转到 /。
82
+ redirectPrefix: '',
81
83
  stateFile: '.docnest/auth.json',
82
84
  localStorageKey: 'my-project:auth-passphrase',
83
85
  sessionTtlMinutes: 24 * 60,
@@ -91,6 +93,8 @@ export default defineConfig({
91
93
  `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
92
94
  建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
93
95
 
96
+ 如果 DocNest 由反向代理挂载在 `/documents/` 等子路径下,将 `auth.redirectPrefix` 配置为 `/documents`。登录页会把原始文档目标写成带此前缀的路径,口令登录后的服务端跳转也会使用同一前缀;不配置时保持跳转到 `/` 的默认行为。
97
+
94
98
  授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
95
99
 
96
100
  如需显式指定端口、文档目录或监听地址:
@@ -136,3 +140,16 @@ npm run pack:check
136
140
  ```
137
141
 
138
142
  最低支持 Node.js 20,验证 Node.js 20 和 22。
143
+
144
+ ## 发布规范
145
+
146
+ DocNest 的 GitHub Release 和 npm 发布统一由 GitHub Actions 完成,不在本地直接执行 `npm publish`。
147
+
148
+ 发布新版本时:
149
+
150
+ 1. 更新 `package.json` 和 `package-lock.json` 的版本号,并运行 `npm test` 与 `npm run pack:check`。
151
+ 2. 将版本提交推送到 `main`。
152
+ 3. 在 `main` 上创建与包版本一致的标签,例如 `v0.1.13`。
153
+ 4. 推送该标签:`git push origin v0.1.13`。
154
+
155
+ 标签推送后,`.github/workflows/publish.yml` 会校验标签与包版本一致,使用 npm OIDC 发布公开包,并自动创建名为 `DocNest v0.1.13` 的 GitHub Release。远程仓库只保留 `main` 分支,发布标签不需要推送临时开发分支。
package/dist/config.d.ts CHANGED
@@ -28,6 +28,11 @@ export type DocNestConfig = {
28
28
  auth?: {
29
29
  enabled?: boolean;
30
30
  passphrase?: string;
31
+ /**
32
+ * Prefix to add to authentication return targets when the service is
33
+ * mounted below a reverse-proxy path, for example `/documents`.
34
+ */
35
+ redirectPrefix?: string;
31
36
  stateFile?: string;
32
37
  localStorageKey?: string;
33
38
  sessionTtlMinutes?: number;
@@ -49,6 +54,7 @@ export type ResolvedDocNestConfig = Required<DocNestConfig> & {
49
54
  auth: {
50
55
  enabled: boolean;
51
56
  passphrase: string;
57
+ redirectPrefix: string;
52
58
  stateFile: string;
53
59
  localStorageKey: string;
54
60
  sessionTtlMinutes: number;
package/dist/config.js CHANGED
@@ -45,6 +45,20 @@ function normalizePositiveNumber(value, fallback) {
45
45
  const normalized = Number(value);
46
46
  return Number.isFinite(normalized) && normalized > 0 ? normalized : fallback;
47
47
  }
48
+ function normalizeRedirectPrefix(value) {
49
+ if (typeof value !== 'string')
50
+ return '';
51
+ const normalized = value.trim();
52
+ if (!normalized ||
53
+ normalized === '/' ||
54
+ !normalized.startsWith('/') ||
55
+ normalized.startsWith('//') ||
56
+ normalized.includes('//') ||
57
+ normalized.includes('\\') ||
58
+ /[\r\n?#]/.test(normalized))
59
+ return '';
60
+ return normalized.replace(/\/+$/, '') || '';
61
+ }
48
62
  function normalizeThemeName(value) {
49
63
  if (value === 'current-docs')
50
64
  return 'slate-modern';
@@ -113,6 +127,7 @@ async function loadConfig(projectRoot = process.cwd()) {
113
127
  auth: {
114
128
  enabled: auth.enabled ?? false,
115
129
  passphrase: typeof auth.passphrase === 'string' ? auth.passphrase : '',
130
+ redirectPrefix: normalizeRedirectPrefix(auth.redirectPrefix),
116
131
  stateFile: node_path_1.default.resolve(projectRoot, auth.stateFile?.trim() || '.docnest/auth.json'),
117
132
  localStorageKey: authLocalStorageKey,
118
133
  sessionTtlMinutes: normalizePositiveNumber(auth.sessionTtlMinutes, 24 * 60),
package/dist/server.js CHANGED
@@ -280,6 +280,17 @@ function safeNextPath(value) {
280
280
  return '/';
281
281
  return value;
282
282
  }
283
+ function authRedirectTarget(value) {
284
+ const nextPath = safeNextPath(value);
285
+ const prefix = typeof AUTH_CONFIG.redirectPrefix === 'string' ? AUTH_CONFIG.redirectPrefix : '';
286
+ if (!prefix ||
287
+ nextPath === prefix ||
288
+ nextPath.startsWith(`${prefix}/`) ||
289
+ nextPath.startsWith(`${prefix}?`) ||
290
+ nextPath.startsWith(`${prefix}#`))
291
+ return nextPath;
292
+ return nextPath === '/' ? `${prefix}/` : `${prefix}${nextPath}`;
293
+ }
283
294
  function setAuthCookie(res, token) {
284
295
  const maxAge = AUTH_MANAGER.sessionMaxAgeSeconds;
285
296
  res.setHeader('Set-Cookie', `${AUTH_MANAGER.sessionCookieName}=${encodeURIComponent(token)}; Max-Age=${maxAge}; Path=/; HttpOnly; SameSite=Lax`);
@@ -292,7 +303,7 @@ function wantsJson(req) {
292
303
  }
293
304
  function renderLoginPage(res, nextPath, error, status = 200) {
294
305
  return res.status(status).render('login', {
295
- nextPath: safeNextPath(nextPath),
306
+ nextPath: authRedirectTarget(nextPath),
296
307
  error: error || '',
297
308
  assetVersion: ASSET_VERSION,
298
309
  });
@@ -316,7 +327,7 @@ app.post('/auth/verify', (req, res) => {
316
327
  app.post('/auth/login', (req, res) => {
317
328
  if (!BUILT_IN_AUTH_ENABLED)
318
329
  return res.redirect('/');
319
- const nextPath = safeNextPath(req.body?.next);
330
+ const nextPath = authRedirectTarget(req.body?.next);
320
331
  const token = AUTH_MANAGER.createSession(String(req.body?.passphrase || ''));
321
332
  if (!token)
322
333
  return renderLoginPage(res, nextPath, '口令不正确,请重试。', 401);
@@ -36,6 +36,8 @@ export default defineConfig({
36
36
  enabled: false,
37
37
  // 仅首次启动且授权状态文件不存在时使用;更换口令需手动删除状态文件后重启。
38
38
  passphrase: '',
39
+ // 通过反向代理挂载在子路径时,为登录成功后的目标补此前缀;默认空值跳转到根路径。
40
+ redirectPrefix: '',
39
41
  stateFile: '.docnest/auth.json',
40
42
  // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
41
43
  localStorageKey: 'my-project:auth-passphrase',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",