Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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>
  • Loading branch information
mikolalysenko and claude committed Mar 5, 2026
commit f46314c934592c40cc0f4995bb318a978eda9f1f
47 changes: 46 additions & 1 deletion npm/socket-patch/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,51 @@
{
"name": "@socketsecurity/socket-patch",
"version": "1.6.3",
"description": "CLI tool for applying security patches to dependencies",
"description": "CLI tool and schema library for applying security patches to dependencies",
"main": "dist/schema/manifest-schema.js",
"types": "dist/schema/manifest-schema.d.ts",
"bin": {
"socket-patch": "bin/socket-patch"
},
"exports": {
"./schema": {
"types": "./dist/schema/manifest-schema.d.ts",
"import": "./dist/schema/manifest-schema.js",
"require": "./dist/schema/manifest-schema.js"
},
"./hash": {
"types": "./dist/hash/git-sha256.d.ts",
"import": "./dist/hash/git-sha256.js",
"require": "./dist/hash/git-sha256.js"
},
"./constants": {
"types": "./dist/constants.d.ts",
"import": "./dist/constants.js",
"require": "./dist/constants.js"
},
"./manifest/operations": {
"types": "./dist/manifest/operations.d.ts",
"import": "./dist/manifest/operations.js",
"require": "./dist/manifest/operations.js"
},
"./manifest/recovery": {
"types": "./dist/manifest/recovery.d.ts",
"import": "./dist/manifest/recovery.js",
"require": "./dist/manifest/recovery.js"
},
"./package-json": {
"types": "./dist/package-json/index.d.ts",
"import": "./dist/package-json/index.js",
"require": "./dist/package-json/index.js"
}
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build": "tsc",
"test": "pnpm run build && node --test dist/**/*.test.js"
},
"keywords": [
"security",
"patch",
Expand All @@ -23,6 +61,13 @@
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"zod": "^3.24.4"
},
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.0.0"
},
"optionalDependencies": {
"@socketsecurity/socket-patch-android-arm64": "1.6.3",
"@socketsecurity/socket-patch-darwin-arm64": "1.6.3",
Expand Down
18 changes: 18 additions & 0 deletions npm/socket-patch/src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Standard paths and constants used throughout the socket-patch system
*/

/**
* Default path to the patch manifest file
*/
export const DEFAULT_PATCH_MANIFEST_PATH = '.socket/manifest.json'

/**
* Default folder for storing patched file blobs
*/
export const DEFAULT_BLOB_FOLDER = '.socket/blob'

/**
* Default Socket directory
*/
export const DEFAULT_SOCKET_DIR = '.socket'
37 changes: 37 additions & 0 deletions npm/socket-patch/src/hash/git-sha256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as crypto from 'crypto'

/**
* Compute Git-compatible SHA256 hash for a buffer
* @param buffer - Buffer or Uint8Array to hash
* @returns Git-compatible SHA256 hash (hex string)
*/
export function computeGitSHA256FromBuffer(
buffer: Buffer | Uint8Array,
): string {
const gitHash = crypto.createHash('sha256')
const header = `blob ${buffer.length}\0`
gitHash.update(header)
gitHash.update(buffer)
return gitHash.digest('hex')
}

/**
* Compute Git-compatible SHA256 hash from an async iterable of chunks
* @param size - Total size of the file in bytes
* @param chunks - Async iterable of Buffer or Uint8Array chunks
* @returns Git-compatible SHA256 hash (hex string)
*/
export async function computeGitSHA256FromChunks(
size: number,
chunks: AsyncIterable<Buffer | Uint8Array>,
): Promise<string> {
const gitHash = crypto.createHash('sha256')
const header = `blob ${size}\0`
gitHash.update(header)

for await (const chunk of chunks) {
gitHash.update(chunk)
}

return gitHash.digest('hex')
}
142 changes: 142 additions & 0 deletions npm/socket-patch/src/manifest/operations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import * as fs from 'fs/promises'
import type { PatchManifest, PatchRecord } from '../schema/manifest-schema.js'
import { PatchManifestSchema } from '../schema/manifest-schema.js'

/**
* Get all blob hashes referenced by a manifest (both beforeHash and afterHash)
* Used for garbage collection and validation
*/
export function getReferencedBlobs(manifest: PatchManifest): Set<string> {
const blobs = new Set<string>()

for (const patchRecord of Object.values(manifest.patches)) {
const record = patchRecord as PatchRecord
for (const fileInfo of Object.values(record.files)) {
blobs.add(fileInfo.beforeHash)
blobs.add(fileInfo.afterHash)
}
}

return blobs
}

/**
* Get only afterHash blobs referenced by a manifest
* Used for apply operations - we only need the patched file content, not the original
* This saves disk space since beforeHash blobs are not needed for applying patches
*/
export function getAfterHashBlobs(manifest: PatchManifest): Set<string> {
const blobs = new Set<string>()

for (const patchRecord of Object.values(manifest.patches)) {
const record = patchRecord as PatchRecord
for (const fileInfo of Object.values(record.files)) {
blobs.add(fileInfo.afterHash)
}
}

return blobs
}

/**
* Get only beforeHash blobs referenced by a manifest
* Used for rollback operations - we need the original file content to restore
*/
export function getBeforeHashBlobs(manifest: PatchManifest): Set<string> {
const blobs = new Set<string>()

for (const patchRecord of Object.values(manifest.patches)) {
const record = patchRecord as PatchRecord
for (const fileInfo of Object.values(record.files)) {
blobs.add(fileInfo.beforeHash)
}
}

return blobs
}

/**
* Calculate differences between two manifests
*/
export interface ManifestDiff {
added: Set<string> // PURLs
removed: Set<string>
modified: Set<string>
}

export function diffManifests(
oldManifest: PatchManifest,
newManifest: PatchManifest,
): ManifestDiff {
const oldPurls = new Set(Object.keys(oldManifest.patches))
const newPurls = new Set(Object.keys(newManifest.patches))

const added = new Set<string>()
const removed = new Set<string>()
const modified = new Set<string>()

// Find added and modified
for (const purl of newPurls) {
if (!oldPurls.has(purl)) {
added.add(purl)
} else {
const oldPatch = oldManifest.patches[purl] as PatchRecord
const newPatch = newManifest.patches[purl] as PatchRecord
if (oldPatch.uuid !== newPatch.uuid) {
modified.add(purl)
}
}
}

// Find removed
for (const purl of oldPurls) {
if (!newPurls.has(purl)) {
removed.add(purl)
}
}

return { added, removed, modified }
}

/**
* Validate a parsed manifest object
*/
export function validateManifest(parsed: unknown): {
success: boolean
manifest?: PatchManifest
error?: string
} {
const result = PatchManifestSchema.safeParse(parsed)
if (result.success) {
return { success: true, manifest: result.data }
}
return {
success: false,
error: result.error.message,
}
}

/**
* Read and parse a manifest from the filesystem
*/
export async function readManifest(path: string): Promise<PatchManifest | null> {
try {
const content = await fs.readFile(path, 'utf-8')
const parsed = JSON.parse(content)
const result = validateManifest(parsed)
return result.success ? result.manifest! : null
} catch {
return null
}
}

/**
* Write a manifest to the filesystem
*/
export async function writeManifest(
path: string,
manifest: PatchManifest,
): Promise<void> {
const content = JSON.stringify(manifest, null, 2)
await fs.writeFile(path, content, 'utf-8')
}
Loading
Loading