trustdex 0.4.4 → 0.5.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.
@@ -0,0 +1,93 @@
1
+ # 신뢰 저장소와 출처
2
+
3
+ <p align="right">
4
+ <a href="./TRUST_STORE.md">English</a> | <strong>한국어</strong>
5
+ </p>
6
+
7
+ TrustDex는 패키지 이름, GitHub 스타 수 또는 브랜딩으로 게시자가 "공식"인지 추측하지 않습니다.
8
+
9
+ 사용자 또는 조직이 로컬 신뢰 저장소에 명시적인 주장을 배치한 경우에만 소스에 `verified` 출처 주장을 부여할 수 있습니다.
10
+
11
+ ## 형식
12
+
13
+ ```json
14
+ {
15
+ "version": 1,
16
+ "claims": [
17
+ {
18
+ "type": "package",
19
+ "subject": "example-mcp-server",
20
+ "publisher": "Example Publisher",
21
+ "status": "verified",
22
+ "evidence": {
23
+ "kind": "manual-review",
24
+ "reference": "https://example.invalid/security-review",
25
+ "checkedAt": "2026-09-19T00:00:00Z"
26
+ }
27
+ }
28
+ ]
29
+ }
30
+ ```
31
+
32
+ 지원되는 주장 유형:
33
+
34
+ - `package` - npm/Python 형식의 패키지 주체
35
+ - `remote` - 원격 MCP 호스트 이름
36
+ - `repository` - 향후 저장소 어댑터용 저장소 신원
37
+
38
+ `verified`라는 단어는 **신뢰 저장소 유지관리자가 기록한 증거에 따라 검증됨**을 의미합니다. OpenAI, TrustDex, GitHub, npm 또는 다른 플랫폼이 확장을 인증했다는 의미가 아닙니다.
39
+
40
+ ## 사용하기
41
+
42
+ ```bash
43
+ node ./bin/trustdex.mjs inspect ./examples/mcp.json \
44
+ --pack official-first \
45
+ --trust-store ./examples/trust-store.example.json
46
+ ```
47
+
48
+ 알 수 없는 서드파티 소스는 `official-first`에서 계속 차단됩니다. 검증된 출처 주장은 초기 신뢰를 설정할 수 있지만 셸 실행과 같은 차단 기능 신호는 여전히 해당 신뢰보다 우선할 수 있습니다.
49
+
50
+ ## 권장 사례
51
+
52
+ - 팀 정책인 신뢰 저장소는 소스 관리에 보관
53
+ - 각 주장을 지속 가능한 증거에 연결
54
+ - 가능한 경우 패키지 버전 또는 소스 커밋 고정
55
+ - 소유권이나 기능이 변경되면 주장을 재검토
56
+ - 증거 필드에 자격 증명, 토큰, 비공개 URL 또는 비밀을 기록하지 않음
57
+
58
+ ## 무결성 기반 패키지 증거
59
+
60
+ `trust-source npm` 및 `trust-source mcp`의 선택적 `--artifact` 플래그는 아티팩트 증거를 기록하기 전에 사용자가 제공한 파일을 검증합니다.
61
+
62
+ - npm 관찰에는 SHA-256, SHA-384 또는 SHA-512를 사용하는 SRI 값이 필요합니다.
63
+ - MCP Registry 관찰에는 SHA-256 다이제스트가 있는 단일 패키지가 필요합니다.
64
+ - 파일은 로컬에서 읽으며 실행하거나 업로드하지 않습니다.
65
+ - 신뢰 저장소 주장은 레지스트리 식별자, 버전, 예상 무결성 메타데이터, 계산된 다이제스트 및 검증 시간을 기록합니다.
66
+ - 설정된 패키지 버전은 검증된 아티팩트 버전과 일치해야 합니다.
67
+
68
+ 증거 형식 예시:
69
+
70
+ ```json
71
+ {
72
+ "kind": "npm-registry",
73
+ "reference": "https://www.npmjs.com/package/example-mcp-server",
74
+ "checkedAt": "2026-09-19T00:00:00Z",
75
+ "artifact": {
76
+ "registryType": "npm",
77
+ "identifier": "example-mcp-server",
78
+ "version": "1.2.3",
79
+ "integrity": "sha512-..."
80
+ },
81
+ "artifactVerification": {
82
+ "verified": true,
83
+ "algorithm": "sha512",
84
+ "digest": "...",
85
+ "encoding": "base64",
86
+ "version": "1.2.3",
87
+ "identifier": "example-mcp-server",
88
+ "verifiedAt": "2026-09-19T00:00:01Z"
89
+ }
90
+ }
91
+ ```
92
+
93
+ 아티팩트 무결성은 제공된 바이트가 해당 시점에 관찰된 레지스트리 메타데이터와 일치한다는 것만 증명합니다. 바이트가 안전하다는 것, 레지스트리 계정이 침해되지 않았다는 것 또는 게시자 이름이 합법적이라는 것은 증명하지 않습니다.
@@ -1,5 +1,9 @@
1
1
  # Trust store and provenance
