Skip to content

Commit a913b2f

Browse files
Add TypeScript schema library to npm wrapper (#35)
* feat: add TypeScript schema library to npm wrapper package Add pared-down TypeScript library to npm/socket-patch/ for use by depscan. Includes schema validation (zod), git-compatible hashing, manifest operations, recovery, and package-json postinstall helpers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: normalize path separators in Go crawler PURLs on Windows On Windows, `Path::strip_prefix` + `to_string_lossy()` produces backslashes in the relative path, which get embedded in the PURL (e.g., `pkg:golang/github.com\\gin-gonic\\gin@v1.9.1`). Replace backslashes with forward slashes to produce correct PURLs on all platforms. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: remove depscan-specific TS utilities, keep only schema Move operational code (constants, hashing, manifest operations, recovery, postinstall detection) back to depscan. Only the Zod schema and its tests remain in the npm wrapper package. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6e18576 commit a913b2f

File tree

5 files changed

+206
-3
lines changed

5 files changed

+206
-3
lines changed

crates/socket-patch-core/src/crawlers/go_crawler.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,9 +276,10 @@ impl GoCrawler {
276276
_dir_name: &str,
277277
seen: &mut HashSet<String>,
278278
) -> Option<CrawledPackage> {
279-
// Get the relative path from the cache root
279+
// Get the relative path from the cache root.
280+
// Normalize to forward slashes so PURLs are correct on Windows.
280281
let rel_path = dir_path.strip_prefix(base_path).ok()?;
281-
let rel_str = rel_path.to_string_lossy();
282+
let rel_str = rel_path.to_string_lossy().replace('\\', "/");
282283

283284
// Find the last `@` to split module path and version
284285
let at_idx = rel_str.rfind('@')?;

npm/socket-patch/package.json

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
{
22
"name": "@socketsecurity/socket-patch",
33
"version": "1.6.3",
4-
"description": "CLI tool for applying security patches to dependencies",
4+
"description": "CLI tool and schema library for applying security patches to dependencies",
55
"bin": {
66
"socket-patch": "bin/socket-patch"
77
},
8+
"exports": {
9+
"./schema": {
10+
"types": "./dist/schema/manifest-schema.d.ts",
11+
"import": "./dist/schema/manifest-schema.js",
12+
"require": "./dist/schema/manifest-schema.js"
13+
}
14+
},
815
"publishConfig": {
916
"access": "public"
1017
},
18+
"scripts": {
19+
"build": "tsc",
20+
"test": "pnpm run build && node --test dist/**/*.test.js"
21+
},
1122
"keywords": [
1223
"security",
1324
"patch",
@@ -23,6 +34,13 @@
2334
"engines": {
2435
"node": ">=18.0.0"
2536
},
37+
"dependencies": {
38+
"zod": "^3.24.4"
39+
},
40+
"devDependencies": {
41+
"typescript": "^5.3.0",
42+
"@types/node": "^20.0.0"
43+
},
2644
"optionalDependencies": {
2745
"@socketsecurity/socket-patch-android-arm64": "1.6.3",
2846
"@socketsecurity/socket-patch-darwin-arm64": "1.6.3",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { describe, it } from 'node:test'
2+
import * as assert from 'node:assert/strict'
3+
import { PatchManifestSchema, PatchRecordSchema } from './manifest-schema.js'
4+
5+
describe('PatchManifestSchema', () => {
6+
it('should validate a well-formed manifest', () => {
7+
const manifest = {
8+
patches: {
9+
'npm:simplehttpserver@0.0.6': {
10+
uuid: '550e8400-e29b-41d4-a716-446655440000',
11+
exportedAt: '2024-01-01T00:00:00Z',
12+
files: {
13+
'node_modules/simplehttpserver/index.js': {
14+
beforeHash: 'abc123',
15+
afterHash: 'def456',
16+
},
17+
},
18+
vulnerabilities: {
19+
'GHSA-jrhj-2j3q-xf3v': {
20+
cves: ['CVE-2024-0001'],
21+
summary: 'Path traversal vulnerability',
22+
severity: 'high',
23+
description: 'Allows reading arbitrary files',
24+
},
25+
},
26+
description: 'Fix path traversal',
27+
license: 'MIT',
28+
tier: 'free',
29+
},
30+
},
31+
}
32+
33+
const result = PatchManifestSchema.safeParse(manifest)
34+
assert.ok(result.success, 'Valid manifest should parse successfully')
35+
assert.equal(
36+
Object.keys(result.data.patches).length,
37+
1,
38+
'Should have one patch entry',
39+
)
40+
})
41+
42+
it('should validate a manifest with multiple patches', () => {
43+
const manifest = {
44+
patches: {
45+
'npm:pkg-a@1.0.0': {
46+
uuid: '550e8400-e29b-41d4-a716-446655440001',
47+
exportedAt: '2024-01-01T00:00:00Z',
48+
files: {
49+
'node_modules/pkg-a/lib/index.js': {
50+
beforeHash: 'aaa',
51+
afterHash: 'bbb',
52+
},
53+
},
54+
vulnerabilities: {},
55+
description: 'Patch A',
56+
license: 'MIT',
57+
tier: 'free',
58+
},
59+
'npm:pkg-b@2.0.0': {
60+
uuid: '550e8400-e29b-41d4-a716-446655440002',
61+
exportedAt: '2024-02-01T00:00:00Z',
62+
files: {
63+
'node_modules/pkg-b/src/main.js': {
64+
beforeHash: 'ccc',
65+
afterHash: 'ddd',
66+
},
67+
},
68+
vulnerabilities: {
69+
'GHSA-xxxx-yyyy-zzzz': {
70+
cves: [],
71+
summary: 'Some vuln',
72+
severity: 'medium',
73+
description: 'A medium severity vulnerability',
74+
},
75+
},
76+
description: 'Patch B',
77+
license: 'Apache-2.0',
78+
tier: 'paid',
79+
},
80+
},
81+
}
82+
83+
const result = PatchManifestSchema.safeParse(manifest)
84+
assert.ok(result.success, 'Multi-patch manifest should parse successfully')
85+
assert.equal(Object.keys(result.data.patches).length, 2)
86+
})
87+
88+
it('should validate an empty manifest', () => {
89+
const manifest = { patches: {} }
90+
const result = PatchManifestSchema.safeParse(manifest)
91+
assert.ok(result.success, 'Empty patches should be valid')
92+
})
93+
94+
it('should reject a manifest missing the patches field', () => {
95+
const result = PatchManifestSchema.safeParse({})
96+
assert.ok(!result.success, 'Missing patches should fail')
97+
})
98+
99+
it('should reject a manifest with invalid patch record', () => {
100+
const manifest = {
101+
patches: {
102+
'npm:bad@1.0.0': {
103+
// missing uuid, exportedAt, files, vulnerabilities, description, license, tier
104+
},
105+
},
106+
}
107+
const result = PatchManifestSchema.safeParse(manifest)
108+
assert.ok(!result.success, 'Invalid patch record should fail')
109+
})
110+
111+
it('should reject a patch with invalid uuid', () => {
112+
const record = {
113+
uuid: 'not-a-valid-uuid',
114+
exportedAt: '2024-01-01T00:00:00Z',
115+
files: {},
116+
vulnerabilities: {},
117+
description: 'Test',
118+
license: 'MIT',
119+
tier: 'free',
120+
}
121+
const result = PatchRecordSchema.safeParse(record)
122+
assert.ok(!result.success, 'Invalid UUID should fail')
123+
})
124+
125+
it('should reject non-object input', () => {
126+
assert.ok(!PatchManifestSchema.safeParse(null).success)
127+
assert.ok(!PatchManifestSchema.safeParse('string').success)
128+
assert.ok(!PatchManifestSchema.safeParse(42).success)
129+
assert.ok(!PatchManifestSchema.safeParse([]).success)
130+
})
131+
})
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { z } from 'zod'
2+
3+
export const DEFAULT_PATCH_MANIFEST_PATH = '.socket/manifest.json'
4+
5+
export const PatchRecordSchema = z.object({
6+
uuid: z.string().uuid(),
7+
exportedAt: z.string(),
8+
files: z.record(
9+
z.string(), // File path
10+
z.object({
11+
beforeHash: z.string(),
12+
afterHash: z.string(),
13+
}),
14+
),
15+
vulnerabilities: z.record(
16+
z.string(), // Vulnerability ID like "GHSA-jrhj-2j3q-xf3v"
17+
z.object({
18+
cves: z.array(z.string()),
19+
summary: z.string(),
20+
severity: z.string(),
21+
description: z.string(),
22+
}),
23+
),
24+
description: z.string(),
25+
license: z.string(),
26+
tier: z.string(),
27+
})
28+
29+
export type PatchRecord = z.infer<typeof PatchRecordSchema>
30+
31+
export const PatchManifestSchema = z.object({
32+
patches: z.record(
33+
z.string(), // Package identifier like "npm:simplehttpserver@0.0.6"
34+
PatchRecordSchema,
35+
),
36+
})
37+
38+
export type PatchManifest = z.infer<typeof PatchManifestSchema>

npm/socket-patch/tsconfig.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "Node16",
5+
"moduleResolution": "Node16",
6+
"declaration": true,
7+
"composite": true,
8+
"outDir": "dist",
9+
"rootDir": "src",
10+
"strict": true,
11+
"skipLibCheck": true,
12+
"esModuleInterop": true
13+
},
14+
"include": ["src"]
15+
}

0 commit comments

Comments
 (0)