dsh-usage-badge 0.1.5 → 0.1.7
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 +2 -2
- package/docs/design-notes.md +55 -8
- package/lib/cache-store.js +113 -14
- package/lib/client.js +160 -12
- package/lib/index.js +210 -39
- package/lib/sync.js +292 -57
- package/lib/webdav.js +67 -15
- package/package.json +1 -1
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|