dsh-sherlock 0.1.1 → 0.1.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 — 2026-11-08
4
+
5
+ - Added TypeScript/checkJs and oxlint gates.
6
+ - Hardened the runtime header filter and added security/edge tests.
7
+ - Made data validation sync-safe and added a data-sync fixture test and monthly workflow.
8
+ - Added benchmark tooling, governance docs, release/data runbooks, and Gitee release.
9
+
3
10
  ## 0.1.1 — 2026-11-08
4
11
 
5
12
  - Initial public release (npm staged-publish path).
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-sherlock
2
2
 
3
+ [![CI](https://github.com/PerryLink/dsh-sherlock/actions/workflows/ci.yml/badge.svg)](https://github.com/PerryLink/dsh-sherlock/actions/workflows/ci.yml)
4
+
3
5
  Sherlock-compatible public username checks for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH).
4
6
 
5
7
  It turns the [Sherlock](https://github.com/sherlock-project/sherlock) site manifest into a DSH-native bundle with two tools and an on-demand skill. It is deliberately **not** a 1:1 port of the Python CLI: blocked, unknown, and unreliable results stay separate from claimed and available ones.
@@ -120,17 +122,35 @@ The data is pinned so installs are reproducible. Update it with `scripts/sync-sh
120
122
  Requires Node 24.9+.
121
123
 
122
124
  ```bash
125
+ npm ci
126
+ npm run typecheck # tsc --checkJs, no emit
127
+ npm run lint # oxlint
123
128
  npm test # node --test, fixture server only
124
129
  npm run check # data-pack validation
125
130
  npm run pack:check # npm pack --dry-run
131
+ npm run gate # all of the above
126
132
  ```
127
133
 
128
- Tests cover status codes, messages, response URLs, WAF, redirects, POST payloads, custom headers, timeouts, body caps, exclusions, negative controls, caching, tool schemas, and data integrity. The real DSH schema validator can be used with:
134
+ Tests cover status codes, messages, response URLs, WAF, redirects, POST payloads, custom headers, timeouts, body caps, exclusions, negative controls, caching, private-host rejection, abort cancellation, array detectors, tool schemas, and data integrity. The real DSH schema validator can be used with:
129
135
 
130
136
  ```bash
131
137
  DSH_TOOLS_PATH=/path/to/dsh-tools/lib/index.js npm test
132
138
  ```
133
139
 
140
+ Live benchmark (network requests; not part of CI):
141
+
142
+ ```bash
143
+ node scripts/benchmark.mjs --username blue --scope core --verify=false
144
+ ```
145
+
146
+ ## More documentation
147
+
148
+ - [Data pack and sync runbook](docs/DATA.md)
149
+ - [Release runbook](docs/RELEASING.md)
150
+ - [Benchmark notes](docs/BENCHMARK.md)
151
+ - [Security policy](SECURITY.md)
152
+ - [Contributing](CONTRIBUTING.md)
153
+
134
154
  ## License
135
155
 
136
156
  Apache-2.0 for the plugin code; MIT for the Sherlock-derived data. See `LICENSE`, `NOTICE`, and `data/SHERLOCK-LICENSE`.
package/README.zh.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # dsh-sherlock
2
2
 
3
+ [![CI](https://github.com/PerryLink/dsh-sherlock/actions/workflows/ci.yml/badge.svg)](https://github.com/PerryLink/dsh-sherlock/actions/workflows/ci.yml)
4
+
3
5
  面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)的 Sherlock 兼容用户名检查插件。
4
6
 
5
7
  它把 [Sherlock](https://github.com/sherlock-project/sherlock) 的站点清单变成一个 DSH 原生 bundle:两个工具 + 一个按需加载的 skill。它不是 Python CLI 的 1:1 移植——**被拦截、未知、不可信**会和已占用、可用明确分开。
@@ -120,17 +122,35 @@ await tools.sherlock_check({ username: 'alice', scope: 'core' })
120
122
  需要 Node 24.9+。
121
123
 
122
124
  ```bash
125
+ npm ci
126
+ npm run typecheck # tsc --checkJs,不产物
127
+ npm run lint # oxlint
123
128
  npm test # node --test,只用本地 fixture
124
129
  npm run check # 数据校验
125
130
  npm run pack:check # npm pack --dry-run
131
+ npm run gate # 以上全部
126
132
  ```
127
133
 
128
- 测试覆盖状态码、message、response_url、WAF、重定向、POST payload、自定义 header、超时、body cap、排除项、负样本、缓存、工具 schema 和数据完整性。真实 DSH schema 校验:
134
+ 测试覆盖状态码、message、response_url、WAF、重定向、POST payload、自定义 header、超时、body cap、排除项、负样本、缓存、私有地址拒绝、abort 取消、数组检测器、工具 schema 和数据完整性。真实 DSH schema 校验:
129
135
 
130
136
  ```bash
131
137
  DSH_TOOLS_PATH=/path/to/dsh-tools/lib/index.js npm test
132
138
  ```
133
139
 
140
+ Live benchmark(会发真实网络请求,不属于 CI):
141
+
142
+ ```bash
143
+ node scripts/benchmark.mjs --username blue --scope core --verify=false
144
+ ```
145
+
146
+ ## 更多文档
147
+
148
+ - [数据包与同步 runbook](docs/DATA.md)
149
+ - [发布 runbook](docs/RELEASING.md)
150
+ - [Benchmark 记录](docs/BENCHMARK.md)
151
+ - [安全策略](SECURITY.md)
152
+ - [贡献指南](CONTRIBUTING.md)
153
+
134
154
  ## 许可
135
155
 
136
156
  插件代码 Apache-2.0;Sherlock 派生数据 MIT。见 `LICENSE`、`NOTICE`、`data/SHERLOCK-LICENSE`。
@@ -33,6 +33,7 @@ const WAF_FINGERPRINTS = [
33
33
  ]
34
34
 
35
35
  const PRIVATE_HOST_RE = /^(localhost|127\.|0\.0\.0\.0|\[::1\]|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|169\.254\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.|\[?fe80:|\[?fc|\[?fd)/i
36
+ const SAFE_HEADERS = /^(accept|accept-language|content-type|user-agent|referer|origin|sec-fetch-(mode|site|dest))$/i
36
37
 
37
38
  /**
38
39
  * @param {{data: any, regexes?: Map<string, RegExp>, config?: Record<string, unknown>}} options
@@ -75,6 +76,8 @@ export function normalizeUsername(input) {
75
76
  const value = input.normalize('NFC').trim().replace(/^@/, '')
76
77
  if (value.length === 0) return { ok: false, reason: 'username must not be empty' }
77
78
  if (value.length > 64) return { ok: false, reason: 'username must be at most 64 characters' }
79
+ // Control characters and URL syntax are deliberately rejected here.
80
+ // oxlint-disable-next-line no-control-regex
78
81
  if (/[\u0000-\u001f\u007f\s/\\?#%]/.test(value)) {
79
82
  return { ok: false, reason: 'username contains whitespace, control characters, or URL syntax' }
80
83
  }
@@ -97,6 +100,10 @@ function interpolate(value, username) {
97
100
  return value
98
101
  }
99
102
 
103
+ /**
104
+ * @param {any} engine
105
+ * @param {{sites?: string[], scope?: 'core'|'all', includeNsfw?: boolean, maxSites?: number}} [options]
106
+ */
100
107
  export function resolveSites(engine, { sites, scope = 'core', includeNsfw, maxSites } = {}) {
101
108
  const config = engine.config
102
109
  let pool = engine.data.sites.filter((site) => {
@@ -135,6 +142,11 @@ export function resolveSites(engine, { sites, scope = 'core', includeNsfw, maxSi
135
142
  return { selected: pool.slice(0, limit), unknown, truncated, eligible: pool.length }
136
143
  }
137
144
 
145
+ /**
146
+ * @param {any} engine
147
+ * @param {string} input
148
+ * @param {{scope?: 'core'|'all', sites?: string[], includeNsfw?: boolean, includeAvailable?: boolean, verify?: boolean, maxSites?: number, signal?: AbortSignal}} [options]
149
+ */
138
150
  export async function checkUsername(engine, input, options = {}) {
139
151
  const parsed = normalizeUsername(input)
140
152
  if (!parsed.ok) throw new Error(`invalid username: ${parsed.reason}`)
@@ -208,6 +220,10 @@ export async function checkUsername(engine, input, options = {}) {
208
220
  return value
209
221
  }
210
222
 
223
+ /**
224
+ * @param {any} engine
225
+ * @param {{scope?: 'core'|'all', includeNsfw?: boolean, query?: string, limit?: number}} [options]
226
+ */
211
227
  export function listSites(engine, { scope = 'core', includeNsfw, query, limit = 50 } = {}) {
212
228
  const config = engine.config
213
229
  let pool = engine.data.sites.filter((site) => {
@@ -268,8 +284,15 @@ function writeCache(engine, key, value) {
268
284
  }
269
285
  }
270
286
 
287
+ /**
288
+ * @template T
289
+ * @param {T[]} items
290
+ * @param {number} limit
291
+ * @param {(item: T, index: number) => Promise<any>} task
292
+ * @param {AbortSignal|undefined} signal
293
+ */
271
294
  async function mapLimit(items, limit, task, signal) {
272
- const results = new Array(items.length)
295
+ const results = Array.from({ length: items.length })
273
296
  let index = 0
274
297
  const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
275
298
  while (true) {
@@ -284,10 +307,17 @@ async function mapLimit(items, limit, task, signal) {
284
307
  return results
285
308
  }
286
309
 
310
+ /** @param {AbortSignal|undefined} signal */
287
311
  function throwIfAborted(signal) {
288
312
  if (signal?.aborted) throw signal.reason ?? new Error('aborted')
289
313
  }
290
314
 
315
+ /**
316
+ * @param {any} engine
317
+ * @param {any} site
318
+ * @param {string} username
319
+ * @param {{signal?: AbortSignal}} [options]
320
+ */
291
321
  async function checkSite(engine, site, username, { signal } = {}) {
292
322
  const started = performance.now()
293
323
  const elapsed = () => Math.round(performance.now() - started)
@@ -355,11 +385,14 @@ function buildRequest(engine, site, username) {
355
385
  const payload = site.payload === null || site.payload === undefined
356
386
  ? undefined
357
387
  : interpolate(site.payload, username)
388
+ const siteHeaders = Object.fromEntries(
389
+ Object.entries(interpolate(site.headers ?? {}, username)).filter(([key]) => SAFE_HEADERS.test(key)),
390
+ )
358
391
  const headers = {
359
392
  accept: 'text/html,application/xhtml+xml,application/json;q=0.9,*/*;q=0.8',
360
393
  'accept-language': 'en-US,en;q=0.9',
361
394
  'user-agent': engine.config.userAgent,
362
- ...interpolate(site.headers ?? {}, username),
395
+ ...siteHeaders,
363
396
  }
364
397
  let body
365
398
  if (payload !== undefined && !['GET', 'HEAD'].includes(site.method)) {
@@ -369,7 +402,7 @@ function buildRequest(engine, site, username) {
369
402
  return { url, method: site.method ?? 'GET', headers, body, site, username }
370
403
  }
371
404
 
372
- function assertPublicHttpUrl(raw, config = {}) {
405
+ export function assertPublicHttpUrl(raw, config = {}) {
373
406
  let url
374
407
  try {
375
408
  url = new URL(raw)
@@ -385,6 +418,10 @@ function assertPublicHttpUrl(raw, config = {}) {
385
418
  return url
386
419
  }
387
420
 
421
+ /**
422
+ * @param {{url: string, method: string, body?: string, headers: Record<string,string>}} request
423
+ * @param {{signal?: AbortSignal, maxRedirects?: number, allowPrivateHosts?: boolean}} [options]
424
+ */
388
425
  async function fetchWithRedirects(request, { signal, maxRedirects = 5, allowPrivateHosts = false } = {}) {
389
426
  let url = request.url
390
427
  let method = request.method
@@ -591,6 +628,13 @@ function combineOutcomes(outcomes) {
591
628
  return { status: STATUS.UNKNOWN, reason: 'no_outcome', confidence: 'low' }
592
629
  }
593
630
 
631
+ /**
632
+ * @param {any} engine
633
+ * @param {any} site
634
+ * @param {string} username
635
+ * @param {any} result
636
+ * @param {{signal?: AbortSignal}} [options]
637
+ */
594
638
  async function verifyClaimed(engine, site, username, result, { signal } = {}) {
595
639
  const control = controlUsername(site)
596
640
  if (!control) {
package/lib/dsh/tools.js CHANGED
@@ -209,6 +209,7 @@ export function createTools(engine, config, toolVersion) {
209
209
  /**
210
210
  * @param {unknown} args
211
211
  * @param {'core'|'all'} defaultScope
212
+ * @returns {{username: string, scope: 'core'|'all', sites?: string[], includeNsfw: boolean, includeAvailable: boolean, verify: boolean, maxSites?: number}}
212
213
  */
213
214
  function parseCheckArgs(args, defaultScope) {
214
215
  if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('arguments must be an object')
@@ -216,6 +217,7 @@ function parseCheckArgs(args, defaultScope) {
216
217
  if (typeof input.username !== 'string') throw new Error('username must be a string')
217
218
  const scope = input.scope ?? defaultScope
218
219
  if (scope !== 'core' && scope !== 'all') throw new Error('scope must be "core" or "all"')
220
+ const typedScope = /** @type {'core'|'all'} */ (scope)
219
221
  let sites
220
222
  if (input.sites !== undefined) {
221
223
  if (!Array.isArray(input.sites) || input.sites.length > 50) throw new Error('sites must be an array of at most 50 strings')
@@ -234,7 +236,7 @@ function parseCheckArgs(args, defaultScope) {
234
236
  }
235
237
  return {
236
238
  username: input.username,
237
- scope,
239
+ scope: typedScope,
238
240
  sites,
239
241
  includeNsfw: input.include_nsfw === true,
240
242
  includeAvailable: input.include_available === true,
@@ -243,12 +245,16 @@ function parseCheckArgs(args, defaultScope) {
243
245
  }
244
246
  }
245
247
 
246
- /** @param {unknown} args */
248
+ /**
249
+ * @param {unknown} args
250
+ * @returns {{query?: string, scope: 'core'|'all', includeNsfw: boolean, limit: number}}
251
+ */
247
252
  function parseSitesArgs(args) {
248
253
  if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('arguments must be an object')
249
254
  const input = /** @type {Record<string, unknown>} */ (args)
250
255
  const scope = input.scope ?? 'core'
251
256
  if (scope !== 'core' && scope !== 'all') throw new Error('scope must be "core" or "all"')
257
+ const typedScope = /** @type {'core'|'all'} */ (scope)
252
258
  if (input.query !== undefined && typeof input.query !== 'string') throw new Error('query must be a string')
253
259
  if (input.include_nsfw !== undefined && typeof input.include_nsfw !== 'boolean') throw new Error('include_nsfw must be a boolean')
254
260
  let limit = 50
@@ -259,8 +265,8 @@ function parseSitesArgs(args) {
259
265
  limit = /** @type {number} */ (input.limit)
260
266
  }
261
267
  return {
262
- query: input.query,
263
- scope,
268
+ query: typeof input.query === 'string' ? input.query : undefined,
269
+ scope: typedScope,
264
270
  includeNsfw: input.include_nsfw === true,
265
271
  limit,
266
272
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-sherlock",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Sherlock-compatible public username checks for DeepSeek Harness, with blocked/unknown results kept separate.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -35,6 +35,9 @@
35
35
  "scripts": {
36
36
  "test": "node --test test/*.test.js",
37
37
  "check": "node scripts/validate-data.mjs",
38
+ "typecheck": "tsc -p tsconfig.json",
39
+ "lint": "oxlint lib scripts test index.js",
40
+ "gate": "npm run check && npm run typecheck && npm run lint && npm test && npm run pack:check",
38
41
  "pack:check": "npm pack --dry-run",
39
42
  "prepack": "node scripts/validate-data.mjs"
40
43
  },
@@ -59,5 +62,10 @@
59
62
  "bugs": {
60
63
  "url": "https://github.com/PerryLink/dsh-sherlock/issues"
61
64
  },
62
- "icon": "./icon.svg"
65
+ "icon": "./icon.svg",
66
+ "devDependencies": {
67
+ "@types/node": "24.19.2",
68
+ "oxlint": "1.92.0",
69
+ "typescript": "7.0.2"
70
+ }
63
71
  }