@typinghare/trick 1.0.0

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.
Files changed (38) hide show
  1. package/.prettierrc.yml +6 -0
  2. package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/Project.xml +52 -0
  3. package/.wander/jameschan312.cn@gmail.com/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.wander/jameschan312.cn@gmail.com/.idea/jsLibraryMappings.xml +6 -0
  5. package/.wander/jameschan312.cn@gmail.com/.idea/misc.xml +6 -0
  6. package/.wander/jameschan312.cn@gmail.com/.idea/modules.xml +8 -0
  7. package/.wander/jameschan312.cn@gmail.com/.idea/prettier.xml +6 -0
  8. package/.wander/jameschan312.cn@gmail.com/.idea/trick.iml +14 -0
  9. package/.wander/jameschan312.cn@gmail.com/.idea/vcs.xml +6 -0
  10. package/.wander/jameschan312.cn@gmail.com/.idea/webResources.xml +14 -0
  11. package/README.md +7 -0
  12. package/bin/trick +3 -0
  13. package/dist/app.d.ts +1 -0
  14. package/dist/app.js +65 -0
  15. package/dist/config.d.ts +22 -0
  16. package/dist/config.js +51 -0
  17. package/dist/constant.d.ts +2 -0
  18. package/dist/constant.js +3 -0
  19. package/dist/encrypt.d.ts +12 -0
  20. package/dist/encrypt.js +86 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.js +6 -0
  23. package/dist/secret.d.ts +5 -0
  24. package/dist/secret.js +14 -0
  25. package/dist/utility.d.ts +1 -0
  26. package/dist/utility.js +27 -0
  27. package/package.json +27 -0
  28. package/src/app.ts +84 -0
  29. package/src/config.ts +73 -0
  30. package/src/constant.ts +4 -0
  31. package/src/encrypt.ts +112 -0
  32. package/src/index.ts +6 -0
  33. package/src/secret.ts +14 -0
  34. package/src/utility.ts +38 -0
  35. package/test/resources/really.json +4 -0
  36. package/test/resources/task.yml +4 -0
  37. package/trick.config.json +12 -0
  38. package/tsconfig.json +17 -0
