@ongtrieuhau861457/runner-tailscale-sync 1.260202.11920

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,129 @@
1
+ /**
2
+ * config.js
3
+ * Load configuration từ .env, CLI flags, và defaults
4
+ */
5
+
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+ const os = require("os");
9
+ const CONST = require("./constants");
10
+
11
+ class Config {
12
+ constructor(options = {}) {
13
+ // Load .env file nếu có
14
+ this.loadEnvFile();
15
+
16
+ // Determine CWD (priority: CLI flag > env > process.cwd())
17
+ this.cwd = options.cwd || process.env.TOOL_CWD || process.cwd();
18
+
19
+ // Runner data directory
20
+ this.runnerDataDir = path.join(this.cwd, CONST.RUNNER_DATA_DIR);
21
+ this.logsDir = path.join(this.runnerDataDir, CONST.LOGS_DIR);
22
+ this.pidDir = path.join(this.runnerDataDir, CONST.PID_DIR);
23
+ this.dataServicesDir = path.join(this.runnerDataDir, CONST.DATA_SERVICES_DIR);
24
+ this.tmpDir = path.join(this.runnerDataDir, CONST.TMP_DIR);
25
+
26
+ // Tailscale config
27
+ this.tailscaleClientId = process.env.TAILSCALE_CLIENT_ID || "";
28
+ this.tailscaleClientSecret = process.env.TAILSCALE_CLIENT_SECRET || "";
29
+ this.tailscaleTags = process.env.TAILSCALE_TAGS || CONST.DEFAULT_TAG;
30
+ this.tailscaleEnable = String(process.env.TAILSCALE_ENABLE || "").trim() === "1";
31
+
32
+ // Services to stop on previous runner
33
+ this.servicesToStop = this.parseServicesList(
34
+ process.env.SERVICES_TO_STOP || "cloudflared,pocketbase,http-server"
35
+ );
36
+
37
+ // Platform detection
38
+ this.isWindows = os.platform() === "win32";
39
+ this.isLinux = os.platform() === "linux";
40
+ this.isMacOS = os.platform() === "darwin";
41
+
42
+ // Logging
43
+ this.verbose = options.verbose || false;
44
+ this.quiet = options.quiet || false;
45
+
46
+ // Git
47
+ this.gitEnabled = String(process.env.GIT_PUSH_ENABLED || "1").trim() === "1";
48
+ this.gitBranch = process.env.GIT_BRANCH || "main";
49
+
50
+ // SSH/Rsync paths (for Windows)
51
+ this.sshPath = process.env.SSH_PATH || "ssh";
52
+ this.rsyncPath = process.env.RSYNC_PATH || "rsync";
53
+ }
54
+
55
+ /**
56
+ * Load .env file
57
+ */
58
+ loadEnvFile() {
59
+ const envPath = path.join(process.cwd(), ".env");
60
+ if (!fs.existsSync(envPath)) return;
61
+
62
+ try {
63
+ const content = fs.readFileSync(envPath, "utf8");
64
+ content.split("\n").forEach(line => {
65
+ const trimmed = line.trim();
66
+ if (!trimmed || trimmed.startsWith("#")) return;
67
+
68
+ const match = trimmed.match(/^([^=]+)=(.*)$/);
69
+ if (match) {
70
+ const key = match[1].trim();
71
+ let value = match[2].trim();
72
+
73
+ // Remove quotes
74
+ if ((value.startsWith('"') && value.endsWith('"')) ||
75
+ (value.startsWith("'") && value.endsWith("'"))) {
76
+ value = value.slice(1, -1);
77
+ }
78
+
79
+ // Only set if not already in env
80
+ if (!process.env[key]) {
81
+ process.env[key] = value;
82
+ }
83
+ }
84
+ });
85
+ } catch (err) {
86
+ // Ignore errors
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Parse comma-separated services list
92
+ */
93
+ parseServicesList(str) {
94
+ return str.split(",").map(s => s.trim()).filter(Boolean);
95
+ }
96
+
97
+ /**
98
+ * Validate required config
99
+ */
100
+ validate() {
101
+ const errors = [];
102
+
103
+ if (this.tailscaleEnable) {
104
+ if (!this.tailscaleClientId) {
105
+ errors.push("TAILSCALE_CLIENT_ID is required when TAILSCALE_ENABLE=1");
106
+ }
107
+ if (!this.tailscaleClientSecret) {
108
+ errors.push("TAILSCALE_CLIENT_SECRET is required when TAILSCALE_ENABLE=1");
109
+ }
110
+ }
111
+
112
+ return errors;
113
+ }
114
+
115
+ /**
116
+ * Get all directories that need to be created
117
+ */
118
+ getDirectoriesToEnsure() {
119
+ return [
120
+ this.runnerDataDir,
121
+ this.logsDir,
122
+ this.pidDir,
123
+ this.dataServicesDir,
124
+ this.tmpDir,
125
+ ];
126
+ }
127
+ }
128
+
129
+ module.exports = Config;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * constants.js
3
+ * Định nghĩa các hằng số dùng chung
4
+ */
5
+
6
+ module.exports = {
7
+ // Exit codes
8
+ EXIT_SUCCESS: 0,
9
+ EXIT_UNKNOWN: 1,
10
+ EXIT_VALIDATION: 2,
11
+ EXIT_NETWORK: 10,
12
+ EXIT_PROCESS: 20,
13
+
14
+ // Directories
15
+ RUNNER_DATA_DIR: ".runner-data",
16
+ LOGS_DIR: "logs",
17
+ PID_DIR: "pid",
18
+ DATA_SERVICES_DIR: "data-services",
19
+ TMP_DIR: "tmp",
20
+
21
+ // Tailscale
22
+ DEFAULT_TAG: "tag:ci",
23
+ CONNECTION_TIMEOUT: 30000,
24
+ STATUS_CHECK_INTERVAL: 2000,
25
+
26
+ // Sync
27
+ RSYNC_TIMEOUT: 300000, // 5 minutes
28
+ SSH_TIMEOUT: 60000, // 1 minute
29
+
30
+ // Git
31
+ GIT_RETRY_COUNT: 3,
32
+ GIT_RETRY_DELAY: 2000,
33
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * errors.js
3
+ * Custom error classes với exit codes
4
+ */
5
+
6
+ class BaseError extends Error {
7
+ constructor(message, exitCode = 1) {
8
+ super(message);
9
+ this.name = this.constructor.name;
10
+ this.exitCode = exitCode;
11
+ Error.captureStackTrace(this, this.constructor);
12
+ }
13
+ }
14
+
15
+ class ValidationError extends BaseError {
16
+ constructor(message) {
17
+ super(message, 2);
18
+ }
19
+ }
20
+
21
+ class NetworkError extends BaseError {
22
+ constructor(message) {
23
+ super(message, 10);
24
+ }
25
+ }
26
+
27
+ class ProcessError extends BaseError {
28
+ constructor(message) {
29
+ super(message, 20);
30
+ }
31
+ }
32
+
33
+ class SyncError extends BaseError {
34
+ constructor(message) {
35
+ super(message, 20);
36
+ }
37
+ }
38
+
39
+ module.exports = {
40
+ BaseError,
41
+ ValidationError,
42
+ NetworkError,
43
+ ProcessError,
44
+ SyncError,
45
+ };
@@ -0,0 +1,154 @@
1
+ /**
2
+ * logger.js
3
+ * Logger với masking sensitive data và version info
4
+ */
5
+
6
+ const path = require("path");
7
+ const { getTimestamp } = require("./time");
8
+
9
+ class Logger {
10
+ constructor(options = {}) {
11
+ this.packageName = options.packageName || "runner-tailscale-sync";
12
+ this.version = options.version || "unknown";
13
+ this.verbose = options.verbose || false;
14
+ this.quiet = options.quiet || false;
15
+ this.command = options.command || "";
16
+
17
+ // Danh sách giá trị phổ biến KHÔNG mask
18
+ this.skipValues = new Set([
19
+ "true", "false", "TRUE", "FALSE",
20
+ "null", "undefined", "NULL",
21
+ "production", "development", "test", "staging"
22
+ ]);
23
+
24
+ // Danh sách key patterns cần mask
25
+ this.sensitivePatterns = [
26
+ "PASSWORD", "SECRET", "KEY", "TOKEN", "API",
27
+ "CLIENT_ID", "CLIENT_SECRET", "AUTH", "OAUTH",
28
+ "PRIVATE", "CREDENTIAL", "ACCESS", "PASSPHRASE",
29
+ ];
30
+ }
31
+
32
+ /**
33
+ * Mask sensitive values trong message
34
+ */
35
+ maskSensitiveData(msg) {
36
+ let maskedMsg = msg;
37
+
38
+ const envValues = Object.entries(process.env)
39
+ .filter(([key, value]) => {
40
+ if (!value || typeof value !== "string") return false;
41
+ const trimmed = value.trim();
42
+
43
+ if (trimmed.length < 6) return false;
44
+ if (this.skipValues.has(trimmed)) return false;
45
+ if (/^\d+$/.test(trimmed)) return false;
46
+
47
+ const upperKey = key.toUpperCase();
48
+ return this.sensitivePatterns.some(pattern => upperKey.includes(pattern));
49
+ })
50
+ .map(([key, value]) => value.trim().replace(/\s+/g, " "))
51
+ .sort((a, b) => b.length - a.length);
52
+
53
+ const uniqueValues = [...new Set(envValues)];
54
+
55
+ for (const value of uniqueValues) {
56
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
57
+ const whitespacePattern = escapedValue.replace(/\s+/g, "\\s+");
58
+ const regex = new RegExp(whitespacePattern, "g");
59
+ maskedMsg = maskedMsg.replace(regex, "*".repeat(value.length));
60
+ }
61
+
62
+ maskedMsg = maskedMsg.replace(/tskey-[a-zA-Z0-9]{30,}/g, "***TAILSCALE_KEY***");
63
+ maskedMsg = maskedMsg.replace(/ghp_[a-zA-Z0-9]{36}/g, "***GITHUB_TOKEN***");
64
+ maskedMsg = maskedMsg.replace(/[A-Za-z0-9+/]{32,}={0,2}/g, "***BASE64_SECRET***");
65
+
66
+ return maskedMsg;
67
+ }
68
+
69
+ /**
70
+ * Format message với prefix
71
+ */
72
+ format(level, msg) {
73
+ const timestamp = getTimestamp();
74
+ const prefix = `[${this.packageName}@${this.version}]`;
75
+ const commandPrefix = `[${this.command || "unknown"}]`;
76
+ const timePrefix = `[${timestamp}]`;
77
+ return `${timePrefix} ${prefix} ${commandPrefix} ${level} ${msg}`;
78
+ }
79
+
80
+ /**
81
+ * Log info
82
+ */
83
+ info(msg) {
84
+ if (this.quiet) return;
85
+ const formatted = this.format("ℹ️", msg);
86
+ const masked = this.maskSensitiveData(formatted);
87
+ process.stdout.write(masked + "\n");
88
+ }
89
+
90
+ /**
91
+ * Log success
92
+ */
93
+ success(msg) {
94
+ if (this.quiet) return;
95
+ const formatted = this.format("✅", msg);
96
+ const masked = this.maskSensitiveData(formatted);
97
+ process.stdout.write(masked + "\n");
98
+ }
99
+
100
+ /**
101
+ * Log warning
102
+ */
103
+ warn(msg) {
104
+ const formatted = this.format("⚠️", msg);
105
+ const masked = this.maskSensitiveData(formatted);
106
+ process.stderr.write(masked + "\n");
107
+ }
108
+
109
+ /**
110
+ * Log error
111
+ */
112
+ error(msg) {
113
+ const formatted = this.format("❌", msg);
114
+ const masked = this.maskSensitiveData(formatted);
115
+ process.stderr.write(masked + "\n");
116
+ }
117
+
118
+ /**
119
+ * Log debug (chỉ khi verbose)
120
+ */
121
+ debug(msg) {
122
+ if (!this.verbose) return;
123
+ const formatted = this.format("🔍", msg);
124
+ const masked = this.maskSensitiveData(formatted);
125
+ process.stdout.write(masked + "\n");
126
+ }
127
+
128
+ /**
129
+ * Log command execution
130
+ */
131
+ command(cmd) {
132
+ if (this.quiet) return;
133
+ const formatted = this.format("🔧", cmd);
134
+ const masked = this.maskSensitiveData(formatted);
135
+ process.stdout.write(masked + "\n");
136
+ }
137
+
138
+ /**
139
+ * Print banner khi khởi động
140
+ */
141
+ printBanner() {
142
+ if (this.quiet) return;
143
+ this.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
144
+ this.info(`📦 ${this.packageName} - version ${this.version}`);
145
+ this.info(`🧾 Đang thực thi version: ${this.version}`);
146
+ if (this.command) {
147
+ this.info(`🎯 Command: ${this.command}`);
148
+ }
149
+ this.info(`🕐 Started at: ${getTimestamp()} (VN Time)`);
150
+ this.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
151
+ }
152
+ }
153
+
154
+ module.exports = Logger;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * time.js
3
+ * Vietnam timezone utilities (Asia/Ho_Chi_Minh)
4
+ */
5
+
6
+ const VN_OFFSET = 7 * 60; // UTC+7 in minutes
7
+
8
+ /**
9
+ * Lấy thời gian Việt Nam hiện tại
10
+ * @returns {Date}
11
+ */
12
+ function getVietnamTime() {
13
+ const now = new Date();
14
+ const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
15
+ return new Date(utc + (VN_OFFSET * 60000));
16
+ }
17
+
18
+ /**
19
+ * Format thời gian theo format cụ thể
20
+ * @param {Date} date
21
+ * @param {string} format - 'yyMMdd', 'HHmm', 'yyMMdd-HHmmss', etc
22
+ * @returns {string}
23
+ */
24
+ function formatVietnamTime(date, format = 'yyMMdd-HHmmss') {
25
+ const yy = String(date.getFullYear()).slice(-2);
26
+ const MM = String(date.getMonth() + 1).padStart(2, '0');
27
+ const dd = String(date.getDate()).padStart(2, '0');
28
+ const HH = String(date.getHours()).padStart(2, '0');
29
+ const mm = String(date.getMinutes()).padStart(2, '0');
30
+ const ss = String(date.getSeconds()).padStart(2, '0');
31
+
32
+ return format
33
+ .replace('yy', yy)
34
+ .replace('MM', MM)
35
+ .replace('dd', dd)
36
+ .replace('HH', HH)
37
+ .replace('mm', mm)
38
+ .replace('ss', ss);
39
+ }
40
+
41
+ /**
42
+ * Tạo version string theo format: 1.yyMMdd.1HHmm
43
+ * @returns {string}
44
+ */
45
+ function generateVersion() {
46
+ const vnTime = getVietnamTime();
47
+ const yyMMdd = formatVietnamTime(vnTime, 'yyMMdd');
48
+ const HHmm = formatVietnamTime(vnTime, 'HHmm');
49
+ return `1.${yyMMdd}.1${HHmm}`;
50
+ }
51
+
52
+ /**
53
+ * Tạo timestamp string cho logs
54
+ * @returns {string}
55
+ */
56
+ function getTimestamp() {
57
+ return formatVietnamTime(getVietnamTime(), 'yyMMdd-HHmmss');
58
+ }
59
+
60
+ module.exports = {
61
+ getVietnamTime,
62
+ formatVietnamTime,
63
+ generateVersion,
64
+ getTimestamp,
65
+ };