dsh-standards 0.0.3 → 1.0.1
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/40-checker/README.md +251 -0
- package/40-checker/bin/dsh-levels.mjs +15 -0
- package/40-checker/bin/dsh-plugin-verify.mjs +8 -0
- package/40-checker/fixtures/bad/index.mjs +12 -0
- package/40-checker/fixtures/bad/package.json +15 -0
- package/40-checker/fixtures/bad-manifest/index.js +1 -0
- package/40-checker/fixtures/bad-manifest/package.json +5 -0
- package/40-checker/fixtures/bundle/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle/package.json +15 -0
- package/40-checker/fixtures/bundle/server.mjs +7 -0
- package/40-checker/fixtures/bundle-v0/cordis.patch.yml +3 -0
- package/40-checker/fixtures/bundle-v0/package.json +21 -0
- package/40-checker/fixtures/bundle-v0/server.mjs +7 -0
- package/40-checker/fixtures/client-missing/client.js +1 -0
- package/40-checker/fixtures/client-missing/index.js +1 -0
- package/40-checker/fixtures/client-missing/package.json +6 -0
- package/40-checker/fixtures/flow-config/index.js +9 -0
- package/40-checker/fixtures/flow-config/package.json +10 -0
- package/40-checker/fixtures/fs-write/index.mjs +7 -0
- package/40-checker/fixtures/fs-write/package.json +10 -0
- package/40-checker/fixtures/good/index.mjs +10 -0
- package/40-checker/fixtures/good/package.json +19 -0
- package/40-checker/fixtures/hang/index.mjs +5 -0
- package/40-checker/fixtures/hang/package.json +10 -0
- package/40-checker/fixtures/known-answer/expected.json +18 -0
- package/40-checker/fixtures/market-index-real.json +18 -0
- package/40-checker/fixtures/market-index.json +18 -0
- package/40-checker/fixtures/negative/bad-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-entry/package.json +1 -0
- package/40-checker/fixtures/negative/no-patch/package.json +1 -0
- package/40-checker/fixtures/negative/root-missing/cordis.patch.yml +2 -0
- package/40-checker/fixtures/negative/root-missing/package.json +1 -0
- package/40-checker/fixtures/negative/workspace/package.json +1 -0
- package/40-checker/package.json +22 -0
- package/40-checker/samples/README.md +8 -0
- package/40-checker/samples/entry18.json +139 -0
- package/40-checker/samples/frame.json +485 -0
- package/40-checker/scripts/apply-overlay.mjs +59 -0
- package/40-checker/scripts/attestation-check.mjs +151 -0
- package/40-checker/scripts/bundle-smoke.mjs +126 -0
- package/40-checker/scripts/claim-check.mjs +145 -0
- package/40-checker/scripts/clause-map.mjs +94 -0
- package/40-checker/scripts/collect-fixtures.mjs +44 -0
- package/40-checker/scripts/dashboard.mjs +85 -0
- package/40-checker/scripts/dco-check.mjs +147 -0
- package/40-checker/scripts/dependabot-check.mjs +40 -0
- package/40-checker/scripts/doc-commands-check.mjs +92 -0
- package/40-checker/scripts/entry-scan.mjs +190 -0
- package/40-checker/scripts/entry-validate.mjs +291 -0
- package/40-checker/scripts/entry18-attribution.mjs +66 -0
- package/40-checker/scripts/entry18-ci.mjs +685 -0
- package/40-checker/scripts/entry18-realweb-summary.mjs +71 -0
- package/40-checker/scripts/entry18-web.mjs +307 -0
- package/40-checker/scripts/evidence-chain.mjs +100 -0
- package/40-checker/scripts/flow-index.mjs +82 -0
- package/40-checker/scripts/flow-sample-ci.mjs +529 -0
- package/40-checker/scripts/governance-check.mjs +210 -0
- package/40-checker/scripts/host-farm.mjs +165 -0
- package/40-checker/scripts/journey-config-observe.mjs +109 -0
- package/40-checker/scripts/journey-disclosure-shape.mjs +83 -0
- package/40-checker/scripts/journey-runtime.mjs +178 -0
- package/40-checker/scripts/journey.mjs +76 -0
- package/40-checker/scripts/label-drift.mjs +74 -0
- package/40-checker/scripts/label-sync.mjs +87 -0
- package/40-checker/scripts/label-verify.mjs +90 -0
- package/40-checker/scripts/landlock-exec.c +101 -0
- package/40-checker/scripts/level-audit.mjs +336 -0
- package/40-checker/scripts/m1-sidecar.mjs +126 -0
- package/40-checker/scripts/m2-runner.mjs +213 -0
- package/40-checker/scripts/round-robin.mjs +88 -0
- package/40-checker/scripts/sample-frame.mjs +72 -0
- package/40-checker/scripts/sample20-ci.mjs +25 -0
- package/40-checker/scripts/sample60-ci.mjs +25 -0
- package/40-checker/scripts/stale-check.mjs +59 -0
- package/40-checker/scripts/surveillance.mjs +88 -0
- package/40-checker/scripts/web-probe.mjs +504 -0
- package/40-checker/src/cli.mjs +300 -0
- package/40-checker/src/client-shape.mjs +37 -0
- package/40-checker/src/declarations.mjs +242 -0
- package/40-checker/src/index.mjs +8 -0
- package/40-checker/src/install-spec.mjs +67 -0
- package/40-checker/src/label.mjs +130 -0
- package/40-checker/src/manifest.mjs +381 -0
- package/40-checker/src/permissions.mjs +60 -0
- package/40-checker/src/report.mjs +204 -0
- package/40-checker/src/result.mjs +60 -0
- package/40-checker/src/runtime.mjs +299 -0
- package/40-checker/src/semver.mjs +194 -0
- package/40-checker/src/util.mjs +148 -0
- package/40-checker/test/attestation.test.mjs +59 -0
- package/40-checker/test/checks.test.mjs +172 -0
- package/40-checker/test/claim.test.mjs +28 -0
- package/40-checker/test/client-shape.test.mjs +35 -0
- package/40-checker/test/dco.test.mjs +51 -0
- package/40-checker/test/entry-validate.test.mjs +172 -0
- package/40-checker/test/governance.test.mjs +76 -0
- package/40-checker/test/label.test.mjs +97 -0
- package/40-checker/test/level-audit.test.mjs +99 -0
- package/40-checker/test/levels.test.mjs +31 -0
- package/40-checker/test/ops.test.mjs +117 -0
- package/40-checker/test/permissions.test.mjs +40 -0
- package/40-checker/test/semver.test.mjs +32 -0
- package/40-checker/test/verify.test.mjs +57 -0
- package/CHANGELOG.md +24 -0
- package/LICENSE +28 -0
- package/LICENSE-APACHE +190 -0
- package/LICENSE-CC-BY +23 -0
- package/NOTICE +14 -0
- package/NOTICE.md +11 -0
- package/README.md +146 -3
- package/bin/dsh-levels.mjs +4 -0
- package/bin/dsh-standards.mjs +19 -0
- package/docs/BOOST-FIXTURES-2026-11-02.md +38 -0
- package/docs/DEVELOPER-JOURNEY.md +40 -0
- package/docs/HOST-J3-BUG-REPORT-DRAFT.md +27 -0
- package/docs/HOST-J3-CONFIG-REPRO-2026-11-02.md +36 -0
- package/docs/NAMING-FALLBACK.md +11 -0
- package/docs/fork-policy.md +12 -0
- package/docs/full-test-plan-2026-11-02.md +207 -0
- package/docs/incident.md +37 -0
- package/docs/onboarding.md +27 -0
- package/docs/privacy.md +12 -0
- package/docs/release.md +16 -0
- package/docs/scope-security.md +62 -0
- package/docs/status-taxonomy.md +54 -0
- package/docs/sunset.md +44 -0
- package/docs/translating.md +12 -0
- package/package.json +58 -12
- package/spec/README.md +27 -0
- package/spec/boost/README.md +8 -0
- package/spec/clauses.json +253 -0
- package/spec/entry/README.md +9 -0
- package/spec/flow/README.md +8 -0
- package/spec/levels.json +371 -0
- package/spec/levels.schema.json +45 -0
- package/spec/reference-suite.json +35 -0
- package/spec/schemas/attestation.schema.json +284 -0
- package/spec/schemas/evidence.schema.json +331 -0
- package/spec/schemas/report.schema.json +64 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dco-check.mjs — enforce DCO 1.1 sign-off and the DSH contribution terms.
|
|
4
|
+
*
|
|
5
|
+
* Rules:
|
|
6
|
+
* 1. every commit has at least one `Signed-off-by: Name <email>`;
|
|
7
|
+
* 2. the author email matches one of the sign-off emails;
|
|
8
|
+
* 3. `AI-Assisted: yes` requires `AI-Tool:` and a human sign-off;
|
|
9
|
+
* 4. with --require-terms, at least one commit carries `DSH-Contribution: dsh-ct/1`
|
|
10
|
+
* or the PR body contains the terms checkbox.
|
|
11
|
+
*
|
|
12
|
+
* This checks attestations, not identity. It cannot verify legal capacity,
|
|
13
|
+
* employment, or patent status.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node 40-checker/scripts/dco-check.mjs --range origin/main..HEAD --require-terms --pr-body-env PR_BODY
|
|
17
|
+
* node 40-checker/scripts/dco-check.mjs --range HEAD~1..HEAD --json
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'node:fs'
|
|
20
|
+
import path from 'node:path'
|
|
21
|
+
import { spawnSync } from 'node:child_process'
|
|
22
|
+
import { fileURLToPath } from 'node:url'
|
|
23
|
+
|
|
24
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
25
|
+
const DEFAULT_ROOT = path.resolve(HERE, '..', '..')
|
|
26
|
+
const TERMS = 'dsh-ct/1'
|
|
27
|
+
|
|
28
|
+
export function parseLog(text) {
|
|
29
|
+
return text
|
|
30
|
+
.split('\x1e')
|
|
31
|
+
.map((record) => record.trim())
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map((record) => {
|
|
34
|
+
const [sha, authorName, authorEmail, ...rest] = record.split('\x1f')
|
|
35
|
+
return { sha: (sha || '').trim(), authorName: authorName || '', authorEmail: authorEmail || '', body: rest.join('\x1f') }
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function checkCommit(commit) {
|
|
40
|
+
const errors = []
|
|
41
|
+
const warnings = []
|
|
42
|
+
const body = commit.body || ''
|
|
43
|
+
const signoffs = [...body.matchAll(/^Signed-off-by:\s*(.+?)\s*<([^>]+)>\s*$/gim)]
|
|
44
|
+
.map((m) => ({ name: m[1].trim(), email: m[2].trim().toLowerCase() }))
|
|
45
|
+
if (signoffs.length === 0) {
|
|
46
|
+
errors.push(`${commit.sha.slice(0, 8)}: missing Signed-off-by`)
|
|
47
|
+
} else {
|
|
48
|
+
const emails = new Set(signoffs.map((s) => s.email))
|
|
49
|
+
if (commit.authorEmail && !emails.has(commit.authorEmail.toLowerCase())) {
|
|
50
|
+
errors.push(`${commit.sha.slice(0, 8)}: author email ${commit.authorEmail} is not among the sign-offs`)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const aiAssisted = /^AI-Assisted:\s*yes\s*$/im.test(body) || /^AI-Generated:\s*yes\s*$/im.test(body)
|
|
54
|
+
if (aiAssisted) {
|
|
55
|
+
if (!/^AI-Tool:\s*\S+/im.test(body)) errors.push(`${commit.sha.slice(0, 8)}: AI-Assisted without AI-Tool`)
|
|
56
|
+
const human = signoffs.some((s) => !/\bbot\b|\[bot\]/i.test(s.name))
|
|
57
|
+
if (!human) errors.push(`${commit.sha.slice(0, 8)}: AI-Assisted needs a human sign-off`)
|
|
58
|
+
}
|
|
59
|
+
const terms = /^(?:DSH-Contribution|Contribution-Agreement):\s*dsh-ct\/1\s*$/im.test(body)
|
|
60
|
+
const entity = /^Entity-CLA:\s*(\S+)\s*$/im.exec(body)
|
|
61
|
+
const onBehalf = /^On-Behalf-Of:\s*(.+?)\s*$/im.exec(body)
|
|
62
|
+
const agent = /^AI-Agent:\s*(\S+)\s*$/im.exec(body)
|
|
63
|
+
if (onBehalf && !entity) warnings.push(`${commit.sha.slice(0, 8)}: On-Behalf-Of present; verify the Entity CLA manually`)
|
|
64
|
+
return {
|
|
65
|
+
sha: commit.sha,
|
|
66
|
+
errors,
|
|
67
|
+
warnings,
|
|
68
|
+
terms,
|
|
69
|
+
entityCla: entity ? entity[1] : null,
|
|
70
|
+
onBehalfOf: onBehalf ? onBehalf[1] : null,
|
|
71
|
+
aiAgent: agent ? agent[1] : null,
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function checkTerms({ commits, prBody, requireTerms }) {
|
|
76
|
+
if (!requireTerms) return { errors: [], warnings: [] }
|
|
77
|
+
const fromCommit = commits.some((c) => /^(?:DSH-Contribution|Contribution-Agreement):\s*dsh-ct\/1\s*$/im.test(c.body || ''))
|
|
78
|
+
const fromPr = /\[[xX]\]\s*I agree to the DSH Contribution Terms v1/i.test(prBody || '')
|
|
79
|
+
|| /dsh-ct\/1/i.test(prBody || '')
|
|
80
|
+
if (!fromCommit && !fromPr) {
|
|
81
|
+
return { errors: ['no DSH Contribution Terms acceptance: add the PR checkbox or DSH-Contribution: dsh-ct/1'], warnings: [] }
|
|
82
|
+
}
|
|
83
|
+
return { errors: [], warnings: [] }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function run({ root = DEFAULT_ROOT, range = 'HEAD~1..HEAD', prBody = '', requireTerms = false } = {}) {
|
|
87
|
+
const result = spawnSync('git', ['-C', root, 'log', '--format=%H%x1f%an%x1f%ae%x1f%B%x1e', range], { encoding: 'utf8' })
|
|
88
|
+
if (result.status !== 0) {
|
|
89
|
+
return { ok: false, errors: [`git log failed: ${(result.stderr || '').trim()}`], warnings: [], commits: [] }
|
|
90
|
+
}
|
|
91
|
+
const commits = parseLog(result.stdout)
|
|
92
|
+
if (commits.length === 0) {
|
|
93
|
+
return { ok: false, errors: [`no commits in range ${range}`], warnings: [], commits: [] }
|
|
94
|
+
}
|
|
95
|
+
const errors = []
|
|
96
|
+
const warnings = []
|
|
97
|
+
const checked = commits.map((commit) => {
|
|
98
|
+
const r = checkCommit(commit)
|
|
99
|
+
errors.push(...r.errors)
|
|
100
|
+
warnings.push(...r.warnings)
|
|
101
|
+
return { sha: commit.sha, terms: r.terms, entityCla: r.entityCla, aiAgent: r.aiAgent }
|
|
102
|
+
})
|
|
103
|
+
const terms = checkTerms({ commits, prBody, requireTerms })
|
|
104
|
+
errors.push(...terms.errors)
|
|
105
|
+
warnings.push(...terms.warnings)
|
|
106
|
+
if (checked.some((c) => c.entityCla)) warnings.push('Entity-CLA trailer present; verify the archived document manually')
|
|
107
|
+
return { ok: errors.length === 0, errors, warnings, commits: checked }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function main(argv = process.argv.slice(2)) {
|
|
111
|
+
let root = DEFAULT_ROOT
|
|
112
|
+
let range = 'HEAD~1..HEAD'
|
|
113
|
+
let prBody = ''
|
|
114
|
+
let prBodyEnv = null
|
|
115
|
+
let requireTerms = false
|
|
116
|
+
let json = false
|
|
117
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
118
|
+
const arg = argv[i]
|
|
119
|
+
if (arg === '--root') root = path.resolve(argv[++i])
|
|
120
|
+
else if (arg === '--range') range = argv[++i]
|
|
121
|
+
else if (arg === '--pr-body') prBody = fs.readFileSync(argv[++i], 'utf8')
|
|
122
|
+
else if (arg === '--pr-body-env') prBodyEnv = argv[++i]
|
|
123
|
+
else if (arg === '--require-terms') requireTerms = true
|
|
124
|
+
else if (arg === '--json') json = true
|
|
125
|
+
else if (arg === '--help' || arg === '-h') {
|
|
126
|
+
console.log('usage: dco-check.mjs [--root <dir>] [--range <git-range>] [--pr-body <file>] [--pr-body-env <ENV>] [--require-terms] [--json]')
|
|
127
|
+
return 0
|
|
128
|
+
} else {
|
|
129
|
+
console.error(`unknown argument: ${arg}`)
|
|
130
|
+
return 2
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (prBodyEnv) prBody = process.env[prBodyEnv] || ''
|
|
134
|
+
const result = run({ root, range, prBody, requireTerms })
|
|
135
|
+
if (json) {
|
|
136
|
+
console.log(JSON.stringify(result, null, 2))
|
|
137
|
+
} else {
|
|
138
|
+
for (const warning of result.warnings) console.log(`WARN ${warning}`)
|
|
139
|
+
for (const error of result.errors) console.log(`FAIL ${error}`)
|
|
140
|
+
console.log(`${result.ok ? 'PASS' : 'FAIL'} dco: ${result.commits.length} commit(s), ${result.errors.length} error(s), ${result.warnings.length} warning(s)`)
|
|
141
|
+
}
|
|
142
|
+
return result.ok ? 0 : 2
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
146
|
+
process.exitCode = main()
|
|
147
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* dependabot-check.mjs — enforce the owner policy: no Dependabot in this repo.
|
|
4
|
+
*
|
|
5
|
+
* Fails when `.github/dependabot.yml`/`.yaml` exists, or when a workflow still
|
|
6
|
+
* references Dependabot (for example an auto-merge job). The policy is recorded
|
|
7
|
+
* in `46`/`51`; this script makes it machine-checkable.
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
14
|
+
const ROOT = path.resolve(HERE, '..', '..')
|
|
15
|
+
const errors = []
|
|
16
|
+
|
|
17
|
+
for (const name of ['dependabot.yml', 'dependabot.yaml']) {
|
|
18
|
+
const file = path.join(ROOT, '.github', name)
|
|
19
|
+
if (fs.existsSync(file)) errors.push(`found ${path.relative(ROOT, file)}`)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const workflows = path.join(ROOT, '.github', 'workflows')
|
|
23
|
+
if (fs.existsSync(workflows)) {
|
|
24
|
+
for (const name of fs.readdirSync(workflows)) {
|
|
25
|
+
if (!/\.ya?ml$/.test(name)) continue
|
|
26
|
+
const text = fs.readFileSync(path.join(workflows, name), 'utf8')
|
|
27
|
+
// Only automation references count. The policy guard itself lives in a
|
|
28
|
+
// workflow and must not trip this check.
|
|
29
|
+
if (/dependabot\[bot\]|dependabot\/fetch-metadata|dependabot-auto-merge|dependabot\/merge|dependabot\/.*action/i.test(text)) {
|
|
30
|
+
errors.push(`workflow ${name} references Dependabot automation`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (errors.length) {
|
|
36
|
+
for (const error of errors) console.error(`FAIL dependabot: ${error}`)
|
|
37
|
+
console.error(`FAIL dependabot: ${errors.length} error(s); owner policy is no Dependabot`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
console.log('PASS dependabot: no config or automation reference')
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* doc-commands-check.mjs — keep the developer path first-party and runnable.
|
|
4
|
+
*
|
|
5
|
+
* Two checks:
|
|
6
|
+
* 1. Current developer docs must not send readers to `npx dsh-levels`,
|
|
7
|
+
* `npx dsh-plugin-kit`, or `npx dsh-plugin-verify`; those bare npm names
|
|
8
|
+
* belong to other projects, not this repository.
|
|
9
|
+
* 2. The first-party template in `examples/plugin-template` must still audit
|
|
10
|
+
* to a non-`not-run` ENTRY state.
|
|
11
|
+
*
|
|
12
|
+
* This is a documentation/CLI guard, not a runtime or security claim.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import os from 'node:os'
|
|
16
|
+
import path from 'node:path'
|
|
17
|
+
import { spawnSync } from 'node:child_process'
|
|
18
|
+
import { fileURLToPath } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
const STANDARDS = path.resolve(HERE, '..', '..')
|
|
22
|
+
const DOCS = [
|
|
23
|
+
'45-三用途落地-官方市场开发者.md',
|
|
24
|
+
...fs.readdirSync(path.join(STANDARDS, 'adoption', 'developer'))
|
|
25
|
+
.filter((name) => name.endsWith('.md'))
|
|
26
|
+
.map((name) => path.join('adoption', 'developer', name)),
|
|
27
|
+
]
|
|
28
|
+
const FORBIDDEN = [
|
|
29
|
+
{ re: /\bnpx\s+dsh-levels\b/, name: 'npx dsh-levels' },
|
|
30
|
+
{ re: /\bnpx\s+dsh-plugin-kit\b/, name: 'npx dsh-plugin-kit' },
|
|
31
|
+
{ re: /\bnpx\s+dsh-plugin-verify\b/, name: 'npx dsh-plugin-verify' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
let errors = 0
|
|
35
|
+
for (const rel of DOCS) {
|
|
36
|
+
const text = fs.readFileSync(path.join(STANDARDS, rel), 'utf8')
|
|
37
|
+
const lines = text.split(/\r?\n/)
|
|
38
|
+
let inFence = false
|
|
39
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
40
|
+
const line = lines[index]
|
|
41
|
+
if (/^\s*```/.test(line)) {
|
|
42
|
+
inFence = !inFence
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (!inFence) continue
|
|
46
|
+
for (const rule of FORBIDDEN) {
|
|
47
|
+
if (rule.re.test(line)) {
|
|
48
|
+
console.error(`FAIL ${rel}:${index + 1}: ${rule.name} resolves to a third-party npm package`)
|
|
49
|
+
errors += 1
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (errors === 0) console.log(`PASS docs: 0 third-party npx command(s) in ${DOCS.length} file(s)`)
|
|
55
|
+
|
|
56
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-doc-commands-'))
|
|
57
|
+
try {
|
|
58
|
+
const out = path.join(tmp, 'audit.json')
|
|
59
|
+
const run = spawnSync(process.execPath, [
|
|
60
|
+
path.join(STANDARDS, '40-checker', 'bin', 'dsh-levels.mjs'),
|
|
61
|
+
'audit',
|
|
62
|
+
path.join(STANDARDS, 'examples', 'plugin-template'),
|
|
63
|
+
'--format', 'json',
|
|
64
|
+
'--out', out,
|
|
65
|
+
], { encoding: 'utf8' })
|
|
66
|
+
|
|
67
|
+
if (run.status !== 0) {
|
|
68
|
+
console.error(`FAIL template: dsh-levels audit exited ${run.status}`)
|
|
69
|
+
if (run.stderr) console.error(run.stderr.trim())
|
|
70
|
+
errors += 1
|
|
71
|
+
} else {
|
|
72
|
+
const report = JSON.parse(fs.readFileSync(out, 'utf8'))
|
|
73
|
+
const state = report?.levels?.entry?.state
|
|
74
|
+
if (!['recognized', 'static', 'runtime'].includes(state)) {
|
|
75
|
+
console.error(`FAIL template: ENTRY state is ${JSON.stringify(state)}, expected recognized/static/runtime`)
|
|
76
|
+
errors += 1
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`PASS template: ENTRY state=${state}`)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error(`FAIL template check: ${error.message}`)
|
|
83
|
+
errors += 1
|
|
84
|
+
} finally {
|
|
85
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (errors > 0) {
|
|
89
|
+
console.error(`FAIL doc-commands: ${errors} error(s)`)
|
|
90
|
+
process.exit(1)
|
|
91
|
+
}
|
|
92
|
+
console.log('PASS doc-commands: first-party developer path is runnable')
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* entry-scan.mjs — read-only C1-C8 scan of public GitHub plugins.
|
|
4
|
+
*
|
|
5
|
+
* It fetches package.json and checks that the referenced patch/client/entry
|
|
6
|
+
* files exist via the GitHub contents API. It stores only package.json and
|
|
7
|
+
* placeholder files; it never executes plugin code. C3 is left not-run because
|
|
8
|
+
* a real install line was not tested on a host.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'node:fs'
|
|
11
|
+
import os from 'node:os'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { spawnSync } from 'node:child_process'
|
|
14
|
+
import { fileURLToPath } from 'node:url'
|
|
15
|
+
|
|
16
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const STD = path.resolve(HERE, '..', '..')
|
|
18
|
+
const VALIDATOR = path.join(STD, '40-checker', 'scripts', 'entry-validate.mjs')
|
|
19
|
+
const TOKEN = process.env.GH_TOKEN
|
|
20
|
+
if (!TOKEN) throw new Error('GH_TOKEN is required')
|
|
21
|
+
|
|
22
|
+
const headers = {
|
|
23
|
+
Authorization: `Bearer ${TOKEN}`,
|
|
24
|
+
Accept: 'application/vnd.github+json',
|
|
25
|
+
'User-Agent': 'dsh-entry-scan',
|
|
26
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function usage() {
|
|
30
|
+
console.log(`entry-scan — static C1-C8 scan of public DSH plugins
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
node 40-checker/scripts/entry-scan.mjs [--limit 10] [--out report.json] [--repos owner/repo,...]
|
|
34
|
+
|
|
35
|
+
Without --repos, searches GitHub topic:dsh-plugin sorted by stars.
|
|
36
|
+
It writes only package.json and placeholder files in a temp directory; no plugin code is executed.
|
|
37
|
+
C3 is not-run because no real install line is tested.
|
|
38
|
+
Exit: 0 scan completed, 3 input/API error.`)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseArgs(argv) {
|
|
42
|
+
const options = { limit: 10, out: null, repos: null }
|
|
43
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
44
|
+
const arg = argv[i]
|
|
45
|
+
const next = () => {
|
|
46
|
+
const value = argv[i + 1]
|
|
47
|
+
if (value === undefined) throw new Error(`${arg} needs a value`)
|
|
48
|
+
i += 1
|
|
49
|
+
return value
|
|
50
|
+
}
|
|
51
|
+
if (arg === '--limit') options.limit = Number(next())
|
|
52
|
+
else if (arg === '--out') options.out = path.resolve(next())
|
|
53
|
+
else if (arg === '--repos') options.repos = next().split(',').map(s => s.trim()).filter(Boolean)
|
|
54
|
+
else if (arg === '--help' || arg === '-h') { usage(); process.exit(0) }
|
|
55
|
+
else throw new Error(`unknown argument: ${arg}`)
|
|
56
|
+
}
|
|
57
|
+
if (!Number.isInteger(options.limit) || options.limit < 1 || options.limit > 30) throw new Error('--limit must be 1..30')
|
|
58
|
+
return options
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function api(pathname) {
|
|
62
|
+
const r = await fetch(`https://api.github.com${pathname}`, { headers })
|
|
63
|
+
if (r.status === 404) return { ok: false, status: 404, json: null }
|
|
64
|
+
const text = await r.text()
|
|
65
|
+
let json = null
|
|
66
|
+
try { json = JSON.parse(text) } catch { json = { raw: text } }
|
|
67
|
+
if (!r.ok) throw new Error(`${pathname} -> ${r.status}: ${text.slice(0, 300)}`)
|
|
68
|
+
return { ok: true, status: r.status, json }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function fileExists(repo, ref, file) {
|
|
72
|
+
if (typeof file !== 'string' || file.length === 0) return false
|
|
73
|
+
const clean = file.replace(/^\.\//, '')
|
|
74
|
+
const encoded = clean.split('/').map(encodeURIComponent).join('/')
|
|
75
|
+
const r = await api(`/repos/${repo}/contents/${encoded}?ref=${encodeURIComponent(ref)}`)
|
|
76
|
+
return r.ok
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function writePlaceholder(dir, file) {
|
|
80
|
+
const clean = file.replace(/^\.\//, '')
|
|
81
|
+
const full = path.resolve(dir, clean)
|
|
82
|
+
if (!full.startsWith(`${dir}${path.sep}`)) return
|
|
83
|
+
fs.mkdirSync(path.dirname(full), { recursive: true })
|
|
84
|
+
fs.writeFileSync(full, '// existence verified via GitHub contents API; content not stored\n')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function scanRepo(repo, meta) {
|
|
88
|
+
const pkgRes = await api(`/repos/${repo}/contents/package.json?ref=${encodeURIComponent(meta.default_branch)}`)
|
|
89
|
+
if (!pkgRes.ok) return null
|
|
90
|
+
const pkg = JSON.parse(Buffer.from(pkgRes.json.content, 'base64').toString('utf8'))
|
|
91
|
+
if (!pkg.dsh || typeof pkg.dsh !== 'object') return null
|
|
92
|
+
if (!pkg.dsh.bundle && !pkg.dsh.client) return null
|
|
93
|
+
const commitRes = await api(`/repos/${repo}/commits?per_page=1`)
|
|
94
|
+
const commit = commitRes.json?.[0]?.sha ?? null
|
|
95
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-entry-scan-'))
|
|
96
|
+
try {
|
|
97
|
+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2))
|
|
98
|
+
const files = []
|
|
99
|
+
if (typeof pkg.dsh.bundle?.patch === 'string') files.push(pkg.dsh.bundle.patch)
|
|
100
|
+
else if (Array.isArray(pkg.dsh.bundle?.patch)) files.push(...pkg.dsh.bundle.patch.filter(item => typeof item === 'string'))
|
|
101
|
+
if (pkg.dsh.client) {
|
|
102
|
+
const client = typeof pkg.exports?.['./client'] === 'string'
|
|
103
|
+
? pkg.exports['./client']
|
|
104
|
+
: pkg.exports?.['./client']?.import ?? pkg.exports?.['./client']?.default
|
|
105
|
+
if (typeof client === 'string') files.push(client)
|
|
106
|
+
}
|
|
107
|
+
const main = typeof pkg.main === 'string' ? pkg.main : typeof pkg.exports?.['.'] === 'string' ? pkg.exports['.'] : pkg.exports?.['.']?.import
|
|
108
|
+
if (typeof main === 'string') files.push(main)
|
|
109
|
+
const paths = new Set()
|
|
110
|
+
const treeRes = await api(`/repos/${repo}/git/trees/${encodeURIComponent(meta.default_branch)}?recursive=1`)
|
|
111
|
+
if (treeRes.ok && Array.isArray(treeRes.json.tree)) {
|
|
112
|
+
for (const item of treeRes.json.tree) paths.add(item.path)
|
|
113
|
+
}
|
|
114
|
+
for (const file of [...new Set(files)]) {
|
|
115
|
+
const clean = file.replace(/^\.\//, '')
|
|
116
|
+
if (paths.has(clean) || (!treeRes.ok && await fileExists(repo, meta.default_branch, file))) {
|
|
117
|
+
writePlaceholder(dir, file)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const metaFile = path.join(dir, 'meta.json')
|
|
121
|
+
fs.writeFileSync(metaFile, JSON.stringify({
|
|
122
|
+
repo,
|
|
123
|
+
topics: meta.topics ?? [],
|
|
124
|
+
archived: meta.archived === true,
|
|
125
|
+
commit,
|
|
126
|
+
pushedAt: String(meta.pushed_at ?? '').slice(0, 10),
|
|
127
|
+
claimLevel: 'reference',
|
|
128
|
+
}, null, 2))
|
|
129
|
+
const run = spawnSync(process.execPath, [VALIDATOR, dir, '--meta', metaFile, '--json'], { encoding: 'utf8', timeout: 30_000 })
|
|
130
|
+
const result = JSON.parse(run.stdout)
|
|
131
|
+
return {
|
|
132
|
+
repo,
|
|
133
|
+
stars: meta.stargazers_count,
|
|
134
|
+
package: pkg.name ?? null,
|
|
135
|
+
version: pkg.version ?? null,
|
|
136
|
+
filesChecked: [...new Set(files)],
|
|
137
|
+
conclusion: result.conclusion,
|
|
138
|
+
checks: result.checks,
|
|
139
|
+
exit: run.status,
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function main() {
|
|
147
|
+
const options = parseArgs(process.argv.slice(2))
|
|
148
|
+
let repos = options.repos
|
|
149
|
+
if (!repos) {
|
|
150
|
+
const search = await api(`/search/repositories?q=${encodeURIComponent('topic:dsh-plugin')}&sort=stars&order=desc&per_page=30`)
|
|
151
|
+
repos = (search.json.items ?? []).map(item => item.full_name)
|
|
152
|
+
}
|
|
153
|
+
const scanned = []
|
|
154
|
+
for (const repo of repos) {
|
|
155
|
+
if (scanned.length >= options.limit) break
|
|
156
|
+
const r = await api(`/repos/${repo}`)
|
|
157
|
+
if (!r.ok) continue
|
|
158
|
+
const row = await scanRepo(repo, r.json)
|
|
159
|
+
if (row) {
|
|
160
|
+
scanned.push(row)
|
|
161
|
+
console.log(`${repo}: ${row.conclusion.status}; ${row.conclusion.failed.join(',') || 'no fail'}${row.conclusion.notRun.length ? `; not-run ${row.conclusion.notRun.join(',')}` : ''}`)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const report = {
|
|
165
|
+
schema: 'dsh-entry-scan/1',
|
|
166
|
+
generatedAt: new Date().toISOString(),
|
|
167
|
+
method: 'static GitHub contents API; C3 not-run; no plugin code executed; file existence only for patch/client/main',
|
|
168
|
+
disclaimer: '非官方 · 非认证 · 非安全审计 · 非背书 · 无担保',
|
|
169
|
+
notClaimed: ['official standard', 'certification', 'security audit', 'ranking', 'market acceptance', 'external adoption'],
|
|
170
|
+
total: scanned.length,
|
|
171
|
+
summary: {
|
|
172
|
+
pass: scanned.filter(r => r.conclusion.status === 'pass').length,
|
|
173
|
+
passWithWarnings: scanned.filter(r => r.conclusion.status === 'pass-with-warnings').length,
|
|
174
|
+
incomplete: scanned.filter(r => r.conclusion.status === 'incomplete').length,
|
|
175
|
+
fail: scanned.filter(r => r.conclusion.status === 'fail').length,
|
|
176
|
+
},
|
|
177
|
+
repos: scanned,
|
|
178
|
+
}
|
|
179
|
+
const output = `${JSON.stringify(report, null, 2)}\n`
|
|
180
|
+
if (options.out) {
|
|
181
|
+
fs.mkdirSync(path.dirname(options.out), { recursive: true })
|
|
182
|
+
fs.writeFileSync(options.out, output)
|
|
183
|
+
console.log(`wrote ${options.out}`)
|
|
184
|
+
} else process.stdout.write(output)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
main().catch(error => {
|
|
188
|
+
console.error(`entry-scan: ${error.message}`)
|
|
189
|
+
process.exit(3)
|
|
190
|
+
})
|