@@ -0,0 +1,6 @@
1
+ trailingComma: es5
2
+ tabWidth: 4
3
+ semi: false
4
+ singleQuote: true
5
+ endOfLine: lf
6
+ printWidth: 80
@@ -0,0 +1,52 @@
1
+ <component name="ProjectCodeStyleConfiguration">
2
+ <code_scheme name="Project" version="173">
3
+ <option name="LINE_SEPARATOR" value="&#10;" />
4
+ <option name="RIGHT_MARGIN" value="80" />
5
+ <HTMLCodeStyleSettings>
6
+ <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" />
7
+ </HTMLCodeStyleSettings>
8
+ <JSCodeStyleSettings version="0">
9
+ <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
10
+ <option name="FORCE_SEMICOLON_STYLE" value="true" />
11
+ <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
12
+ <option name="USE_DOUBLE_QUOTES" value="false" />
13
+ <option name="FORCE_QUOTE_STYlE" value="true" />
14
+ <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
15
+ <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
16
+ <option name="SPACES_WITHIN_IMPORTS" value="true" />
17
+ </JSCodeStyleSettings>
18
+ <TypeScriptCodeStyleSettings version="0">
19
+ <option name="USE_SEMICOLON_AFTER_STATEMENT" value="false" />
20
+ <option name="FORCE_SEMICOLON_STYLE" value="true" />
21
+ <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" />
22
+ <option name="USE_DOUBLE_QUOTES" value="false" />
23
+ <option name="FORCE_QUOTE_STYlE" value="true" />
24
+ <option name="ENFORCE_TRAILING_COMMA" value="WhenMultiline" />
25
+ <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" />
26
+ <option name="SPACES_WITHIN_IMPORTS" value="true" />
27
+ </TypeScriptCodeStyleSettings>
28
+ <VueCodeStyleSettings>
29
+ <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" />
30
+ <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" />
31
+ </VueCodeStyleSettings>
32
+ <codeStyleSettings language="HTML">
33
+ <option name="SOFT_MARGINS" value="80" />
34
+ <indentOptions>
35
+ <option name="CONTINUATION_INDENT_SIZE" value="4" />
36
+ </indentOptions>
37
+ </codeStyleSettings>
38
+ <codeStyleSettings language="JavaScript">
39
+ <option name="SOFT_MARGINS" value="80" />
40
+ </codeStyleSettings>
41
+ <codeStyleSettings language="TypeScript">
42
+ <option name="SOFT_MARGINS" value="80" />
43
+ </codeStyleSettings>
44
+ <codeStyleSettings language="Vue">
45
+ <option name="SOFT_MARGINS" value="80" />
46
+ <indentOptions>
47
+ <option name="INDENT_SIZE" value="4" />
48
+ <option name="TAB_SIZE" value="4" />
49
+ </indentOptions>
50
+ </codeStyleSettings>
51
+ </code_scheme>
52
+ </component>
@@ -0,0 +1,5 @@
1
+ <component name="ProjectCodeStyleConfiguration">
2
+ <state>
3
+ <option name="USE_PER_PROJECT_SETTINGS" value="true" />
4
+ </state>
5
+ </component>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="JavaScriptLibraryMappings">
4
+ <includedPredefinedLibrary name="Node.js Core" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="MarkdownSettingsMigration">
4
+ <option name="stateVersion" value="1" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectModuleManager">
4
+ <modules>
5
+ <module fileurl="file://$PROJECT_DIR$/.idea/trick.iml" filepath="$PROJECT_DIR$/.idea/trick.iml" />
6
+ </modules>
7
+ </component>
8
+ </project>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="PrettierConfiguration">
4
+ <option name="myConfigurationMode" value="AUTOMATIC" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="WEB_MODULE" version="4">
3
+ <component name="NewModuleRootManager">
4
+ <content url="file://$MODULE_DIR$">
5
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
6
+ <excludeFolder url="file://$MODULE_DIR$/.tmp" />
7
+ <excludeFolder url="file://$MODULE_DIR$/dist" />
8
+ <excludeFolder url="file://$MODULE_DIR$/temp" />
9
+ <excludeFolder url="file://$MODULE_DIR$/tmp" />
10
+ </content>
11
+ <orderEntry type="inheritedJdk" />
12
+ <orderEntry type="sourceFolder" forTests="false" />
13
+ </component>
14
+ </module>
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
@@ -0,0 +1,14 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="WebResourcesPaths">
4
+ <contentEntries>
5
+ <entry url="file://$PROJECT_DIR$">
6
+ <entryData>
7
+ <resourceRoots>
8
+ <path value="file://$PROJECT_DIR$/src" />
9
+ </resourceRoots>
10
+ </entryData>
11
+ </entry>
12
+ </contentEntries>
13
+ </component>
14
+ </project>
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # [Trick](https://github.com/typinghare/trick)
2
+
3
+ # Install
4
+
5
+ ```shell
6
+ pnpm add -g @typinghare/trick
7
+ ```
package/bin/trick ADDED
@@ -0,0 +1,3 @@
1
+ #! /usr/bin/env node
2
+
3
+ import '../dist/index.js'
package/dist/app.d.ts ADDED
@@ -0,0 +1 @@
1
+ export {};
package/dist/app.js ADDED
@@ -0,0 +1,65 @@
1
+ import { Command } from 'commander';
2
+ import { getTargetFromConfig, updateConfig } from './config.js';
3
+ import { decryptFiles, encryptFiles } from './encrypt.js';
4
+ import { getSecret } from './secret.js';
5
+ import { TRICK_ENCRYPTED_DIR } from './constant.js';
6
+ import fsExtra from 'fs-extra';
7
+ import { resolve_error } from './utility.js';
8
+ const program = new Command();
9
+ program.version('Trick v1.0.0 \nby James Chen (jameschan312.cn@gmail.com)');
10
+ program.description('Save credential files to remote safely.');
11
+ program
12
+ .command('add')
13
+ .description('Adds a target.')
14
+ .argument('<secret-name>', 'The name of secret in the environment')
15
+ .argument('[files...]', 'Files this target will encrypt')
16
+ .action(async (secretName, files) => {
17
+ await updateConfig((config) => {
18
+ try {
19
+ getTargetFromConfig(config, secretName);
20
+ }
21
+ catch (err) {
22
+ config.targets.push({
23
+ secret_name: secretName,
24
+ files,
25
+ });
26
+ return true;
27
+ }
28
+ console.error(`Target with the secret name already exists: ${secretName}`);
29
+ console.error('Abort adding target');
30
+ process.exit(1);
31
+ });
32
+ });
33
+ program
34
+ .command('encrypt')
35
+ .description('Encrypt the credential files.')
36
+ .argument('<secret-name>', 'The name of secret in the environment')
37
+ .action(async (secretName) => {
38
+ await updateConfig((config) => {
39
+ const target = getTargetFromConfig(config, secretName);
40
+ const secret = getSecret(target.secret_name);
41
+ const srcFilePaths = target.files;
42
+ fsExtra.ensureDir(TRICK_ENCRYPTED_DIR);
43
+ encryptFiles(srcFilePaths, TRICK_ENCRYPTED_DIR, secret, config.iteration_count);
44
+ return false;
45
+ });
46
+ });
47
+ program
48
+ .command('decrypt')
49
+ .description('Decrypt the credential files.')
50
+ .argument('<secret-name>', 'The name of secret in the environment')
51
+ .action(async (secretName) => {
52
+ await updateConfig((config) => {
53
+ const target = getTargetFromConfig(config, secretName);
54
+ const secret = getSecret(target.secret_name);
55
+ const srcFilePaths = target.files;
56
+ fsExtra.ensureDir(TRICK_ENCRYPTED_DIR);
57
+ decryptFiles(srcFilePaths, TRICK_ENCRYPTED_DIR, secret, config.iteration_count);
58
+ return false;
59
+ });
60
+ });
61
+ program.parse();
62
+ process.on('uncaughtException', (err) => {
63
+ resolve_error(err);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,22 @@
1
+ export declare const CONFIG_FILE_NAME: string;
2
+ export interface Config {
3
+ iteration_count: number;
4
+ targets: Target[];
5
+ }
6
+ export interface Target {
7
+ secret_name: string;
8
+ files: string[];
9
+ }
10
+ export declare class WriteConfigError extends Error {
11
+ }
12
+ export declare class ReadConfigError extends Error {
13
+ }
14
+ export declare function writeConfig(config: Config): Promise<void>;
15
+ export declare function readConfig(): Promise<Config | null>;
16
+ export declare function updateConfig(callback: UpdateConfigCallback): Promise<void>;
17
+ export type UpdateConfigCallback = (config: Config) => boolean;
18
+ export declare class TargetNotFoundError extends Error {
19
+ readonly secretName: string;
20
+ constructor(secretName: string);
21
+ }
22
+ export declare function getTargetFromConfig(config: Config, secretName: string): Target;
package/dist/config.js ADDED
@@ -0,0 +1,51 @@
1
+ import fsExtra from 'fs-extra';
2
+ export const CONFIG_FILE_NAME = 'trick.config.json';
3
+ const defaultConfig = {
4
+ iteration_count: 114514,
5
+ targets: [],
6
+ };
7
+ export class WriteConfigError extends Error {
8
+ }
9
+ export class ReadConfigError extends Error {
10
+ }
11
+ export async function writeConfig(config) {
12
+ try {
13
+ await fsExtra.writeJson(CONFIG_FILE_NAME, config);
14
+ }
15
+ catch (err) {
16
+ throw new WriteConfigError();
17
+ }
18
+ }
19
+ export async function readConfig() {
20
+ if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
21
+ return null;
22
+ }
23
+ try {
24
+ return (await fsExtra.readJSON(CONFIG_FILE_NAME));
25
+ }
26
+ catch (err) {
27
+ throw new ReadConfigError();
28
+ }
29
+ }
30
+ export async function updateConfig(callback) {
31
+ const config = (await readConfig()) || defaultConfig;
32
+ if (callback(config)) {
33
+ await writeConfig(config);
34
+ }
35
+ }
36
+ export class TargetNotFoundError extends Error {
37
+ secretName;
38
+ constructor(secretName) {
39
+ super(`Target not found: ${secretName}`);
40
+ this.secretName = secretName;
41
+ }
42
+ }
43
+ export function getTargetFromConfig(config, secretName) {
44
+ const targets = config.targets;
45
+ for (const target of targets) {
46
+ if (target.secret_name === secretName) {
47
+ return target;
48
+ }
49
+ }
50
+ throw new TargetNotFoundError(secretName);
51
+ }
@@ -0,0 +1,2 @@
1
+ export declare const TRICK_ROOT_DIR = ".trick";
2
+ export declare const TRICK_ENCRYPTED_DIR: string;
@@ -0,0 +1,3 @@
1
+ import * as path from 'node:path';
2
+ export const TRICK_ROOT_DIR = '.trick';
3
+ export const TRICK_ENCRYPTED_DIR = path.join(TRICK_ROOT_DIR, 'encrypted');
@@ -0,0 +1,12 @@
1
+ export declare class FailToEncryptFileError extends Error {
2
+ readonly srcFilePath: string;
3
+ constructor(srcFilePath: string);
4
+ }
5
+ export declare class FailToDecryptFileError extends Error {
6
+ readonly destFilePath: string;
7
+ constructor(destFilePath: string);
8
+ }
9
+ export declare function encryptFile(srcFilePath: string, destFilePath: string, secret: string, iteration_count: number): Promise<void>;
10
+ export declare function decryptFile(srcFilePath: string, destFilePath: string, secret: string, iteration_count: number): Promise<void>;
11
+ export declare function encryptFiles(srcFilePaths: string[], destDir: string, secret: string, iteration_count: number): Promise<void>;
12
+ export declare function decryptFiles(srcFilePaths: string[], destDir: string, secret: string, iteration_count: number): Promise<void>;
@@ -0,0 +1,86 @@
1
+ import { execa } from 'execa';
2
+ import * as path from 'node:path';
3
+ import fsExtra from 'fs-extra';
4
+ export class FailToEncryptFileError extends Error {
5
+ srcFilePath;
6
+ constructor(srcFilePath) {
7
+ super(`Fail to encrypt source file: ${srcFilePath}`);
8
+ this.srcFilePath = srcFilePath;
9
+ }
10
+ }
11
+ export class FailToDecryptFileError extends Error {
12
+ destFilePath;
13
+ constructor(destFilePath) {
14
+ super(`Fail to decrypt destination file: ${destFilePath}`);
15
+ this.destFilePath = destFilePath;
16
+ }
17
+ }
18
+ export async function encryptFile(srcFilePath, destFilePath, secret, iteration_count) {
19
+ if (!(await fsExtra.pathExists(srcFilePath))) {
20
+ throw new FailToEncryptFileError(srcFilePath);
21
+ }
22
+ const command = [
23
+ 'openssl',
24
+ 'enc',
25
+ '-aes-256-cbc',
26
+ '-salt',
27
+ '-pbkdf2',
28
+ '-iter',
29
+ iteration_count,
30
+ '-in',
31
+ srcFilePath,
32
+ '-out',
33
+ destFilePath,
34
+ '-pass',
35
+ `pass:${secret}`,
36
+ ].join(' ');
37
+ await fsExtra.ensureDir(path.dirname(destFilePath));
38
+ try {
39
+ await execa(`${command}`, { shell: true });
40
+ }
41
+ catch (err) {
42
+ throw new FailToEncryptFileError(srcFilePath);
43
+ }
44
+ }
45
+ export async function decryptFile(srcFilePath, destFilePath, secret, iteration_count) {
46
+ if (!(await fsExtra.pathExists(destFilePath))) {
47
+ throw new FailToDecryptFileError(destFilePath);
48
+ }
49
+ const command = [
50
+ 'openssl',
51
+ 'enc',
52
+ '-d',
53
+ '-aes-256-cbc',
54
+ '-salt',
55
+ '-pbkdf2',
56
+ '-iter',
57
+ iteration_count,
58
+ '-in',
59
+ destFilePath,
60
+ '-out',
61
+ srcFilePath,
62
+ '-pass',
63
+ `pass:${secret}`,
64
+ ].join(' ');
65
+ await fsExtra.ensureDir(path.dirname(srcFilePath));
66
+ try {
67
+ await execa(`${command}`, { shell: true });
68
+ }
69
+ catch (err) {
70
+ throw new FailToDecryptFileError(destFilePath);
71
+ }
72
+ }
73
+ export async function encryptFiles(srcFilePaths, destDir, secret, iteration_count) {
74
+ for (const srcFilePath of srcFilePaths) {
75
+ const destFilePath = path.join(destDir, srcFilePath);
76
+ await encryptFile(srcFilePath, destFilePath, secret, iteration_count);
77
+ console.log(`[ENCRYPTED] ${srcFilePath} -> ${destFilePath}`);
78
+ }
79
+ }
80
+ export async function decryptFiles(srcFilePaths, destDir, secret, iteration_count) {
81
+ for (const srcFilePath of srcFilePaths) {
82
+ const destFilePath = path.join(destDir, srcFilePath);
83
+ await decryptFile(srcFilePath, destFilePath, secret, iteration_count);
84
+ console.log(`[DECRYPTED] ${destFilePath} -> ${srcFilePath}`);
85
+ }
86
+ }
@@ -0,0 +1,6 @@
1
+ export * from './app.js';
2
+ export * from './config.js';
3
+ export * from './constant.js';
4
+ export * from './encrypt.js';
5
+ export * from './utility.js';
6
+ export * from './secret.js';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './app.js';
2
+ export * from './config.js';
3
+ export * from './constant.js';
4
+ export * from './encrypt.js';
5
+ export * from './utility.js';
6
+ export * from './secret.js';
@@ -0,0 +1,5 @@
1
+ export declare class SecretNotFoundError extends Error {
2
+ readonly secretName: string;
3
+ constructor(secretName: string);
4
+ }
5
+ export declare function getSecret(secretName: string): string;
package/dist/secret.js ADDED
@@ -0,0 +1,14 @@
1
+ export class SecretNotFoundError extends Error {
2
+ secretName;
3
+ constructor(secretName) {
4
+ super(`Secret ${secretName} is not presented in the environment`);
5
+ this.secretName = secretName;
6
+ }
7
+ }
8
+ export function getSecret(secretName) {
9
+ const secret = process.env[secretName];
10
+ if (secret === undefined) {
11
+ throw new SecretNotFoundError(secretName);
12
+ }
13
+ return secret;
14
+ }
@@ -0,0 +1 @@
1
+ export declare function resolve_error(err: any): void;
@@ -0,0 +1,27 @@
1
+ import { ReadConfigError, TargetNotFoundError, WriteConfigError, } from './config.js';
2
+ import { FailToDecryptFileError, FailToEncryptFileError } from './encrypt.js';
3
+ import chalk from 'chalk';
4
+ export function resolve_error(err) {
5
+ if (!(err instanceof Error)) {
6
+ console.error(`Unknown error: ${err}`);
7
+ process.exit(2);
8
+ }
9
+ if (err instanceof WriteConfigError) {
10
+ console.error(chalk.red('Fail to write Trick config file'));
11
+ }
12
+ else if (err instanceof ReadConfigError) {
13
+ console.error(chalk.red('Fail to read Trick config file'));
14
+ }
15
+ else if (err instanceof TargetNotFoundError) {
16
+ console.error(chalk.red(err.message));
17
+ }
18
+ else if (err instanceof FailToEncryptFileError) {
19
+ console.error(chalk.red(err.message));
20
+ console.error(chalk.yellow('Make sure the file exists and you have enough permission to access'));
21
+ }
22
+ else if (err instanceof FailToDecryptFileError) {
23
+ console.error(chalk.red(err.message));
24
+ console.error(chalk.yellow('Make sure the file exists and you have enough permission to access'));
25
+ }
26
+ process.exit(1);
27
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@typinghare/trick",
3
+ "description": "Save credential files to remote safely.",
4
+ "version": "1.0.0",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "author": {
9
+ "name": "James Chen",
10
+ "email": "jameschan312.cn@gmail.com"
11
+ },
12
+ "homepage": "https://github.com/typinghare/trick",
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "chalk": "^5.3.0",
16
+ "commander": "^12.1.0",
17
+ "execa": "^9.3.0",
18
+ "fs-extra": "^11.2.0"
19
+ },
20
+ "devDependencies": {
21
+ "@types/fs-extra": "^11.0.4",
22
+ "prettier": "^3.3.3"
23
+ },
24
+ "scripts": {
25
+ "build": "tsc"
26
+ }
27
+ }
package/src/app.ts ADDED
@@ -0,0 +1,84 @@
1
+ import { Command } from 'commander'
2
+ import { getTargetFromConfig, Target, updateConfig } from './config.js'
3
+ import { decryptFiles, encryptFiles } from './encrypt.js'
4
+ import { getSecret } from './secret.js'
5
+ import { TRICK_ENCRYPTED_DIR } from './constant.js'
6
+ import fsExtra from 'fs-extra'
7
+ import { resolve_error } from './utility.js'
8
+
9
+ const program = new Command()
10
+
11
+ program.version('Trick v1.0.0 \nby James Chen (jameschan312.cn@gmail.com)')
12
+ program.description('Save credential files to remote safely.')
13
+
14
+ program
15
+ .command('add')
16
+ .description('Adds a target.')
17
+ .argument('<secret-name>', 'The name of secret in the environment')
18
+ .argument('[files...]', 'Files this target will encrypt')
19
+ .action(async (secretName: string, files: string[]): Promise<void> => {
20
+ await updateConfig((config) => {
21
+ try {
22
+ getTargetFromConfig(config, secretName)
23
+ } catch (err) {
24
+ config.targets.push({
25
+ secret_name: secretName,
26
+ files,
27
+ })
28
+ return true
29
+ }
30
+
31
+ console.error(
32
+ `Target with the secret name already exists: ${secretName}`
33
+ )
34
+ console.error('Abort adding target')
35
+ process.exit(1)
36
+ })
37
+ })
38
+
39
+ program
40
+ .command('encrypt')
41
+ .description('Encrypt the credential files.')
42
+ .argument('<secret-name>', 'The name of secret in the environment')
43
+ .action(async (secretName: string): Promise<void> => {
44
+ await updateConfig((config) => {
45
+ const target: Target = getTargetFromConfig(config, secretName)
46
+ const secret: string = getSecret(target.secret_name)
47
+ const srcFilePaths: string[] = target.files
48
+ fsExtra.ensureDir(TRICK_ENCRYPTED_DIR)
49
+ encryptFiles(
50
+ srcFilePaths,
51
+ TRICK_ENCRYPTED_DIR,
52
+ secret,
53
+ config.iteration_count
54
+ )
55
+ return false
56
+ })
57
+ })
58
+
59
+ program
60
+ .command('decrypt')
61
+ .description('Decrypt the credential files.')
62
+ .argument('<secret-name>', 'The name of secret in the environment')
63
+ .action(async (secretName: string): Promise<void> => {
64
+ await updateConfig((config) => {
65
+ const target: Target = getTargetFromConfig(config, secretName)
66
+ const secret: string = getSecret(target.secret_name)
67
+ const srcFilePaths: string[] = target.files
68
+ fsExtra.ensureDir(TRICK_ENCRYPTED_DIR)
69
+ decryptFiles(
70
+ srcFilePaths,
71
+ TRICK_ENCRYPTED_DIR,
72
+ secret,
73
+ config.iteration_count
74
+ )
75
+ return false
76
+ })
77
+ })
78
+
79
+ program.parse()
80
+
81
+ process.on('uncaughtException', (err) => {
82
+ resolve_error(err)
83
+ process.exit(1)
84
+ })
package/src/config.ts ADDED
@@ -0,0 +1,73 @@
1
+ import fsExtra from 'fs-extra'
2
+
3
+ export const CONFIG_FILE_NAME: string = 'trick.config.json'
4
+
5
+ export interface Config {
6
+ iteration_count: number
7
+ targets: Target[]
8
+ }
9
+
10
+ export interface Target {
11
+ secret_name: string
12
+ files: string[]
13
+ }
14
+
15
+ const defaultConfig: Config = {
16
+ iteration_count: 114514,
17
+ targets: [],
18
+ }
19
+
20
+ export class WriteConfigError extends Error {}
21
+
22
+ export class ReadConfigError extends Error {}
23
+
24
+ export async function writeConfig(config: Config): Promise<void> {
25
+ try {
26
+ await fsExtra.writeJson(CONFIG_FILE_NAME, config)
27
+ } catch (err) {
28
+ throw new WriteConfigError()
29
+ }
30
+ }
31
+
32
+ export async function readConfig(): Promise<Config | null> {
33
+ if (!fsExtra.existsSync(CONFIG_FILE_NAME)) {
34
+ return null
35
+ }
36
+
37
+ try {
38
+ return (await fsExtra.readJSON(CONFIG_FILE_NAME)) as Config
39
+ } catch (err) {
40
+ throw new ReadConfigError()
41
+ }
42
+ }
43
+
44
+ export async function updateConfig(
45
+ callback: UpdateConfigCallback
46
+ ): Promise<void> {
47
+ const config: Config = (await readConfig()) || defaultConfig
48
+ if (callback(config)) {
49
+ await writeConfig(config)
50
+ }
51
+ }
52
+
53
+ export type UpdateConfigCallback = (config: Config) => boolean
54
+
55
+ export class TargetNotFoundError extends Error {
56
+ public constructor(public readonly secretName: string) {
57
+ super(`Target not found: ${secretName}`)
58
+ }
59
+ }
60
+
61
+ export function getTargetFromConfig(
62
+ config: Config,
63
+ secretName: string
64
+ ): Target {
65
+ const targets = config.targets
66
+ for (const target of targets) {
67
+ if (target.secret_name === secretName) {
68
+ return target
69
+ }
70
+ }
71
+
72
+ throw new TargetNotFoundError(secretName)
73
+ }
@@ -0,0 +1,4 @@
1
+ import * as path from 'node:path'
2
+
3
+ export const TRICK_ROOT_DIR = '.trick'
4
+ export const TRICK_ENCRYPTED_DIR = path.join(TRICK_ROOT_DIR, 'encrypted')
package/src/encrypt.ts ADDED
@@ -0,0 +1,112 @@
1
+ import { execa } from 'execa'
2
+ import * as path from 'node:path'
3
+ import fsExtra from 'fs-extra'
4
+
5
+ export class FailToEncryptFileError extends Error {
6
+ public constructor(public readonly srcFilePath: string) {
7
+ super(`Fail to encrypt source file: ${srcFilePath}`)
8
+ }
9
+ }
10
+
11
+ export class FailToDecryptFileError extends Error {
12
+ public constructor(public readonly destFilePath: string) {
13
+ super(`Fail to decrypt destination file: ${destFilePath}`)
14
+ }
15
+ }
16
+
17
+ export async function encryptFile(
18
+ srcFilePath: string,
19
+ destFilePath: string,
20
+ secret: string,
21
+ iteration_count: number
22
+ ): Promise<void> {
23
+ if (!(await fsExtra.pathExists(srcFilePath))) {
24
+ throw new FailToEncryptFileError(srcFilePath)
25
+ }
26
+
27
+ const command: string = [
28
+ 'openssl',
29
+ 'enc',
30
+ '-aes-256-cbc',
31
+ '-salt',
32
+ '-pbkdf2',
33
+ '-iter',
34
+ iteration_count,
35
+ '-in',
36
+ srcFilePath,
37
+ '-out',
38
+ destFilePath,
39
+ '-pass',
40
+ `pass:${secret}`,
41
+ ].join(' ')
42
+
43
+ await fsExtra.ensureDir(path.dirname(destFilePath))
44
+
45
+ try {
46
+ await execa(`${command}`, { shell: true })
47
+ } catch (err) {
48
+ throw new FailToEncryptFileError(srcFilePath)
49
+ }
50
+ }
51
+
52
+ export async function decryptFile(
53
+ srcFilePath: string,
54
+ destFilePath: string,
55
+ secret: string,
56
+ iteration_count: number
57
+ ): Promise<void> {
58
+ if (!(await fsExtra.pathExists(destFilePath))) {
59
+ throw new FailToDecryptFileError(destFilePath)
60
+ }
61
+
62
+ const command: string = [
63
+ 'openssl',
64
+ 'enc',
65
+ '-d',
66
+ '-aes-256-cbc',
67
+ '-salt',
68
+ '-pbkdf2',
69
+ '-iter',
70
+ iteration_count,
71
+ '-in',
72
+ destFilePath,
73
+ '-out',
74
+ srcFilePath,
75
+ '-pass',
76
+ `pass:${secret}`,
77
+ ].join(' ')
78
+
79
+ await fsExtra.ensureDir(path.dirname(srcFilePath))
80
+
81
+ try {
82
+ await execa(`${command}`, { shell: true })
83
+ } catch (err) {
84
+ throw new FailToDecryptFileError(destFilePath)
85
+ }
86
+ }
87
+
88
+ export async function encryptFiles(
89
+ srcFilePaths: string[],
90
+ destDir: string,
91
+ secret: string,
92
+ iteration_count: number
93
+ ): Promise<void> {
94
+ for (const srcFilePath of srcFilePaths) {
95
+ const destFilePath: string = path.join(destDir, srcFilePath)
96
+ await encryptFile(srcFilePath, destFilePath, secret, iteration_count)
97
+ console.log(`[ENCRYPTED] ${srcFilePath} -> ${destFilePath}`)
98
+ }
99
+ }
100
+
101
+ export async function decryptFiles(
102
+ srcFilePaths: string[],
103
+ destDir: string,
104
+ secret: string,
105
+ iteration_count: number
106
+ ): Promise<void> {
107
+ for (const srcFilePath of srcFilePaths) {
108
+ const destFilePath: string = path.join(destDir, srcFilePath)
109
+ await decryptFile(srcFilePath, destFilePath, secret, iteration_count)
110
+ console.log(`[DECRYPTED] ${destFilePath} -> ${srcFilePath}`)
111
+ }
112
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export * from './app.js'
2
+ export * from './config.js'
3
+ export * from './constant.js'
4
+ export * from './encrypt.js'
5
+ export * from './utility.js'
6
+ export * from './secret.js'
package/src/secret.ts ADDED
@@ -0,0 +1,14 @@
1
+ export class SecretNotFoundError extends Error {
2
+ public constructor(public readonly secretName: string) {
3
+ super(`Secret ${secretName} is not presented in the environment`)
4
+ }
5
+ }
6
+
7
+ export function getSecret(secretName: string): string {
8
+ const secret = process.env[secretName]
9
+ if (secret === undefined) {
10
+ throw new SecretNotFoundError(secretName)
11
+ }
12
+
13
+ return secret
14
+ }
package/src/utility.ts ADDED
@@ -0,0 +1,38 @@
1
+ import {
2
+ ReadConfigError,
3
+ TargetNotFoundError,
4
+ WriteConfigError,
5
+ } from './config.js'
6
+ import { FailToDecryptFileError, FailToEncryptFileError } from './encrypt.js'
7
+ import chalk from 'chalk'
8
+
9
+ export function resolve_error(err: any): void {
10
+ if (!(err instanceof Error)) {
11
+ console.error(`Unknown error: ${err}`)
12
+ process.exit(2)
13
+ }
14
+
15
+ if (err instanceof WriteConfigError) {
16
+ console.error(chalk.red('Fail to write Trick config file'))
17
+ } else if (err instanceof ReadConfigError) {
18
+ console.error(chalk.red('Fail to read Trick config file'))
19
+ } else if (err instanceof TargetNotFoundError) {
20
+ console.error(chalk.red(err.message))
21
+ } else if (err instanceof FailToEncryptFileError) {
22
+ console.error(chalk.red(err.message))
23
+ console.error(
24
+ chalk.yellow(
25
+ 'Make sure the file exists and you have enough permission to access'
26
+ )
27
+ )
28
+ } else if (err instanceof FailToDecryptFileError) {
29
+ console.error(chalk.red(err.message))
30
+ console.error(
31
+ chalk.yellow(
32
+ 'Make sure the file exists and you have enough permission to access'
33
+ )
34
+ )
35
+ }
36
+
37
+ process.exit(1)
38
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "message": "Ok",
3
+ "userId": 123
4
+ }
@@ -0,0 +1,4 @@
1
+ my_task:
2
+ name: My Task
3
+ time_unit: milliseconds
4
+ time: 350
@@ -0,0 +1,12 @@
1
+ {
2
+ "iteration_count": 114514,
3
+ "targets": [
4
+ {
5
+ "secret_name": "TEST_SECRET",
6
+ "files": [
7
+ "test/resources/really.json",
8
+ "test/resources/task.yml"
9
+ ]
10
+ }
11
+ ]
12
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "NodeNext",
5
+ "outDir": "dist",
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "strict": true,
9
+ "skipDefaultLibCheck": true,
10
+ "skipLibCheck": true,
11
+ "moduleResolution": "NodeNext",
12
+ "declaration": true
13
+ },
14
+ "include": [
15
+ "src/**/*.ts"
16
+ ]
17
+ }