2
2
 
3
+ <p align="right">
4
+ <strong>English</strong> | <a href="./TRUST_STORE.ko.md">한국어</a>
5
+ </p>
6
+
3
7
  TrustDex does not guess whether a publisher is "official" from a package name, GitHub stars, or branding.
4
8
 
5
9
  A source can receive a `verified` provenance assertion only when the user or an organization places an explicit claim in a local trust store.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trustdex",
3
- "version": "0.4.4",
3
+ "version": "0.5.1",
4
4
  "description": "A local-first trust, provenance, and review gate for AI agent tools, MCP servers, skills, and plugins.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,7 +9,7 @@
9
9
  "scripts": {
10
10
  "test": "node --test",
11
11
  "test:research": "node --test test/research-fixtures.test.mjs",
12
- "check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/codex-config.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
12
+ "check": "node --check bin/trustdex.mjs && node --check src/canonical.mjs && node --check src/claude-config.mjs && node --check src/codex-config.mjs && node --check src/extensions.mjs && node --check src/gate.mjs && node --check src/inspect.mjs && node --check src/policy.mjs && node --check src/policy-packs.mjs && node --check src/provenance.mjs && node --check src/provenance-online.mjs && node --check src/review.mjs && node --check src/snapshot.mjs && node --check src/trust-record.mjs",
13
13
  "prepublishOnly": "npm test && npm run check"
14
14
  },
