@enconvo/dxt 0.2.6

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,7 @@
1
+ interface PackOptions {
2
+ extensionPath: string;
3
+ outputPath?: string;
4
+ silent?: boolean;
5
+ }
6
+ export declare function packExtension({ extensionPath, outputPath, silent, }: PackOptions): Promise<boolean>;
7
+ export {};
@@ -0,0 +1,194 @@
1
+ import { confirm } from "@inquirer/prompts";
2
+ import { createHash } from "crypto";
3
+ import { zipSync } from "fflate";
4
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "fs";
5
+ import { basename, join, relative, resolve, sep } from "path";
6
+ import { getAllFilesWithCount, readDxtIgnorePatterns } from "../node/files.js";
7
+ import { validateManifest } from "../node/validate.js";
8
+ import { DxtManifestSchema } from "../schemas.js";
9
+ import { getLogger } from "../shared/log.js";
10
+ import { initExtension } from "./init.js";
11
+ function formatFileSize(bytes) {
12
+ if (bytes < 1024) {
13
+ return `${bytes}B`;
14
+ }
15
+ else if (bytes < 1024 * 1024) {
16
+ return `${(bytes / 1024).toFixed(1)}kB`;
17
+ }
18
+ else {
19
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
20
+ }
21
+ }
22
+ function sanitizeNameForFilename(name) {
23
+ // Replace spaces with hyphens
24
+ // Remove or replace characters that are problematic in filenames
25
+ return name
26
+ .toLowerCase()
27
+ .replace(/\s+/g, "-") // Replace spaces with hyphens
28
+ .replace(/[^a-z0-9-_.]/g, "") // Keep only alphanumeric, hyphens, underscores, and dots
29
+ .replace(/-+/g, "-") // Replace multiple hyphens with single hyphen
30
+ .replace(/^-+|-+$/g, "") // Remove leading/trailing hyphens
31
+ .substring(0, 100); // Limit length to 100 characters
32
+ }
33
+ export async function packExtension({ extensionPath, outputPath, silent, }) {
34
+ const resolvedPath = resolve(extensionPath);
35
+ const logger = getLogger({ silent });
36
+ // Check if directory exists
37
+ if (!existsSync(resolvedPath) || !statSync(resolvedPath).isDirectory()) {
38
+ logger.error(`ERROR: Directory not found: ${extensionPath}`);
39
+ return false;
40
+ }
41
+ // Check if manifest exists
42
+ const manifestPath = join(resolvedPath, "manifest.json");
43
+ if (!existsSync(manifestPath)) {
44
+ logger.log(`No manifest.json found in ${extensionPath}`);
45
+ const shouldInit = await confirm({
46
+ message: "Would you like to create a manifest.json file?",
47
+ default: true,
48
+ });
49
+ if (shouldInit) {
50
+ const success = await initExtension(extensionPath);
51
+ if (!success) {
52
+ logger.error("ERROR: Failed to create manifest");
53
+ return false;
54
+ }
55
+ }
56
+ else {
57
+ logger.error("ERROR: Cannot pack extension without manifest.json");
58
+ return false;
59
+ }
60
+ }
61
+ // Validate manifest first
62
+ logger.log("Validating manifest...");
63
+ if (!validateManifest(manifestPath)) {
64
+ logger.error("ERROR: Cannot pack extension with invalid manifest");
65
+ return false;
66
+ }
67
+ // Read and parse manifest
68
+ let manifest;
69
+ try {
70
+ const manifestContent = readFileSync(manifestPath, "utf-8");
71
+ const manifestData = JSON.parse(manifestContent);
72
+ manifest = DxtManifestSchema.parse(manifestData);
73
+ }
74
+ catch (error) {
75
+ logger.error("ERROR: Failed to parse manifest.json");
76
+ if (error instanceof Error) {
77
+ logger.error(` ${error.message}`);
78
+ }
79
+ return false;
80
+ }
81
+ // Determine output path
82
+ const extensionName = basename(resolvedPath);
83
+ const finalOutputPath = outputPath
84
+ ? resolve(outputPath)
85
+ : resolve(`${extensionName}.dxt`);
86
+ // Ensure output directory exists
87
+ const outputDir = join(finalOutputPath, "..");
88
+ mkdirSync(outputDir, { recursive: true });
89
+ try {
90
+ // Read .dxtignore patterns if present
91
+ const dxtIgnorePatterns = readDxtIgnorePatterns(resolvedPath);
92
+ // Get all files in the extension directory
93
+ const { files, ignoredCount } = getAllFilesWithCount(resolvedPath, resolvedPath, {}, dxtIgnorePatterns);
94
+ // Print package header
95
+ logger.log(`\n📦 ${manifest.name}@${manifest.version}`);
96
+ // Print file list
97
+ logger.log("Archive Contents");
98
+ const fileEntries = Object.entries(files);
99
+ let totalUnpackedSize = 0;
100
+ // Sort files for consistent output
101
+ fileEntries.sort(([a], [b]) => a.localeCompare(b));
102
+ // Group files by directory for deep nesting
103
+ const directoryGroups = new Map();
104
+ const shallowFiles = [];
105
+ for (const [filePath, fileData] of fileEntries) {
106
+ const relPath = relative(resolvedPath, filePath);
107
+ const content = fileData.data;
108
+ const size = typeof content === "string"
109
+ ? Buffer.byteLength(content, "utf8")
110
+ : content.length;
111
+ totalUnpackedSize += size;
112
+ // Check if file is deeply nested (3+ levels)
113
+ const parts = relPath.split(sep);
114
+ if (parts.length > 3) {
115
+ // Group by the first 3 directory levels
116
+ const groupKey = parts.slice(0, 3).join("/");
117
+ if (!directoryGroups.has(groupKey)) {
118
+ directoryGroups.set(groupKey, { files: [], totalSize: 0 });
119
+ }
120
+ const group = directoryGroups.get(groupKey);
121
+ group.files.push(relPath);
122
+ group.totalSize += size;
123
+ }
124
+ else {
125
+ shallowFiles.push({ path: relPath, size });
126
+ }
127
+ }
128
+ // Print shallow files first
129
+ for (const { path, size } of shallowFiles) {
130
+ logger.log(`${formatFileSize(size).padStart(8)} ${path}`);
131
+ }
132
+ // Print grouped directories
133
+ for (const [dir, { files, totalSize }] of directoryGroups) {
134
+ if (files.length === 1) {
135
+ // If only one file in the group, print it normally
136
+ const filePath = files[0];
137
+ const fileSize = totalSize;
138
+ logger.log(`${formatFileSize(fileSize).padStart(8)} ${filePath}`);
139
+ }
140
+ else {
141
+ // Print directory summary
142
+ logger.log(`${formatFileSize(totalSize).padStart(8)} ${dir}/ [and ${files.length} more files]`);
143
+ }
144
+ }
145
+ // Create zip with preserved file permissions
146
+ const zipFiles = {};
147
+ const isUnix = process.platform !== "win32";
148
+ for (const [filePath, fileData] of Object.entries(files)) {
149
+ if (isUnix) {
150
+ // Set external file attributes to preserve Unix permissions
151
+ // The mode needs to be shifted to the upper 16 bits for ZIP format
152
+ zipFiles[filePath] = [
153
+ fileData.data,
154
+ { os: 3, attrs: (fileData.mode & 0o777) << 16 },
155
+ ];
156
+ }
157
+ else {
158
+ // On Windows, use default ZIP attributes (no Unix permissions)
159
+ zipFiles[filePath] = fileData.data;
160
+ }
161
+ }
162
+ const zipData = zipSync(zipFiles, {
163
+ level: 9, // Maximum compression
164
+ mtime: new Date(),
165
+ });
166
+ // Write zip file
167
+ writeFileSync(finalOutputPath, zipData);
168
+ // Calculate SHA sum
169
+ const shasum = createHash("sha1").update(zipData).digest("hex");
170
+ // Print archive details
171
+ const sanitizedName = sanitizeNameForFilename(manifest.name);
172
+ const archiveName = `${sanitizedName}-${manifest.version}.dxt`;
173
+ logger.log("\nArchive Details");
174
+ logger.log(`name: ${manifest.name}`);
175
+ logger.log(`version: ${manifest.version}`);
176
+ logger.log(`filename: ${archiveName}`);
177
+ logger.log(`package size: ${formatFileSize(zipData.length)}`);
178
+ logger.log(`unpacked size: ${formatFileSize(totalUnpackedSize)}`);
179
+ logger.log(`shasum: ${shasum}`);
180
+ logger.log(`total files: ${fileEntries.length}`);
181
+ logger.log(`ignored (.dxtignore) files: ${ignoredCount}`);
182
+ logger.log(`\nOutput: ${finalOutputPath}`);
183
+ return true;
184
+ }
185
+ catch (error) {
186
+ if (error instanceof Error) {
187
+ logger.error(`ERROR: Archive error: ${error.message}`);
188
+ }
189
+ else {
190
+ logger.error("ERROR: Unknown archive error occurred");
191
+ }
192
+ return false;
193
+ }
194
+ }
@@ -0,0 +1,7 @@
1
+ interface UnpackOptions {
2
+ dxtPath: string;
3
+ outputDir?: string;
4
+ silent?: boolean;
5
+ }
6
+ export declare function unpackExtension({ dxtPath, outputDir, silent, }: UnpackOptions): Promise<boolean>;
7
+ export {};
@@ -0,0 +1,101 @@
1
+ import { unzipSync } from "fflate";
2
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync, } from "fs";
3
+ import { join, resolve, sep } from "path";
4
+ import { extractSignatureBlock } from "../node/sign.js";
5
+ import { getLogger } from "../shared/log.js";
6
+ export async function unpackExtension({ dxtPath, outputDir, silent, }) {
7
+ const logger = getLogger({ silent });
8
+ const resolvedDxtPath = resolve(dxtPath);
9
+ if (!existsSync(resolvedDxtPath)) {
10
+ logger.error(`ERROR: DXT file not found: ${dxtPath}`);
11
+ return false;
12
+ }
13
+ const finalOutputDir = outputDir ? resolve(outputDir) : process.cwd();
14
+ if (!existsSync(finalOutputDir)) {
15
+ mkdirSync(finalOutputDir, { recursive: true });
16
+ }
17
+ try {
18
+ const fileContent = readFileSync(resolvedDxtPath);
19
+ const { originalContent } = extractSignatureBlock(fileContent);
20
+ // Parse file attributes from ZIP central directory
21
+ const fileAttributes = new Map();
22
+ const isUnix = process.platform !== "win32";
23
+ if (isUnix) {
24
+ // Parse ZIP central directory to extract file attributes
25
+ const zipBuffer = originalContent;
26
+ // Find end of central directory record
27
+ let eocdOffset = -1;
28
+ for (let i = zipBuffer.length - 22; i >= 0; i--) {
29
+ if (zipBuffer.readUInt32LE(i) === 0x06054b50) {
30
+ eocdOffset = i;
31
+ break;
32
+ }
33
+ }
34
+ if (eocdOffset !== -1) {
35
+ const centralDirOffset = zipBuffer.readUInt32LE(eocdOffset + 16);
36
+ const centralDirEntries = zipBuffer.readUInt16LE(eocdOffset + 8);
37
+ let offset = centralDirOffset;
38
+ for (let i = 0; i < centralDirEntries; i++) {
39
+ if (zipBuffer.readUInt32LE(offset) === 0x02014b50) {
40
+ const externalAttrs = zipBuffer.readUInt32LE(offset + 38);
41
+ const filenameLength = zipBuffer.readUInt16LE(offset + 28);
42
+ const filename = zipBuffer.toString("utf8", offset + 46, offset + 46 + filenameLength);
43
+ // Extract Unix permissions from external attributes (upper 16 bits)
44
+ const mode = (externalAttrs >> 16) & 0o777;
45
+ if (mode > 0) {
46
+ fileAttributes.set(filename, mode);
47
+ }
48
+ const extraFieldLength = zipBuffer.readUInt16LE(offset + 30);
49
+ const commentLength = zipBuffer.readUInt16LE(offset + 32);
50
+ offset += 46 + filenameLength + extraFieldLength + commentLength;
51
+ }
52
+ else {
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ const decompressed = unzipSync(originalContent);
59
+ for (const relativePath in decompressed) {
60
+ if (Object.prototype.hasOwnProperty.call(decompressed, relativePath)) {
61
+ const data = decompressed[relativePath];
62
+ const fullPath = join(finalOutputDir, relativePath);
63
+ // Prevent zip slip attacks by validating the resolved path
64
+ const normalizedPath = resolve(fullPath);
65
+ const normalizedOutputDir = resolve(finalOutputDir);
66
+ if (!normalizedPath.startsWith(normalizedOutputDir + sep) &&
67
+ normalizedPath !== normalizedOutputDir) {
68
+ throw new Error(`Path traversal attempt detected: ${relativePath}`);
69
+ }
70
+ const dir = join(fullPath, "..");
71
+ if (!existsSync(dir)) {
72
+ mkdirSync(dir, { recursive: true });
73
+ }
74
+ writeFileSync(fullPath, data);
75
+ // Restore Unix file permissions if available
76
+ if (isUnix && fileAttributes.has(relativePath)) {
77
+ try {
78
+ const mode = fileAttributes.get(relativePath);
79
+ if (mode !== undefined) {
80
+ chmodSync(fullPath, mode);
81
+ }
82
+ }
83
+ catch (error) {
84
+ // Silently ignore permission errors
85
+ }
86
+ }
87
+ }
88
+ }
89
+ logger.log(`Extension unpacked successfully to ${finalOutputDir}`);
90
+ return true;
91
+ }
92
+ catch (error) {
93
+ if (error instanceof Error) {
94
+ logger.error(`ERROR: Failed to unpack extension: ${error.message}`);
95
+ }
96
+ else {
97
+ logger.error("ERROR: An unknown error occurred during unpacking.");
98
+ }
99
+ return false;
100
+ }
101
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ export * from "./cli/init.js";
2
+ export * from "./cli/pack.js";
3
+ export * from "./schemas.js";
4
+ export * from "./shared/config.js";
5
+ export * from "./types.js";
6
+ export * from "./node/files.js";
7
+ export * from "./node/sign.js";
8
+ export * from "./node/validate.js";
package/dist/cli.js ADDED
@@ -0,0 +1,11 @@
1
+ // CLI-specific exports
2
+ export * from "./cli/init.js";
3
+ export * from "./cli/pack.js";
4
+ // Include all shared exports
5
+ export * from "./schemas.js";
6
+ export * from "./shared/config.js";
7
+ export * from "./types.js";
8
+ // Include node exports since CLI needs them
9
+ export * from "./node/files.js";
10
+ export * from "./node/sign.js";
11
+ export * from "./node/validate.js";
@@ -0,0 +1,9 @@
1
+ export * from "./cli/init.js";
2
+ export * from "./cli/pack.js";
3
+ export * from "./cli/unpack.js";
4
+ export * from "./node/files.js";
5
+ export * from "./node/sign.js";
6
+ export * from "./node/validate.js";
7
+ export * from "./schemas.js";
8
+ export * from "./shared/config.js";
9
+ export * from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ // Default export includes everything (backward compatibility)
2
+ export * from "./cli/init.js";
3
+ export * from "./cli/pack.js";
4
+ export * from "./cli/unpack.js";
5
+ export * from "./node/files.js";
6
+ export * from "./node/sign.js";
7
+ export * from "./node/validate.js";
8
+ export * from "./schemas.js";
9
+ export * from "./shared/config.js";
10
+ export * from "./types.js";
@@ -0,0 +1,20 @@
1
+ export declare const EXCLUDE_PATTERNS: string[];
2
+ /**
3
+ * Read and parse .dxtignore file patterns
4
+ */
5
+ export declare function readDxtIgnorePatterns(baseDir: string): string[];
6
+ /**
7
+ * Used for testing, calls the same methods as the other ignore checks
8
+ */
9
+ export declare function shouldExclude(filePath: string, additionalPatterns?: string[]): boolean;
10
+ export declare function getAllFiles(dirPath: string, baseDir?: string, fileList?: Record<string, Uint8Array>, additionalPatterns?: string[]): Record<string, Uint8Array>;
11
+ interface FileWithPermissions {
12
+ data: Uint8Array;
13
+ mode: number;
14
+ }
15
+ export interface GetAllFilesResult {
16
+ files: Record<string, FileWithPermissions>;
17
+ ignoredCount: number;
18
+ }
19
+ export declare function getAllFilesWithCount(dirPath: string, baseDir?: string, fileList?: Record<string, FileWithPermissions>, additionalPatterns?: string[], ignoredCount?: number): GetAllFilesResult;
20
+ export {};
@@ -0,0 +1,115 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
2
+ import ignore from "ignore";
3
+ import { join, relative, sep } from "path";
4
+ // Files/patterns to exclude from the package
5
+ export const EXCLUDE_PATTERNS = [
6
+ ".DS_Store",
7
+ "Thumbs.db",
8
+ ".gitignore",
9
+ ".git",
10
+ ".dxtignore",
11
+ "*.log",
12
+ ".env*",
13
+ ".npm",
14
+ ".npmrc",
15
+ ".yarnrc",
16
+ ".yarn",
17
+ ".eslintrc",
18
+ ".editorconfig",
19
+ ".prettierrc",
20
+ ".prettierignore",
21
+ ".eslintignore",
22
+ ".nycrc",
23
+ ".babelrc",
24
+ ".pnp.*",
25
+ "node_modules/.cache",
26
+ "node_modules/.bin",
27
+ "*.map",
28
+ ".env.local",
29
+ ".env.*.local",
30
+ "npm-debug.log*",
31
+ "yarn-debug.log*",
32
+ "yarn-error.log*",
33
+ "package-lock.json",
34
+ "yarn.lock",
35
+ "*.dxt",
36
+ "*.d.ts",
37
+ "*.tsbuildinfo",
38
+ "tsconfig.json",
39
+ ];
40
+ /**
41
+ * Read and parse .dxtignore file patterns
42
+ */
43
+ export function readDxtIgnorePatterns(baseDir) {
44
+ const dxtIgnorePath = join(baseDir, ".dxtignore");
45
+ if (!existsSync(dxtIgnorePath)) {
46
+ return [];
47
+ }
48
+ try {
49
+ const content = readFileSync(dxtIgnorePath, "utf-8");
50
+ return content
51
+ .split(/\r?\n/)
52
+ .map((line) => line.trim())
53
+ .filter((line) => line.length > 0 && !line.startsWith("#"));
54
+ }
55
+ catch (error) {
56
+ console.warn(`Warning: Could not read .dxtignore file: ${error instanceof Error ? error.message : "Unknown error"}`);
57
+ return [];
58
+ }
59
+ }
60
+ function buildIgnoreChecker(additionalPatterns) {
61
+ return ignore().add(EXCLUDE_PATTERNS).add(additionalPatterns);
62
+ }
63
+ /**
64
+ * Used for testing, calls the same methods as the other ignore checks
65
+ */
66
+ export function shouldExclude(filePath, additionalPatterns = []) {
67
+ return buildIgnoreChecker(additionalPatterns).ignores(filePath);
68
+ }
69
+ export function getAllFiles(dirPath, baseDir = dirPath, fileList = {}, additionalPatterns = []) {
70
+ const files = readdirSync(dirPath);
71
+ const ignoreChecker = buildIgnoreChecker(additionalPatterns);
72
+ for (const file of files) {
73
+ const filePath = join(dirPath, file);
74
+ const relativePath = relative(baseDir, filePath);
75
+ if (ignoreChecker.ignores(relativePath)) {
76
+ continue;
77
+ }
78
+ const stat = statSync(filePath);
79
+ if (stat.isDirectory()) {
80
+ getAllFiles(filePath, baseDir, fileList, additionalPatterns);
81
+ }
82
+ else {
83
+ // Use forward slashes in zip file paths
84
+ const zipPath = relativePath.split(sep).join("/");
85
+ fileList[zipPath] = readFileSync(filePath);
86
+ }
87
+ }
88
+ return fileList;
89
+ }
90
+ export function getAllFilesWithCount(dirPath, baseDir = dirPath, fileList = {}, additionalPatterns = [], ignoredCount = 0) {
91
+ const files = readdirSync(dirPath);
92
+ const ignoreChecker = buildIgnoreChecker(additionalPatterns);
93
+ for (const file of files) {
94
+ const filePath = join(dirPath, file);
95
+ const relativePath = relative(baseDir, filePath);
96
+ if (ignoreChecker.ignores(relativePath)) {
97
+ ignoredCount++;
98
+ continue;
99
+ }
100
+ const stat = statSync(filePath);
101
+ if (stat.isDirectory()) {
102
+ const result = getAllFilesWithCount(filePath, baseDir, fileList, additionalPatterns, ignoredCount);
103
+ ignoredCount = result.ignoredCount;
104
+ }
105
+ else {
106
+ // Use forward slashes in zip file paths
107
+ const zipPath = relativePath.split(sep).join("/");
108
+ fileList[zipPath] = {
109
+ data: readFileSync(filePath),
110
+ mode: stat.mode,
111
+ };
112
+ }
113
+ }
114
+ return { files: fileList, ignoredCount };
115
+ }
@@ -0,0 +1,32 @@
1
+ import type { DxtSignatureInfo } from "../types.js";
2
+ /**
3
+ * Signs a DXT file with the given certificate and private key using PKCS#7
4
+ *
5
+ * @param dxtPath Path to the DXT file to sign
6
+ * @param certPath Path to the certificate file (PEM format)
7
+ * @param keyPath Path to the private key file (PEM format)
8
+ * @param intermediates Optional array of intermediate certificate paths
9
+ */
10
+ export declare function signDxtFile(dxtPath: string, certPath: string, keyPath: string, intermediates?: string[]): void;
11
+ /**
12
+ * Verifies a signed DXT file using OS certificate store
13
+ *
14
+ * @param dxtPath Path to the signed DXT file
15
+ * @returns Signature information including verification status
16
+ */
17
+ export declare function verifyDxtFile(dxtPath: string): Promise<DxtSignatureInfo>;
18
+ /**
19
+ * Extracts the signature block from a signed DXT file
20
+ */
21
+ export declare function extractSignatureBlock(fileContent: Buffer): {
22
+ originalContent: Buffer;
23
+ pkcs7Signature?: Buffer;
24
+ };
25
+ /**
26
+ * Verifies certificate chain against OS trust store
27
+ */
28
+ export declare function verifyCertificateChain(certificate: Buffer, intermediates?: Buffer[]): Promise<boolean>;
29
+ /**
30
+ * Removes signature from a DXT file
31
+ */
32
+ export declare function unsignDxtFile(dxtPath: string): void;