dsh-sherlock 0.1.0 → 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 +13 -2
- package/README.md +21 -1
- package/README.zh.md +21 -1
- package/lib/core/engine.js +47 -3
- package/lib/dsh/tools.js +10 -4
- package/package.json +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.1.
|
|
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
|
+
|
|
10
|
+
## 0.1.1 — 2026-11-08
|
|
4
11
|
|
|
5
|
-
- Initial release.
|
|
12
|
+
- Initial public release (npm staged-publish path).
|
|
6
13
|
- `sherlock_check` and `sherlock_sites` tools over a pinned Sherlock manifest.
|
|
7
14
|
- Six result states with confidence and evidence.
|
|
8
15
|
- Random negative-control verification for claimed results.
|
|
9
16
|
- Per-host rate limiting, bounded response bodies, cancellation, and an in-memory cache.
|
|
10
17
|
- Optional `sherlock-osint` skill.
|
|
11
18
|
- 481-site data snapshot, 45 curated core names (41 effective), 47 exclusions.
|
|
19
|
+
|
|
20
|
+
## 0.1.0 — 2026-11-08
|
|
21
|
+
|
|
22
|
+
- Internal packaging iteration; not published to npm.
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# dsh-sherlock
|
|
2
2
|
|
|
3
|
+
[](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
|
+
[](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
|
|
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`。
|
package/lib/core/engine.js
CHANGED
|
@@ -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 =
|
|
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
|
-
...
|
|
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
|
-
/**
|
|
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.
|
|
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
|
}
|