15
15
  "engines": {
@@ -0,0 +1,106 @@
1
+ const SERVER_KEYS = {
2
+ stdio: new Set(['type', 'command', 'args', 'env']),
3
+ http: new Set(['type', 'url', 'headers', 'headersHelper']),
4
+ sse: new Set(['type', 'url', 'headers', 'headersHelper'])
5
+ };
6
+ const ENV_REFERENCE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-[^}]*)?\}/g;
7
+
8
+ function isRecord(value) {
9
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+
12
+ function requireKeys(value, allowed, context) {
13
+ if (Object.keys(value).some((key) => !allowed.has(key))) {
14
+ throw new Error(`Unsupported ${context} field. Claude MCP inspection fails closed on unknown settings.`);
15
+ }
16
+ }
17
+
18
+ function environmentReferences(value) {
19
+ return [...new Set([...value.matchAll(ENV_REFERENCE)].map((match) => match[1]))].sort();
20
+ }
21
+
22
+ // Return inspection input only. Gating must preserve entries from the original
23
+ // configuration, never these internal adapter annotations.
24
+ export function parseClaudeMcpConfig(config) {
25
+ if (!isRecord(config) || !isRecord(config.mcpServers)) {
26
+ throw new Error('Expected a Claude MCP JSON object containing an mcpServers object.');
27
+ }
28
+ requireKeys(config, new Set(['mcpServers']), 'Claude MCP root');
29
+
30
+ const entries = Object.entries(config.mcpServers).map(([name, server]) => {
31
+ if (!/^[A-Za-z0-9_-]+$/.test(name) || ['__proto__', 'constructor', 'prototype'].includes(name)) {
32
+ throw new Error('Unsupported Claude MCP server name.');
33
+ }
34
+ if (!isRecord(server)) throw new Error('Each Claude MCP server must be an object.');
35
+ const type = server.type === undefined ? 'stdio' : server.type;
36
+ if (typeof type !== 'string' || !Object.hasOwn(SERVER_KEYS, type)) {
37
+ throw new Error('Unsupported Claude MCP transport. Supported: stdio, http, sse.');
38
+ }
39
+ requireKeys(server, SERVER_KEYS[type], 'Claude MCP server');
40
+ const signals = [];
41
+ const adapter = { runtime: 'claude-mcp-v1', transport: type };
42
+
43
+ if (type === 'stdio') {
44
+ if (typeof server.command !== 'string' || !server.command.trim()) {
45
+ throw new Error('Claude stdio MCP servers require a non-empty command.');
46
+ }
47
+ if (server.command.includes('${')) {
48
+ throw new Error('Claude command variable expansion is unsupported; use an explicit executable.');
49
+ }
50
+ if (server.args !== undefined && (!Array.isArray(server.args) || server.args.some((arg) => typeof arg !== 'string' || arg.includes('${')))) {
51
+ throw new Error('Claude MCP args must be strings without unresolved variable expansion.');
52
+ }
53
+ if (server.env !== undefined && (!isRecord(server.env) || Object.values(server.env).some((value) => typeof value !== 'string'))) {
54
+ throw new Error('Claude MCP env must contain string values.');
55
+ }
56
+ // Reference names are safe metadata; do not fingerprint env values or defaults.
57
+ adapter.envReferences = Object.fromEntries(Object.entries(server.env || {})
58
+ .sort(([a], [b]) => a.localeCompare(b))
59
+ .map(([key, value]) => [key, environmentReferences(value)]));
60
+ } else {
61
+ if (typeof server.url !== 'string' || !server.url.trim() || server.url.includes('${')) {
62
+ throw new Error('Claude remote MCP servers require a literal HTTP(S) URL.');
63
+ }
64
+ let endpoint;
65
+ try { endpoint = new URL(server.url); } catch {
66
+ throw new Error('Invalid Claude MCP URL.');
67
+ }
68
+ if (!['https:', 'http:'].includes(endpoint.protocol)) {
69
+ throw new Error('Unsupported Claude MCP URL scheme.');
70
+ }
71
+ // The generic inspector fingerprints its input URL. Reject URL credentials
72
+ // and query/fragment values before they can enter that fingerprint.
73
+ if (endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
74
+ throw new Error('Claude MCP URL credentials, query parameters, and fragments are unsupported; use headers.');
75
+ }
76
+ if (server.headers !== undefined) {
77
+ if (!isRecord(server.headers) || Object.values(server.headers).some((value) => typeof value !== 'string')) {
78
+ throw new Error('Claude MCP headers must contain string values.');
79
+ }
80
+ adapter.headerNames = Object.keys(server.headers).sort();
81
+ adapter.headerReferences = Object.fromEntries(Object.entries(server.headers)
82
+ .sort(([a], [b]) => a.localeCompare(b))
83
+ .map(([key, value]) => [key, environmentReferences(value)]));
84
+ if (adapter.headerNames.length) {
85
+ const references = Object.values(adapter.headerReferences);
86
+ if (references.some((refs) => refs.length)) signals.push('env-http-headers');
87
+ if (references.some((refs) => !refs.length)) signals.push('static-http-headers');
88
+ }
89
+ }
90
+ if (server.headersHelper !== undefined) {
91
+ if (typeof server.headersHelper !== 'string' || !server.headersHelper.trim()) {
92
+ throw new Error('Claude MCP headersHelper must be a non-empty string.');
93
+ }
94
+ signals.push('header-helper-command');
95
+ adapter.hasHeadersHelper = true;
96
+ }
97
+ }
98
+
99
+ return [name, {
100
+ ...server,
101
+ _trustdexSignals: signals,
102
+ _trustdexFingerprintData: adapter
103
+ }];
104
+ });
105
+ return { mcpServers: Object.fromEntries(entries) };
106
+ }
package/src/inspect.mjs CHANGED
@@ -42,6 +42,57 @@ function looksLikePath(value) {
42
42
  value.startsWith('../') || /^[A-Za-z]:\\/.test(value);
43
43
  }
44
44
 
45
+ function fingerprintUrl(value) {
46
+ try {
47
+ const url = new URL(String(value));
48
+ return {
49
+ origin: `${url.protocol.toLowerCase()}//${url.host.toLowerCase()}`,
50
+ pathname: url.pathname,
51
+ queryKeys: [...new Set(url.searchParams.keys())].sort()
52
+ };
53
+ } catch {
54
+ return { invalid: true };
55
+ }
56
+ }
57
+
58
+ function fingerprintArgs(args) {
59
+ const values = Array.isArray(args) ? args.map(String) : [];
60
+ const sanitized = [];
61
+ let redactNext = false;
62
+
63
+ for (const value of values) {
64
+ if (redactNext) {
65
+ sanitized.push('<redacted>');
66
+ redactNext = false;
67
+ continue;
68
+ }
69
+
70
+ const assignment = value.match(/^(-{1,2}[^=]+)=(.*)$/);
71
+ if (assignment && SECRET_KEY_RE.test(assignment[1])) {
72
+ sanitized.push(`${assignment[1]}=<redacted>`);
73
+ continue;
74
+ }
75
+ if (/^-{1,2}/.test(value) && SECRET_KEY_RE.test(value)) {
76
+ sanitized.push(value);
77
+ redactNext = true;
78
+ continue;
79
+ }
80
+ if (value === '-H' || value === '--header') {
81
+ sanitized.push(value);
82
+ redactNext = true;
83
+ continue;
84
+ }
85
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
86
+ sanitized.push(fingerprintUrl(value));
87
+ continue;
88
+ }
89
+
90
+ sanitized.push(value);
91
+ }
92
+
93
+ return sanitized;
94
+ }
95
+
45
96
  function extractRemoteEndpoint(server) {
46
97
  const candidate = server?.url || server?.endpoint;
47
98
  if (!candidate) return null;
@@ -53,6 +104,8 @@ function extractRemoteEndpoint(server) {
53
104
  host: url.hostname.toLowerCase(),
54
105
  origin: `${protocol}//${url.host.toLowerCase()}`,
55
106
  protocol,
107
+ pathname: url.pathname,
108
+ queryKeys: [...new Set(url.searchParams.keys())].sort(),
56
109
  hasEmbeddedCredentials: Boolean(url.username || url.password),
57
110
  hasQuery: Boolean(url.search)
58
111
  };
@@ -95,9 +148,13 @@ function normalizeServer(name, server = {}) {
95
148
 
96
149
  const fingerprint = sha256Json({
97
150
  command,
98
- args,
99
- url: server.url || null,
100
- endpoint: server.endpoint || null,
151
+ args: fingerprintArgs(args),
152
+ remoteEndpoint: remote ? {
153
+ origin: remote.origin,
154
+ pathname: remote.pathname,
155
+ queryKeys: remote.queryKeys
156
+ } : null,
157
+ invalidEndpoint: (server.url || server.endpoint) && !remote ? true : null,
101
158
  envKeys,
102
159
  adapter: server._trustdexFingerprintData || null
103
160
  });