dsh-usage-badge 0.1.5 → 0.1.6

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/lib/webdav.js CHANGED
@@ -259,6 +259,65 @@ const transport = (url, { method, headers, body, timeout }) =>
259
259
  req.end()
260
260
  })
261
261
 
262
+ /**
263
+ * The one place a WebDAV base URL is judged and normalised.
264
+ *
265
+ * Exported so the **save route** can refuse exactly what the client refuses. When the two had their own
266
+ * copies of this rule the host accepted addresses the client could never use — a query string, embedded
267
+ * credentials, `ftp:` — and the panel stored one and displayed it while the sync could not run at all.
268
+ * Two copies of a rule drift; one copy cannot.
269
+ *
270
+ * @param {string} baseUrl
271
+ * @param {{allowTrailingSlashFix?:boolean}} [options] `allowTrailingSlashFix` appends the slash a
272
+ * collection needs instead of refusing the address, which is what a person typing a host:port means.
273
+ * It is the **second** parameter on purpose: a single destructured parameter silently ignores a string
274
+ * argument and falls back to the permissive default, which is the opposite of failing loudly.
275
+ * @returns {{ok:true, url:string, insecure:boolean} | {ok:false, reason:string}}
276
+ */
277
+ export function normalizeWebdavUrl(baseUrl, options = {}) {
278
+ const { allowTrailingSlashFix = true } = options ?? {}
279
+ const text = String(baseUrl ?? '').trim()
280
+ let url
281
+ try {
282
+ url = new URL(text)
283
+ } catch {
284
+ return { ok: false, reason: `this is not a URL: ${baseUrl}` }
285
+ }
286
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
287
+ return { ok: false, reason: `the URL must be http or https, got ${url.protocol}` }
288
+ }
289
+ // **A host has to be there.** `new URL('http:///dav')` does not fail: WHATWG drops the empty authority
290
+ // and reads `dav` as the host, so a missing hostname silently retargets the address at another machine.
291
+ // Whoever typed three slashes meant to fill something in, not to point somewhere else.
292
+ const afterScheme = text.includes('://') ? text.slice(text.indexOf('://') + 3) : ''
293
+ if (afterScheme === '' || afterScheme.startsWith('/')) {
294
+ return { ok: false, reason: 'the URL has no host — it has to look like http://nas:19347/dav/' }
295
+ }
296
+ if (url.username || url.password) {
297
+ return { ok: false, reason: 'put the username and password in their own fields, not in the URL' }
298
+ }
299
+ if (url.search) return { ok: false, reason: 'the URL must not carry a query string' }
300
+ if (url.hash) return { ok: false, reason: 'the URL must not carry a #fragment' }
301
+ // An encoded slash is not a slash to the server: Apache's default `AllowEncodedSlashes` policy answers
302
+ // `%2f` with a 404, and appending a real slash after it produces a path nobody meant. Refused by name so
303
+ // the field says why, instead of being accepted and 404ing at the first request.
304
+ if (/%2f/i.test(url.pathname)) {
305
+ return { ok: false, reason: 'the URL contains an encoded slash (%2f), which this server family rejects by default — write the "/" literally' }
306
+ }
307
+ // `new URL('http://nas:19347').pathname` is `"/"`, not `""`: the root is a collection and already ends
308
+ // in its slash, so this is false for a bare host and nothing is appended. Only a **path** below the root
309
+ // that lacks the slash (`/dav`) reaches the branch.
310
+ if (!/\/$/.test(url.pathname)) {
311
+ if (!allowTrailingSlashFix) {
312
+ // The target is a collection; a WebDAV server answers a slash-less directory with a 301, and
313
+ // chasing that redirect is what makes a missing slash look like a wrong password.
314
+ return { ok: false, reason: `the URL must end with "/" (a collection), got ${baseUrl}` }
315
+ }
316
+ url.pathname = `${url.pathname}/`
317
+ }
318
+ return { ok: true, url: url.toString(), insecure: url.protocol === 'http:' }
319
+ }
320
+
262
321
  /**
263
322
  * A WebDAV client bound to one server and one set of credentials.
264
323
  *
@@ -275,25 +334,18 @@ export class WebDavClient {
275
334
  * digest HA1, which needs no plaintext at all).
276
335
  */
277
336
  constructor({ baseUrl, username, password = null, passwordHash = null, allowInsecureTls = false, timeoutMs = CONNECT_TIMEOUT_MS, uploadTimeoutMs = UPLOAD_TIMEOUT_MS, userAgent = 'dsh-usage-badge' }) {
278
- const url = new URL(baseUrl)
279
- if (url.protocol !== 'http:' && url.protocol !== 'https:') {
280
- throw new Error(`the WebDAV URL must be http or https, got ${url.protocol}`)
281
- }
282
- if (url.username || url.password) {
283
- throw new Error('put the username and password in their own fields, not in the URL')
284
- }
285
- if (url.search || url.hash) throw new Error('the WebDAV URL must not carry a query or fragment')
286
- if (!/\/$/.test(url.pathname)) {
287
- // Not normalised silently: the alias target is a directory, and a missing slash is a
288
- // common cause of 301s that look like authentication failures.
289
- throw new Error(`the WebDAV URL must end with "/" (a collection), got ${baseUrl}`)
290
- }
291
- if (url.protocol === 'http:' && !allowInsecureTls) {
337
+ // The same judgement the save route uses, in one place. `allowTrailingSlashFix` is off here on
338
+ // purpose: a person typing `host:port` should have the slash added for them by the route, but a client
339
+ // constructed from a stored configuration must be handed an address that is already right — silently
340
+ // changing it here would hide a configuration that was written wrong.
341
+ const judged = normalizeWebdavUrl(baseUrl, { allowTrailingSlashFix: false })
342
+ if (!judged.ok) throw new Error(judged.reason)
343
+ if (judged.insecure && !allowInsecureTls) {
292
344
  // Digest over plain HTTP is not a secret leak by itself, but it is brute-forceable by a
293
345
  // passive eavesdropper. Say so once here; the panel says it again.
294
346
  this.insecureWarning = 'this WebDAV URL is plain http://; digest authentication over an unencrypted connection is brute-forceable by anyone who can watch it'
295
347
  }
296
- this.baseUrl = url
348
+ this.baseUrl = new URL(judged.url)
297
349
  this.username = username
298
350
  this.password = password
299
351
  this.passwordHash = passwordHash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-usage-badge",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "DeepSeek Harness 的用量徽标插件:侧边栏底部一行显示当天的 token 总量与估算费用,点开可看 24 小时 / 近 7 日 / 近 30 日 / 近 12 个月 / 按年用量,按 provider 筛选,并按 DeepSeek 官方定价与中国法定节假日计价。A usage badge plugin for DeepSeek Harness: today's tokens and estimated spend in the sidebar, with 24h / 7d / 30d / 12-month and per-year charts, official DeepSeek pricing and Chinese public holidays.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",