docnest 0.1.10 → 0.1.11

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
@@ -77,7 +77,7 @@ export default defineConfig({
77
77
  授权只有一道关口,没有账号、密码组合或角色体系。首次启动时,`passphrase` 会初始化到由
78
78
  `stateFile` 指定的本地授权状态文件;状态文件只保存带随机盐的口令哈希,不保存明文口令。
79
79
  按极简模式约定,前端会把可回填的口令永久保存到浏览器 `localStorage`(不写入 URL);每次打开或刷新文档页面,前端都会把当前缓存的口令交给后台重新校验。校验失败会回到口令页,校验成功才会继续打开当前页面;输入页会保留跳转前的目标地址,错误口令会停留在输入页。由于 `localStorage` 是浏览器本地明文存储,请只在受信任的浏览器环境使用此模式。
80
- `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。右上角的“修改口令”可以动态更新口令,修改后会清除本地缓存并使所有现有会话立即失效,必须使用新口令重新进入。
80
+ `localStorageKey` 可选,用于为不同宿主项目隔离浏览器存储键。内置授权不提供“修改口令”或“退出”按钮;需要更换口令时,停止服务、删除 `stateFile`,修改配置中的 `passphrase` 后重新启动。
81
81
  建议把 `.docnest/` 加入宿主项目的 `.gitignore`,并为口令状态文件设置仅当前用户可读写的权限。
82
82
 
83
83
  授权开启但既没有状态文件,也没有初始 `passphrase` 时,DocNest 会拒绝启动,避免意外以空口令开放文档。
package/dist/auth.d.ts CHANGED
@@ -12,7 +12,5 @@ export type DocNestAuthManager = {
12
12
  verifyPassphrase: (passphrase: string) => boolean;
13
13
  createSession: (passphrase: string) => string | null;
14
14
  authenticateSession: (token: string | undefined) => boolean;
15
- revokeSession: (token: string | undefined) => void;
16
- changePassphrase: (currentPassphrase: string, nextPassphrase: string) => boolean;
17
15
  };
18
16
  export declare function createAuthManager(config: DocNestAuthConfig): DocNestAuthManager;
package/dist/auth.js CHANGED
@@ -130,11 +130,9 @@ function createAuthManager(config) {
130
130
  verifyPassphrase: () => true,
131
131
  createSession: () => null,
132
132
  authenticateSession: () => true,
133
- revokeSession: () => undefined,
134
- changePassphrase: () => false,
135
133
  };
136
134
  }
137
- let state = readOrCreateState(config);
135
+ const state = readOrCreateState(config);
138
136
  function verifyPassphrase(passphrase) {
139
137
  return matchesHash(passphrase, state);
140
138
  }
@@ -163,19 +161,6 @@ function createAuthManager(config) {
163
161
  session.expiresAt = Date.now() + sessionMaxAgeSeconds * 1000;
164
162
  return true;
165
163
  }
166
- function revokeSession(token) {
167
- if (token)
168
- sessions.delete(token);
169
- }
170
- function changePassphrase(currentPassphrase, nextPassphrase) {
171
- if (!verifyPassphrase(currentPassphrase))
172
- return false;
173
- assertPassphrase(nextPassphrase, '新授权口令');
174
- state = createState(nextPassphrase);
175
- writeState(config.stateFile, state);
176
- sessions.clear();
177
- return true;
178
- }
179
164
  return {
180
165
  enabled,
181
166
  stateFile: config.stateFile,
@@ -184,7 +169,5 @@ function createAuthManager(config) {
184
169
  verifyPassphrase,
185
170
  createSession,
186
171
  authenticateSession,
187
- revokeSession,
188
- changePassphrase,
189
172
  };
190
173
  }
package/dist/server.js CHANGED
@@ -297,10 +297,7 @@ function renderLoginPage(res, nextPath, error, status = 200) {
297
297
  app.get('/login', (req, res) => {
298
298
  if (!BUILT_IN_AUTH_ENABLED)
299
299
  return res.redirect('/');
300
- const message = req.query.changed
301
- ? '口令已更新,请使用新口令重新进入。'
302
- : '';
303
- return renderLoginPage(res, req.query.next, message);
300
+ return renderLoginPage(res, req.query.next, '');
304
301
  });
305
302
  app.post('/auth/verify', (req, res) => {
306
303
  if (!BUILT_IN_AUTH_ENABLED)
@@ -323,29 +320,6 @@ app.post('/auth/login', (req, res) => {
323
320
  setAuthCookie(res, token);
324
321
  return res.redirect(nextPath);
325
322
  });
326
- app.post('/auth/logout', (req, res) => {
327
- AUTH_MANAGER.revokeSession(readAuthToken(req));
328
- clearAuthCookie(res);
329
- return res.redirect('/login');
330
- });
331
- app.post('/auth/change-password', (req, res) => {
332
- if (!BUILT_IN_AUTH_ENABLED || !AUTH_MANAGER.authenticateSession(readAuthToken(req))) {
333
- return res.status(401).json({ ok: false, error: '当前会话未授权。' });
334
- }
335
- try {
336
- const changed = AUTH_MANAGER.changePassphrase(String(req.body?.currentPassphrase || ''), String(req.body?.nextPassphrase || ''));
337
- if (!changed)
338
- return res.status(401).json({ ok: false, error: '当前口令不正确。' });
339
- clearAuthCookie(res);
340
- return res.json({ ok: true });
341
- }
342
- catch (error) {
343
- return res.status(400).json({
344
- ok: false,
345
- error: error instanceof Error ? error.message : '新口令无效。',
346
- });
347
- }
348
- });
349
323
  function documentAuthMiddleware(req, res, next) {
350
324
  if (!BUILT_IN_AUTH_ENABLED)
351
325
  return next();
@@ -34,10 +34,10 @@ export default defineConfig({
34
34
  auth: {
35
35
  // 极简单口令授权;关闭时文档中心保持本地开放。
36
36
  enabled: false,
37
- // 仅首次启动且授权状态文件不存在时使用,之后可在页面内修改。
37
+ // 仅首次启动且授权状态文件不存在时使用;更换口令需手动删除状态文件后重启。
38
38
  passphrase: '',
39
39
  stateFile: '.docnest/auth.json',
40
- // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到用户退出或修改口令。
40
+ // 前端 localStorage 的键;口令会永久保存在浏览器本地,直到手动清除或口令失效。
41
41
  localStorageKey: 'my-project:auth-passphrase',
42
42
  sessionTtlMinutes: 24 * 60,
43
43
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docnest",
3
- "version": "0.1.10",
3
+ "version": "0.1.11",
4
4
  "description": "可复用的本地 Markdown 文档中心",
5
5
  "repository": {
6
6
  "type": "git",
@@ -5778,43 +5778,6 @@ html.docnest-auth-pending::before {
5778
5778
  font-size: 0.9rem;
5779
5779
  }
5780
5780
 
5781
- .auth-actions {
5782
- display: inline-flex;
5783
- align-items: center;
5784
- gap: 0.4rem;
5785
- }
5786
-
5787
- .auth-logout-form {
5788
- display: inline-flex;
5789
- margin: 0;
5790
- }
5791
-
5792
- .auth-change-password-btn,
5793
- .auth-logout-btn {
5794
- white-space: nowrap;
5795
- }
5796
-
5797
- .auth-modal[hidden] {
5798
- display: none;
5799
- }
5800
-
5801
- .auth-modal {
5802
- position: fixed;
5803
- inset: 0;
5804
- z-index: 1000;
5805
- display: grid;
5806
- place-items: center;
5807
- padding: 1.25rem;
5808
- }
5809
-
5810
- .auth-modal-backdrop {
5811
- position: absolute;
5812
- inset: 0;
5813
- background: rgba(10, 16, 24, 0.42);
5814
- backdrop-filter: blur(4px);
5815
- }
5816
-
5817
- .auth-modal-card,
5818
5781
  .auth-login-card {
5819
5782
  position: relative;
5820
5783
  z-index: 1;
@@ -5825,25 +5788,11 @@ html.docnest-auth-pending::before {
5825
5788
  box-shadow: 0 24px 70px rgba(17, 29, 40, 0.2);
5826
5789
  }
5827
5790
 
5828
- .auth-modal-card {
5829
- width: min(100%, 440px);
5830
- padding: 1.5rem;
5831
- }
5832
-
5833
- .auth-modal-header {
5834
- display: flex;
5835
- align-items: flex-start;
5836
- justify-content: space-between;
5837
- gap: 1rem;
5838
- }
5839
-
5840
- .auth-modal-header h2,
5841
5791
  .auth-login-card h1 {
5842
5792
  margin: 0;
5843
5793
  color: var(--linear-text, #18212a);
5844
5794
  }
5845
5795
 
5846
- .auth-modal-eyebrow,
5847
5796
  .auth-login-eyebrow {
5848
5797
  margin: 0 0 0.35rem;
5849
5798
  color: var(--linear-accent, #267c83);
@@ -5852,35 +5801,12 @@ html.docnest-auth-pending::before {
5852
5801
  letter-spacing: 0.12em;
5853
5802
  }
5854
5803
 
5855
- .auth-modal-close {
5856
- width: 2rem;
5857
- height: 2rem;
5858
- padding: 0;
5859
- border: 0;
5860
- border-radius: 999px;
5861
- color: var(--linear-text-muted, #63727f);
5862
- background: transparent;
5863
- font-size: 1.5rem;
5864
- line-height: 1;
5865
- cursor: pointer;
5866
- }
5867
-
5868
- .auth-modal-close:hover {
5869
- background: var(--linear-bg-subtle, #eef2f5);
5870
- }
5871
-
5872
- .auth-modal-help,
5873
5804
  .auth-login-description,
5874
5805
  .auth-login-note {
5875
5806
  color: var(--linear-text-muted, #63727f);
5876
5807
  line-height: 1.6;
5877
5808
  }
5878
5809
 
5879
- .auth-modal-help {
5880
- margin: 1rem 0 1.25rem;
5881
- font-size: 0.875rem;
5882
- }
5883
-
5884
5810
  .auth-form {
5885
5811
  display: grid;
5886
5812
  gap: 0.9rem;
@@ -5920,15 +5846,7 @@ html.docnest-auth-pending::before {
5920
5846
  font-size: 0.875rem;
5921
5847
  }
5922
5848
 
5923
- .auth-modal-actions {
5924
- display: flex;
5925
- justify-content: flex-end;
5926
- gap: 0.6rem;
5927
- margin-top: 0.25rem;
5928
- }
5929
-
5930
- .auth-primary-btn,
5931
- .auth-secondary-btn {
5849
+ .auth-primary-btn {
5932
5850
  min-height: 2.5rem;
5933
5851
  padding: 0.55rem 0.95rem;
5934
5852
  border-radius: 8px;
@@ -5943,12 +5861,6 @@ html.docnest-auth-pending::before {
5943
5861
  background: var(--linear-accent, #267c83);
5944
5862
  }
5945
5863
 
5946
- .auth-secondary-btn {
5947
- border: 1px solid var(--linear-border, #d7dee5);
5948
- color: var(--linear-text, #18212a);
5949
- background: var(--linear-bg-subtle, #eef2f5);
5950
- }
5951
-
5952
5864
  .auth-page {
5953
5865
  min-height: 100vh;
5954
5866
  display: grid;
@@ -6010,16 +5922,3 @@ html.docnest-auth-pending::before {
6010
5922
  font-size: 0.75rem;
6011
5923
  text-align: center;
6012
5924
  }
6013
-
6014
- @media (max-width: 768px) {
6015
- .auth-actions {
6016
- gap: 0.25rem;
6017
- }
6018
-
6019
- .auth-change-password-btn,
6020
- .auth-logout-btn {
6021
- min-width: 44px;
6022
- padding-inline: 0.5rem;
6023
- font-size: 0.75rem;
6024
- }
6025
- }
@@ -92,78 +92,6 @@
92
92
  document.documentElement.classList.remove('docnest-auth-pending');
93
93
  }
94
94
 
95
- function initChangePassword() {
96
- var modal = document.getElementById('auth-change-password-modal');
97
- var openButton = document.getElementById('auth-change-password-btn');
98
- var form = document.getElementById('auth-change-password-form');
99
- var error = document.getElementById('auth-change-password-error');
100
- if (!modal || !openButton || !form) return;
101
-
102
- var closeButtons = modal.querySelectorAll('[data-auth-close]');
103
-
104
- function setError(message) {
105
- if (!error) return;
106
- error.textContent = message || '';
107
- error.hidden = !message;
108
- }
109
-
110
- function closeModal() {
111
- modal.hidden = true;
112
- setError('');
113
- form.reset();
114
- }
115
-
116
- openButton.addEventListener('click', function () {
117
- modal.hidden = false;
118
- var firstInput = form.querySelector('input');
119
- if (firstInput) firstInput.focus();
120
- });
121
-
122
- closeButtons.forEach(function (button) {
123
- button.addEventListener('click', closeModal);
124
- });
125
-
126
- form.addEventListener('submit', async function (event) {
127
- event.preventDefault();
128
- setError('');
129
- var formData = new FormData(form);
130
- var nextPassphrase = String(formData.get('nextPassphrase') || '');
131
- var confirmPassphrase = String(formData.get('confirmPassphrase') || '');
132
- if (nextPassphrase !== confirmPassphrase) {
133
- setError('两次输入的新口令不一致。');
134
- return;
135
- }
136
-
137
- try {
138
- var response = await fetch('/auth/change-password', {
139
- method: 'POST',
140
- credentials: 'same-origin',
141
- headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
142
- body: JSON.stringify({
143
- currentPassphrase: String(formData.get('currentPassphrase') || ''),
144
- nextPassphrase: nextPassphrase,
145
- }),
146
- });
147
- var result = await response.json().catch(function () { return {}; });
148
- if (!response.ok || !result.ok) {
149
- throw new Error(result.error || '修改口令失败,请重试。');
150
- }
151
- clearPassphrase();
152
- window.location.assign('/login?changed=1');
153
- } catch (requestError) {
154
- setError(requestError instanceof Error ? requestError.message : '修改口令失败,请重试。');
155
- }
156
- });
157
- }
158
-
159
- function initLogout() {
160
- document.querySelectorAll('.auth-logout-form').forEach(function (form) {
161
- form.addEventListener('submit', function () {
162
- clearPassphrase();
163
- });
164
- });
165
- }
166
-
167
95
  function initLoginPage() {
168
96
  var form = document.querySelector('.auth-login-form');
169
97
  var error = document.querySelector('.auth-login-form .auth-form-error');
@@ -211,8 +139,6 @@
211
139
  return;
212
140
  }
213
141
  removePendingState();
214
- initChangePassword();
215
- initLogout();
216
142
  });
217
143
  }
218
144
 
@@ -54,52 +54,10 @@
54
54
  <span class="theme-icon" aria-hidden="true">A</span>
55
55
  <span class="theme-text">跟随系统</span>
56
56
  </button>
57
- <% if (docsAuthEnabled) { %>
58
- <div class="auth-actions" aria-label="文档授权">
59
- <button id="auth-change-password-btn" class="header-action-btn auth-change-password-btn" type="button">
60
- 修改口令
61
- </button>
62
- <form action="/auth/logout" method="post" class="auth-logout-form">
63
- <button class="header-action-btn auth-logout-btn" type="submit">退出</button>
64
- </form>
65
- </div>
66
- <% } %>
67
57
  </div>
68
58
  </div>
69
59
  </div>
70
60
  </header>
71
61
  <% if (docsAuthEnabled) { %>
72
- <div class="auth-modal" id="auth-change-password-modal" hidden>
73
- <div class="auth-modal-backdrop" data-auth-close></div>
74
- <section class="auth-modal-card" role="dialog" aria-modal="true" aria-labelledby="auth-change-password-title">
75
- <div class="auth-modal-header">
76
- <div>
77
- <p class="auth-modal-eyebrow">DOCUMENT ACCESS</p>
78
- <h2 id="auth-change-password-title">修改授权口令</h2>
79
- </div>
80
- <button type="button" class="auth-modal-close" data-auth-close aria-label="关闭">×</button>
81
- </div>
82
- <p class="auth-modal-help">修改后所有已打开的文档会退出授权,需要使用新口令重新进入。</p>
83
- <form id="auth-change-password-form" class="auth-form">
84
- <label>
85
- 当前口令
86
- <input type="password" name="currentPassphrase" autocomplete="current-password" required>
87
- </label>
88
- <label>
89
- 新口令
90
- <input type="password" name="nextPassphrase" autocomplete="new-password" required>
91
- </label>
92
- <label>
93
- 确认新口令
94
- <input type="password" name="confirmPassphrase" autocomplete="new-password" required>
95
- </label>
96
- <p class="auth-form-error" id="auth-change-password-error" role="alert" hidden></p>
97
- <div class="auth-modal-actions">
98
- <button type="button" class="auth-secondary-btn" data-auth-close>取消</button>
99
- <button type="submit" class="auth-primary-btn">保存新口令</button>
100
- </div>
101
- </form>
102
- </section>
103
- </div>
104
62
  <script src="/static/js/core/auth.js?v=<%= typeof assetVersion !== 'undefined' ? assetVersion : 'dev' %>"></script>
105
63
  <% } %>