afront 1.0.24 → 1.0.25
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.
- package/.babelrc +13 -13
- package/.env +1 -1
- package/LICENSE +21 -21
- package/README.md +94 -94
- package/build-prod/index.html +1 -1
- package/build-prod/manifest.json +25 -25
- package/build-prod/offline.html +1023 -1023
- package/build-prod/robots.txt +3 -3
- package/build-prod-static/index.html +1 -1
- package/build-prod-static/manifest.json +25 -25
- package/build-prod-static/offline.html +1023 -1023
- package/build-prod-static/robots.txt +3 -3
- package/install.js +415 -415
- package/package.json +102 -96
- package/server.js +40 -40
- package/src/ARoutes/AFRoutes.js +28 -28
- package/src/Api/api.config.js +266 -266
- package/src/Api/login.service.js +44 -44
- package/src/App.js +28 -28
- package/src/Components/Background/MeshGradient.js +18 -18
- package/src/Components/Footer/Footer.js +108 -108
- package/src/Components/Header/Header.js +149 -149
- package/src/Components/Loading/LoadingIndicator.js +12 -12
- package/src/Components/Loading/LoadingIndicator.module.css +34 -34
- package/src/Components/Loading/LoadingSpinner.js +27 -27
- package/src/Components/Loading/LoadingSpinner.module.css +100 -100
- package/src/Components/RequireAuth.js +29 -29
- package/src/LoadingFallback.js +13 -13
- package/src/PageNotFound.js +19 -19
- package/src/Pages/Home.js +50 -50
- package/src/Pages/Signup.js +230 -230
- package/src/Pages/Support.js +68 -68
- package/src/Routes/ARoutes.js +66 -66
- package/src/Routes/ARoutesStatic.js +83 -83
- package/src/Static/appStatic.js +16 -16
- package/src/Static/indexStatic.js +13 -13
- package/src/Style/App.module.css +11 -11
- package/src/Style/MeshGradient.module.css +130 -130
- package/src/Style/PageNotFound.module.css +37 -37
- package/src/Style/Style.module.css +686 -686
- package/src/Style/Support.module.css +185 -185
- package/src/Utils/LoadingContext.js +5 -5
- package/src/index.js +25 -25
- package/webpack.build-prod.js +141 -140
- package/webpack.dev.js +127 -127
- package/webpack.prod.js +148 -147
- package/webpack.ssr.prod.js +97 -97
- package/npm-shrinkwrap.json +0 -9641
package/install.js
CHANGED
|
@@ -1,415 +1,415 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const { https } = require('follow-redirects');
|
|
5
|
-
const { exec } = require('child_process');
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const tmp = require('tmp');
|
|
8
|
-
const AdmZip = require('adm-zip');
|
|
9
|
-
const readline = require('readline');
|
|
10
|
-
|
|
11
|
-
// Configuration
|
|
12
|
-
const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.
|
|
13
|
-
|
|
14
|
-
// Define files to skip
|
|
15
|
-
const SKIP_FILES = ['FUNDING.yml', 'CODE_OF_CONDUCT.md', 'SECURITY.md', 'install.js', '.npmrc'];
|
|
16
|
-
|
|
17
|
-
// Initialize readline interface
|
|
18
|
-
const rl = readline.createInterface({
|
|
19
|
-
input: process.stdin,
|
|
20
|
-
output: process.stdout
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Spinner function
|
|
24
|
-
const spinner = (text, delay = 100) => {
|
|
25
|
-
const spinnerChars = ['|', '/', '-', '\\'];
|
|
26
|
-
let i = 0;
|
|
27
|
-
const interval = setInterval(() => {
|
|
28
|
-
readline.cursorTo(process.stdout, 0);
|
|
29
|
-
process.stdout.write(`${text} ${spinnerChars[i++]}`);
|
|
30
|
-
i = i % spinnerChars.length;
|
|
31
|
-
}, delay);
|
|
32
|
-
|
|
33
|
-
return () => {
|
|
34
|
-
clearInterval(interval);
|
|
35
|
-
readline.cursorTo(process.stdout, 0);
|
|
36
|
-
process.stdout.write(`${text} Done.\n`);
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const askQuestion = (question) => {
|
|
41
|
-
return new Promise((resolve) => {
|
|
42
|
-
rl.question(question, (answer) => {
|
|
43
|
-
resolve(answer.trim().toLowerCase());
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
const downloadFile = (url, destination) => {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const file = fs.createWriteStream(destination);
|
|
51
|
-
const stopSpinner = spinner('Downloading');
|
|
52
|
-
https.get(url, (response) => {
|
|
53
|
-
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
54
|
-
return downloadFile(response.headers.location, destination).then(resolve).catch(reject);
|
|
55
|
-
}
|
|
56
|
-
if (response.statusCode !== 200) {
|
|
57
|
-
reject(new Error(`Failed to download file. Status code: ${response.statusCode}`));
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
response.pipe(file);
|
|
61
|
-
file.on('finish', () => {
|
|
62
|
-
file.close(() => {
|
|
63
|
-
if (fs.statSync(destination).size > 0) {
|
|
64
|
-
stopSpinner();
|
|
65
|
-
resolve();
|
|
66
|
-
} else {
|
|
67
|
-
stopSpinner();
|
|
68
|
-
reject(new Error('Downloaded file is empty.'));
|
|
69
|
-
}
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
}).on('error', (err) => {
|
|
73
|
-
// attempt to remove partial download only if it's inside allowed temp locations
|
|
74
|
-
safeUnlink(destination).finally(() => reject(err));
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const extractZip = (zipPath, extractTo) => {
|
|
80
|
-
return new Promise((resolve, reject) => {
|
|
81
|
-
const stopSpinner = spinner('Extracting');
|
|
82
|
-
try {
|
|
83
|
-
const zip = new AdmZip(zipPath);
|
|
84
|
-
zip.extractAllTo(extractTo, true);
|
|
85
|
-
fs.readdir(extractTo, (err, files) => {
|
|
86
|
-
if (err) {
|
|
87
|
-
stopSpinner();
|
|
88
|
-
console.error('Error reading extracted folder:', err);
|
|
89
|
-
reject(err);
|
|
90
|
-
} else {
|
|
91
|
-
stopSpinner();
|
|
92
|
-
resolve();
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
} catch (err) {
|
|
96
|
-
stopSpinner();
|
|
97
|
-
console.error('Error extracting zip file:', err);
|
|
98
|
-
reject(err);
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const runNpmInstall = (directory) => {
|
|
104
|
-
return new Promise((resolve, reject) => {
|
|
105
|
-
const stopSpinner = spinner('Running npm install');
|
|
106
|
-
exec('npm install --legacy-peer-deps --no-audit --no-fund', { cwd: directory }, (err, stdout, stderr) => {
|
|
107
|
-
if (err) {
|
|
108
|
-
stopSpinner();
|
|
109
|
-
console.error('Error running npm install:', stderr);
|
|
110
|
-
reject(err);
|
|
111
|
-
} else {
|
|
112
|
-
stopSpinner();
|
|
113
|
-
console.log('npm install output:', stdout);
|
|
114
|
-
resolve();
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const createDirIfNotExists = (dirPath) => {
|
|
121
|
-
return new Promise((resolve, reject) => {
|
|
122
|
-
fs.mkdir(dirPath, { recursive: true }, (err) => {
|
|
123
|
-
if (err) {
|
|
124
|
-
reject(new Error('Error creating directory:', err));
|
|
125
|
-
} else {
|
|
126
|
-
resolve();
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
});
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
const isPathInside = (root, target) => {
|
|
133
|
-
const r = path.resolve(root);
|
|
134
|
-
const t = path.resolve(target);
|
|
135
|
-
return t === r || t.startsWith(r + path.sep);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// Security: path validated against SAFE_UNLINK_ROOT before unlink
|
|
140
|
-
const SAFE_UNLINK_ROOT = path.resolve(process.cwd());
|
|
141
|
-
|
|
142
|
-
const safeUnlink = async (targetPath) => {
|
|
143
|
-
const resolved = path.resolve(targetPath);
|
|
144
|
-
|
|
145
|
-
if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
|
|
146
|
-
throw new Error(`Blocked unlink outside safe root: ${resolved}`);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const stats = await fs.promises.lstat(resolved);
|
|
150
|
-
|
|
151
|
-
if (!stats.isFile()) {
|
|
152
|
-
throw new Error(`Refusing to unlink non-file: ${resolved}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
await fs.promises.unlink(resolved);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const safeRm = (dirPath, cb) => {
|
|
160
|
-
const resolved = path.resolve(dirPath);
|
|
161
|
-
if (!isPathInside(process.cwd(), resolved)) {
|
|
162
|
-
return cb(new Error(`Refusing to remove directory outside current working directory: ${resolved}`));
|
|
163
|
-
}
|
|
164
|
-
fs.rm(resolved, { recursive: true, force: true }, cb);
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
// Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
|
|
168
|
-
const safeMoveFile = async (resolvedSrc, resolvedDest) => {
|
|
169
|
-
const srcFlags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
|
|
170
|
-
let srcHandle;
|
|
171
|
-
let destHandle;
|
|
172
|
-
const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
|
|
173
|
-
try {
|
|
174
|
-
srcHandle = await fs.promises.open(resolvedSrc, srcFlags);
|
|
175
|
-
} catch (err) {
|
|
176
|
-
throw err;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
try {
|
|
180
|
-
const stats = await srcHandle.stat();
|
|
181
|
-
if (stats.isDirectory()) {
|
|
182
|
-
await srcHandle.close();
|
|
183
|
-
throw new Error('Source is a directory');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
await createDirIfNotExists(path.dirname(resolvedDest));
|
|
187
|
-
|
|
188
|
-
destHandle = await fs.promises.open(tmpName, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, stats.mode);
|
|
189
|
-
|
|
190
|
-
const bufferSize = 64 * 1024;
|
|
191
|
-
const buffer = Buffer.allocUnsafe(bufferSize);
|
|
192
|
-
let position = 0;
|
|
193
|
-
while (true) {
|
|
194
|
-
const { bytesRead } = await srcHandle.read(buffer, 0, bufferSize, position);
|
|
195
|
-
if (!bytesRead) break;
|
|
196
|
-
let written = 0;
|
|
197
|
-
while (written < bytesRead) {
|
|
198
|
-
const { bytesWritten } = await destHandle.write(buffer, written, bytesRead - written);
|
|
199
|
-
written += bytesWritten;
|
|
200
|
-
}
|
|
201
|
-
position += bytesRead;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
await destHandle.sync();
|
|
205
|
-
await destHandle.close();
|
|
206
|
-
await srcHandle.close();
|
|
207
|
-
|
|
208
|
-
await fs.promises.rename(tmpName, resolvedDest);
|
|
209
|
-
} catch (err) {
|
|
210
|
-
try {
|
|
211
|
-
if (destHandle) await destHandle.close();
|
|
212
|
-
} catch (e) {}
|
|
213
|
-
try {
|
|
214
|
-
await safeUnlink(tmpName, [path.dirname(resolvedDest)]).catch(() => {});
|
|
215
|
-
} catch (e) {}
|
|
216
|
-
try {
|
|
217
|
-
if (srcHandle) await srcHandle.close();
|
|
218
|
-
} catch (e) {}
|
|
219
|
-
throw err;
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
const promptForFolderName = async () => {
|
|
224
|
-
const answer = await askQuestion('AFront: Enter the name of the destination folder: ');
|
|
225
|
-
return answer;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
const promptForReplace = async (dirPath) => {
|
|
229
|
-
const answer = await askQuestion(`The directory ${dirPath} already exists. Do you want to replace it? (yes/no): `);
|
|
230
|
-
return answer === 'yes' || answer === 'y';
|
|
231
|
-
};
|
|
232
|
-
|
|
233
|
-
const removeDir = (dirPath) => {
|
|
234
|
-
return new Promise((resolve, reject) => {
|
|
235
|
-
console.log(`Removing existing directory: ${dirPath}`);
|
|
236
|
-
safeRm(dirPath, (err) => {
|
|
237
|
-
if (err) return reject(err);
|
|
238
|
-
resolve();
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
};
|
|
242
|
-
|
|
243
|
-
const SAFE_READDIR_ROOT = path.resolve(os.tmpdir());
|
|
244
|
-
|
|
245
|
-
const safeReaddir = (dirPath, cb) => {
|
|
246
|
-
const resolved = path.resolve(dirPath);
|
|
247
|
-
|
|
248
|
-
if (!resolved.startsWith(SAFE_READDIR_ROOT + path.sep)) {
|
|
249
|
-
return cb(new Error(`Blocked readdir outside safe root: ${resolved}`));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
fs.lstat(resolved, (err, stats) => {
|
|
253
|
-
if (err) return cb(err);
|
|
254
|
-
if (!stats.isDirectory()) {
|
|
255
|
-
return cb(new Error(`Not a directory: ${resolved}`));
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
fs.readdir(resolved, cb);
|
|
259
|
-
});
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const moveFiles = (srcPath, destPath) => {
|
|
264
|
-
return new Promise((resolve, reject) => {
|
|
265
|
-
safeReaddir(srcPath, (err, files) => {
|
|
266
|
-
if (err) {
|
|
267
|
-
return reject(err);
|
|
268
|
-
}
|
|
269
|
-
let pending = files.length;
|
|
270
|
-
if (!pending) return resolve();
|
|
271
|
-
files.forEach((file) => {
|
|
272
|
-
// Normalize and validate file name to avoid path traversal
|
|
273
|
-
const fileName = path.basename(file);
|
|
274
|
-
if (SKIP_FILES.includes(fileName)) {
|
|
275
|
-
if (!--pending) resolve();
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
const srcRoot = path.resolve(srcPath);
|
|
280
|
-
const destRoot = path.resolve(destPath);
|
|
281
|
-
const resolvedSrc = path.resolve(srcPath, file);
|
|
282
|
-
const resolvedDest = path.resolve(destPath, file);
|
|
283
|
-
|
|
284
|
-
// Ensure resolved paths are inside their respective roots
|
|
285
|
-
if (
|
|
286
|
-
!(resolvedSrc === srcRoot || resolvedSrc.startsWith(srcRoot + path.sep)) ||
|
|
287
|
-
!(resolvedDest === destRoot || resolvedDest.startsWith(destRoot + path.sep))
|
|
288
|
-
) {
|
|
289
|
-
console.warn(`Skipping suspicious path: ${file}`);
|
|
290
|
-
if (!--pending) resolve();
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
// Use lstat to detect symlinks and refuse to follow them
|
|
295
|
-
fs.lstat(resolvedSrc, (err, stats) => {
|
|
296
|
-
if (err) {
|
|
297
|
-
return reject(err);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (stats.isSymbolicLink()) {
|
|
301
|
-
console.warn(`Skipping symbolic link for safety: ${resolvedSrc}`);
|
|
302
|
-
if (!--pending) resolve();
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (stats.isDirectory()) {
|
|
307
|
-
createDirIfNotExists(resolvedDest)
|
|
308
|
-
.then(() => moveFiles(resolvedSrc, resolvedDest))
|
|
309
|
-
.then(() => {
|
|
310
|
-
if (!--pending) resolve();
|
|
311
|
-
})
|
|
312
|
-
.catch(reject);
|
|
313
|
-
} else {
|
|
314
|
-
// Use atomic-safe move: copy from a non-following FD to temp file, then rename
|
|
315
|
-
createDirIfNotExists(path.dirname(resolvedDest))
|
|
316
|
-
.then(() => {
|
|
317
|
-
safeMoveFile(resolvedSrc, resolvedDest)
|
|
318
|
-
.then(() => {
|
|
319
|
-
if (!--pending) resolve();
|
|
320
|
-
})
|
|
321
|
-
.catch(reject);
|
|
322
|
-
})
|
|
323
|
-
.catch(reject);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
};
|
|
330
|
-
|
|
331
|
-
const main = async () => {
|
|
332
|
-
try {
|
|
333
|
-
const tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
|
334
|
-
const zipPath = path.join(tmpDir.name, 'archive.zip');
|
|
335
|
-
|
|
336
|
-
let folderName = process.argv[2];
|
|
337
|
-
if (folderName === '.') {
|
|
338
|
-
folderName = path.basename(process.cwd());
|
|
339
|
-
} else if (!folderName) {
|
|
340
|
-
folderName = await promptForFolderName();
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// Sanitize the provided folder name to prevent path traversal or absolute paths
|
|
344
|
-
const sanitizeFolderName = (name) => {
|
|
345
|
-
if (!name) return '';
|
|
346
|
-
// If user provided '.', use current dir basename
|
|
347
|
-
if (name === '.') return path.basename(process.cwd());
|
|
348
|
-
// Disallow absolute paths
|
|
349
|
-
if (path.isAbsolute(name)) {
|
|
350
|
-
console.warn('Absolute paths are not allowed for destination; using basename.');
|
|
351
|
-
name = path.basename(name);
|
|
352
|
-
}
|
|
353
|
-
// Disallow parent traversal and nested paths
|
|
354
|
-
if (name === '..' || name.includes(path.sep) || name.includes('..')) {
|
|
355
|
-
console.warn('Path traversal detected in destination name; using basename of input.');
|
|
356
|
-
name = path.basename(name);
|
|
357
|
-
}
|
|
358
|
-
// Finally, ensure it's a single path segment
|
|
359
|
-
name = path.basename(name);
|
|
360
|
-
if (!name) throw new Error('Invalid destination folder name');
|
|
361
|
-
return name;
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
folderName = sanitizeFolderName(folderName);
|
|
365
|
-
|
|
366
|
-
const destDir = path.join(process.cwd(), folderName);
|
|
367
|
-
|
|
368
|
-
if (fs.existsSync(destDir)) {
|
|
369
|
-
const replace = await promptForReplace(destDir);
|
|
370
|
-
if (replace) {
|
|
371
|
-
await removeDir(destDir);
|
|
372
|
-
await createDirIfNotExists(destDir);
|
|
373
|
-
} else {
|
|
374
|
-
console.log('Operation aborted.');
|
|
375
|
-
rl.close();
|
|
376
|
-
process.exit(0);
|
|
377
|
-
}
|
|
378
|
-
} else {
|
|
379
|
-
await createDirIfNotExists(destDir);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
await downloadFile(GITHUB_ZIP_URL, zipPath);
|
|
383
|
-
console.log('Downloaded successfully.');
|
|
384
|
-
|
|
385
|
-
await extractZip(zipPath, tmpDir.name);
|
|
386
|
-
|
|
387
|
-
const extractedFolderName = fs.readdirSync(tmpDir.name)[0];
|
|
388
|
-
const extractedFolderPath = path.join(tmpDir.name, extractedFolderName);
|
|
389
|
-
|
|
390
|
-
fs.readdirSync(extractedFolderPath);
|
|
391
|
-
|
|
392
|
-
await moveFiles(extractedFolderPath, destDir);
|
|
393
|
-
|
|
394
|
-
// Remove any bundled lockfiles from the extracted archive to avoid integrity
|
|
395
|
-
// checksum mismatches originating from the release zip's lockfile.
|
|
396
|
-
try {
|
|
397
|
-
await safeUnlink(path.join(destDir, 'package-lock.json')).catch(() => {});
|
|
398
|
-
await safeUnlink(path.join(destDir, 'npm-shrinkwrap.json')).catch(() => {});
|
|
399
|
-
} catch (e) {
|
|
400
|
-
// ignore
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
await runNpmInstall(destDir);
|
|
404
|
-
|
|
405
|
-
rl.close();
|
|
406
|
-
|
|
407
|
-
process.exit(0);
|
|
408
|
-
} catch (err) {
|
|
409
|
-
console.error('Error:', err);
|
|
410
|
-
rl.close();
|
|
411
|
-
process.exit(1);
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
main();
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { https } = require('follow-redirects');
|
|
5
|
+
const { exec } = require('child_process');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const tmp = require('tmp');
|
|
8
|
+
const AdmZip = require('adm-zip');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
// Configuration
|
|
12
|
+
const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.25.zip'; // Updated URL
|
|
13
|
+
|
|
14
|
+
// Define files to skip
|
|
15
|
+
const SKIP_FILES = ['FUNDING.yml', 'CODE_OF_CONDUCT.md', 'SECURITY.md', 'install.js', '.npmrc'];
|
|
16
|
+
|
|
17
|
+
// Initialize readline interface
|
|
18
|
+
const rl = readline.createInterface({
|
|
19
|
+
input: process.stdin,
|
|
20
|
+
output: process.stdout
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Spinner function
|
|
24
|
+
const spinner = (text, delay = 100) => {
|
|
25
|
+
const spinnerChars = ['|', '/', '-', '\\'];
|
|
26
|
+
let i = 0;
|
|
27
|
+
const interval = setInterval(() => {
|
|
28
|
+
readline.cursorTo(process.stdout, 0);
|
|
29
|
+
process.stdout.write(`${text} ${spinnerChars[i++]}`);
|
|
30
|
+
i = i % spinnerChars.length;
|
|
31
|
+
}, delay);
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
clearInterval(interval);
|
|
35
|
+
readline.cursorTo(process.stdout, 0);
|
|
36
|
+
process.stdout.write(`${text} Done.\n`);
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const askQuestion = (question) => {
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
rl.question(question, (answer) => {
|
|
43
|
+
resolve(answer.trim().toLowerCase());
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const downloadFile = (url, destination) => {
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const file = fs.createWriteStream(destination);
|
|
51
|
+
const stopSpinner = spinner('Downloading');
|
|
52
|
+
https.get(url, (response) => {
|
|
53
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
54
|
+
return downloadFile(response.headers.location, destination).then(resolve).catch(reject);
|
|
55
|
+
}
|
|
56
|
+
if (response.statusCode !== 200) {
|
|
57
|
+
reject(new Error(`Failed to download file. Status code: ${response.statusCode}`));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
response.pipe(file);
|
|
61
|
+
file.on('finish', () => {
|
|
62
|
+
file.close(() => {
|
|
63
|
+
if (fs.statSync(destination).size > 0) {
|
|
64
|
+
stopSpinner();
|
|
65
|
+
resolve();
|
|
66
|
+
} else {
|
|
67
|
+
stopSpinner();
|
|
68
|
+
reject(new Error('Downloaded file is empty.'));
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}).on('error', (err) => {
|
|
73
|
+
// attempt to remove partial download only if it's inside allowed temp locations
|
|
74
|
+
safeUnlink(destination).finally(() => reject(err));
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const extractZip = (zipPath, extractTo) => {
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const stopSpinner = spinner('Extracting');
|
|
82
|
+
try {
|
|
83
|
+
const zip = new AdmZip(zipPath);
|
|
84
|
+
zip.extractAllTo(extractTo, true);
|
|
85
|
+
fs.readdir(extractTo, (err, files) => {
|
|
86
|
+
if (err) {
|
|
87
|
+
stopSpinner();
|
|
88
|
+
console.error('Error reading extracted folder:', err);
|
|
89
|
+
reject(err);
|
|
90
|
+
} else {
|
|
91
|
+
stopSpinner();
|
|
92
|
+
resolve();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} catch (err) {
|
|
96
|
+
stopSpinner();
|
|
97
|
+
console.error('Error extracting zip file:', err);
|
|
98
|
+
reject(err);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const runNpmInstall = (directory) => {
|
|
104
|
+
return new Promise((resolve, reject) => {
|
|
105
|
+
const stopSpinner = spinner('Running npm install');
|
|
106
|
+
exec('npm install --legacy-peer-deps --no-audit --no-fund', { cwd: directory }, (err, stdout, stderr) => {
|
|
107
|
+
if (err) {
|
|
108
|
+
stopSpinner();
|
|
109
|
+
console.error('Error running npm install:', stderr);
|
|
110
|
+
reject(err);
|
|
111
|
+
} else {
|
|
112
|
+
stopSpinner();
|
|
113
|
+
console.log('npm install output:', stdout);
|
|
114
|
+
resolve();
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const createDirIfNotExists = (dirPath) => {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
fs.mkdir(dirPath, { recursive: true }, (err) => {
|
|
123
|
+
if (err) {
|
|
124
|
+
reject(new Error('Error creating directory:', err));
|
|
125
|
+
} else {
|
|
126
|
+
resolve();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const isPathInside = (root, target) => {
|
|
133
|
+
const r = path.resolve(root);
|
|
134
|
+
const t = path.resolve(target);
|
|
135
|
+
return t === r || t.startsWith(r + path.sep);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
// Security: path validated against SAFE_UNLINK_ROOT before unlink
|
|
140
|
+
const SAFE_UNLINK_ROOT = path.resolve(process.cwd());
|
|
141
|
+
|
|
142
|
+
const safeUnlink = async (targetPath) => {
|
|
143
|
+
const resolved = path.resolve(targetPath);
|
|
144
|
+
|
|
145
|
+
if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
|
|
146
|
+
throw new Error(`Blocked unlink outside safe root: ${resolved}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const stats = await fs.promises.lstat(resolved);
|
|
150
|
+
|
|
151
|
+
if (!stats.isFile()) {
|
|
152
|
+
throw new Error(`Refusing to unlink non-file: ${resolved}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
await fs.promises.unlink(resolved);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
const safeRm = (dirPath, cb) => {
|
|
160
|
+
const resolved = path.resolve(dirPath);
|
|
161
|
+
if (!isPathInside(process.cwd(), resolved)) {
|
|
162
|
+
return cb(new Error(`Refusing to remove directory outside current working directory: ${resolved}`));
|
|
163
|
+
}
|
|
164
|
+
fs.rm(resolved, { recursive: true, force: true }, cb);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
|
|
168
|
+
const safeMoveFile = async (resolvedSrc, resolvedDest) => {
|
|
169
|
+
const srcFlags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
|
|
170
|
+
let srcHandle;
|
|
171
|
+
let destHandle;
|
|
172
|
+
const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
|
|
173
|
+
try {
|
|
174
|
+
srcHandle = await fs.promises.open(resolvedSrc, srcFlags);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const stats = await srcHandle.stat();
|
|
181
|
+
if (stats.isDirectory()) {
|
|
182
|
+
await srcHandle.close();
|
|
183
|
+
throw new Error('Source is a directory');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await createDirIfNotExists(path.dirname(resolvedDest));
|
|
187
|
+
|
|
188
|
+
destHandle = await fs.promises.open(tmpName, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, stats.mode);
|
|
189
|
+
|
|
190
|
+
const bufferSize = 64 * 1024;
|
|
191
|
+
const buffer = Buffer.allocUnsafe(bufferSize);
|
|
192
|
+
let position = 0;
|
|
193
|
+
while (true) {
|
|
194
|
+
const { bytesRead } = await srcHandle.read(buffer, 0, bufferSize, position);
|
|
195
|
+
if (!bytesRead) break;
|
|
196
|
+
let written = 0;
|
|
197
|
+
while (written < bytesRead) {
|
|
198
|
+
const { bytesWritten } = await destHandle.write(buffer, written, bytesRead - written);
|
|
199
|
+
written += bytesWritten;
|
|
200
|
+
}
|
|
201
|
+
position += bytesRead;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await destHandle.sync();
|
|
205
|
+
await destHandle.close();
|
|
206
|
+
await srcHandle.close();
|
|
207
|
+
|
|
208
|
+
await fs.promises.rename(tmpName, resolvedDest);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
try {
|
|
211
|
+
if (destHandle) await destHandle.close();
|
|
212
|
+
} catch (e) {}
|
|
213
|
+
try {
|
|
214
|
+
await safeUnlink(tmpName, [path.dirname(resolvedDest)]).catch(() => {});
|
|
215
|
+
} catch (e) {}
|
|
216
|
+
try {
|
|
217
|
+
if (srcHandle) await srcHandle.close();
|
|
218
|
+
} catch (e) {}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const promptForFolderName = async () => {
|
|
224
|
+
const answer = await askQuestion('AFront: Enter the name of the destination folder: ');
|
|
225
|
+
return answer;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const promptForReplace = async (dirPath) => {
|
|
229
|
+
const answer = await askQuestion(`The directory ${dirPath} already exists. Do you want to replace it? (yes/no): `);
|
|
230
|
+
return answer === 'yes' || answer === 'y';
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const removeDir = (dirPath) => {
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
|
+
console.log(`Removing existing directory: ${dirPath}`);
|
|
236
|
+
safeRm(dirPath, (err) => {
|
|
237
|
+
if (err) return reject(err);
|
|
238
|
+
resolve();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const SAFE_READDIR_ROOT = path.resolve(os.tmpdir());
|
|
244
|
+
|
|
245
|
+
const safeReaddir = (dirPath, cb) => {
|
|
246
|
+
const resolved = path.resolve(dirPath);
|
|
247
|
+
|
|
248
|
+
if (!resolved.startsWith(SAFE_READDIR_ROOT + path.sep)) {
|
|
249
|
+
return cb(new Error(`Blocked readdir outside safe root: ${resolved}`));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
fs.lstat(resolved, (err, stats) => {
|
|
253
|
+
if (err) return cb(err);
|
|
254
|
+
if (!stats.isDirectory()) {
|
|
255
|
+
return cb(new Error(`Not a directory: ${resolved}`));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fs.readdir(resolved, cb);
|
|
259
|
+
});
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
const moveFiles = (srcPath, destPath) => {
|
|
264
|
+
return new Promise((resolve, reject) => {
|
|
265
|
+
safeReaddir(srcPath, (err, files) => {
|
|
266
|
+
if (err) {
|
|
267
|
+
return reject(err);
|
|
268
|
+
}
|
|
269
|
+
let pending = files.length;
|
|
270
|
+
if (!pending) return resolve();
|
|
271
|
+
files.forEach((file) => {
|
|
272
|
+
// Normalize and validate file name to avoid path traversal
|
|
273
|
+
const fileName = path.basename(file);
|
|
274
|
+
if (SKIP_FILES.includes(fileName)) {
|
|
275
|
+
if (!--pending) resolve();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const srcRoot = path.resolve(srcPath);
|
|
280
|
+
const destRoot = path.resolve(destPath);
|
|
281
|
+
const resolvedSrc = path.resolve(srcPath, file);
|
|
282
|
+
const resolvedDest = path.resolve(destPath, file);
|
|
283
|
+
|
|
284
|
+
// Ensure resolved paths are inside their respective roots
|
|
285
|
+
if (
|
|
286
|
+
!(resolvedSrc === srcRoot || resolvedSrc.startsWith(srcRoot + path.sep)) ||
|
|
287
|
+
!(resolvedDest === destRoot || resolvedDest.startsWith(destRoot + path.sep))
|
|
288
|
+
) {
|
|
289
|
+
console.warn(`Skipping suspicious path: ${file}`);
|
|
290
|
+
if (!--pending) resolve();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Use lstat to detect symlinks and refuse to follow them
|
|
295
|
+
fs.lstat(resolvedSrc, (err, stats) => {
|
|
296
|
+
if (err) {
|
|
297
|
+
return reject(err);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (stats.isSymbolicLink()) {
|
|
301
|
+
console.warn(`Skipping symbolic link for safety: ${resolvedSrc}`);
|
|
302
|
+
if (!--pending) resolve();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (stats.isDirectory()) {
|
|
307
|
+
createDirIfNotExists(resolvedDest)
|
|
308
|
+
.then(() => moveFiles(resolvedSrc, resolvedDest))
|
|
309
|
+
.then(() => {
|
|
310
|
+
if (!--pending) resolve();
|
|
311
|
+
})
|
|
312
|
+
.catch(reject);
|
|
313
|
+
} else {
|
|
314
|
+
// Use atomic-safe move: copy from a non-following FD to temp file, then rename
|
|
315
|
+
createDirIfNotExists(path.dirname(resolvedDest))
|
|
316
|
+
.then(() => {
|
|
317
|
+
safeMoveFile(resolvedSrc, resolvedDest)
|
|
318
|
+
.then(() => {
|
|
319
|
+
if (!--pending) resolve();
|
|
320
|
+
})
|
|
321
|
+
.catch(reject);
|
|
322
|
+
})
|
|
323
|
+
.catch(reject);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const main = async () => {
|
|
332
|
+
try {
|
|
333
|
+
const tmpDir = tmp.dirSync({ unsafeCleanup: true });
|
|
334
|
+
const zipPath = path.join(tmpDir.name, 'archive.zip');
|
|
335
|
+
|
|
336
|
+
let folderName = process.argv[2];
|
|
337
|
+
if (folderName === '.') {
|
|
338
|
+
folderName = path.basename(process.cwd());
|
|
339
|
+
} else if (!folderName) {
|
|
340
|
+
folderName = await promptForFolderName();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Sanitize the provided folder name to prevent path traversal or absolute paths
|
|
344
|
+
const sanitizeFolderName = (name) => {
|
|
345
|
+
if (!name) return '';
|
|
346
|
+
// If user provided '.', use current dir basename
|
|
347
|
+
if (name === '.') return path.basename(process.cwd());
|
|
348
|
+
// Disallow absolute paths
|
|
349
|
+
if (path.isAbsolute(name)) {
|
|
350
|
+
console.warn('Absolute paths are not allowed for destination; using basename.');
|
|
351
|
+
name = path.basename(name);
|
|
352
|
+
}
|
|
353
|
+
// Disallow parent traversal and nested paths
|
|
354
|
+
if (name === '..' || name.includes(path.sep) || name.includes('..')) {
|
|
355
|
+
console.warn('Path traversal detected in destination name; using basename of input.');
|
|
356
|
+
name = path.basename(name);
|
|
357
|
+
}
|
|
358
|
+
// Finally, ensure it's a single path segment
|
|
359
|
+
name = path.basename(name);
|
|
360
|
+
if (!name) throw new Error('Invalid destination folder name');
|
|
361
|
+
return name;
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
folderName = sanitizeFolderName(folderName);
|
|
365
|
+
|
|
366
|
+
const destDir = path.join(process.cwd(), folderName);
|
|
367
|
+
|
|
368
|
+
if (fs.existsSync(destDir)) {
|
|
369
|
+
const replace = await promptForReplace(destDir);
|
|
370
|
+
if (replace) {
|
|
371
|
+
await removeDir(destDir);
|
|
372
|
+
await createDirIfNotExists(destDir);
|
|
373
|
+
} else {
|
|
374
|
+
console.log('Operation aborted.');
|
|
375
|
+
rl.close();
|
|
376
|
+
process.exit(0);
|
|
377
|
+
}
|
|
378
|
+
} else {
|
|
379
|
+
await createDirIfNotExists(destDir);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await downloadFile(GITHUB_ZIP_URL, zipPath);
|
|
383
|
+
console.log('Downloaded successfully.');
|
|
384
|
+
|
|
385
|
+
await extractZip(zipPath, tmpDir.name);
|
|
386
|
+
|
|
387
|
+
const extractedFolderName = fs.readdirSync(tmpDir.name)[0];
|
|
388
|
+
const extractedFolderPath = path.join(tmpDir.name, extractedFolderName);
|
|
389
|
+
|
|
390
|
+
fs.readdirSync(extractedFolderPath);
|
|
391
|
+
|
|
392
|
+
await moveFiles(extractedFolderPath, destDir);
|
|
393
|
+
|
|
394
|
+
// Remove any bundled lockfiles from the extracted archive to avoid integrity
|
|
395
|
+
// checksum mismatches originating from the release zip's lockfile.
|
|
396
|
+
try {
|
|
397
|
+
await safeUnlink(path.join(destDir, 'package-lock.json')).catch(() => {});
|
|
398
|
+
await safeUnlink(path.join(destDir, 'npm-shrinkwrap.json')).catch(() => {});
|
|
399
|
+
} catch (e) {
|
|
400
|
+
// ignore
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
await runNpmInstall(destDir);
|
|
404
|
+
|
|
405
|
+
rl.close();
|
|
406
|
+
|
|
407
|
+
process.exit(0);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
console.error('Error:', err);
|
|
410
|
+
rl.close();
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
main();
|