@overscore/cli 0.13.0 → 0.13.1
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/dist/index.js +82 -9
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,11 +3,19 @@ import fs from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import http from "http";
|
|
5
5
|
import net from "net";
|
|
6
|
+
import os from "os";
|
|
6
7
|
import { execSync } from "child_process";
|
|
7
8
|
import { createInterface } from "readline";
|
|
8
9
|
import { createHash, randomBytes } from "crypto";
|
|
9
10
|
import yauzl from "yauzl";
|
|
10
11
|
// ── Shared helpers ──────────────────────────────────────────────────
|
|
12
|
+
// Name CLI-minted API keys after the machine + date so stale keys are
|
|
13
|
+
// recognizable and revocable in the Hub UI (mitigates key sprawl). Keys are
|
|
14
|
+
// project-scoped; revoke unused ones under Project Settings → API Keys.
|
|
15
|
+
function cliKeyName() {
|
|
16
|
+
const host = (os.hostname() || "unknown").split(".")[0];
|
|
17
|
+
return `cli ${host} ${new Date().toISOString().slice(0, 10)}`;
|
|
18
|
+
}
|
|
11
19
|
function parseEnv(content) {
|
|
12
20
|
const env = {};
|
|
13
21
|
for (const line of content.split("\n")) {
|
|
@@ -187,7 +195,7 @@ async function refreshApiKey(projectSlug) {
|
|
|
187
195
|
const keyRes = await fetch(`${HUB_URL}/api/projects/${projectSlug}/api-keys`, {
|
|
188
196
|
method: "POST",
|
|
189
197
|
headers: { Authorization: `Bearer ${deviceToken}`, "Content-Type": "application/json" },
|
|
190
|
-
body: JSON.stringify({ name:
|
|
198
|
+
body: JSON.stringify({ name: cliKeyName() }),
|
|
191
199
|
});
|
|
192
200
|
if (keyRes.ok) {
|
|
193
201
|
const data = (await keyRes.json());
|
|
@@ -239,27 +247,85 @@ const SECRET_FILE_EXTENSIONS = new Set([
|
|
|
239
247
|
".pfx",
|
|
240
248
|
".jks",
|
|
241
249
|
".keystore",
|
|
250
|
+
".p8",
|
|
251
|
+
".ppk",
|
|
252
|
+
".gpg",
|
|
253
|
+
".asc",
|
|
254
|
+
".tfstate", // Terraform state — often contains plaintext secrets
|
|
255
|
+
".tfvars",
|
|
256
|
+
]);
|
|
257
|
+
// Exact filenames that are secrets regardless of (or lacking) an extension.
|
|
258
|
+
const SECRET_FILE_NAMES = new Set([
|
|
259
|
+
"service-account.json",
|
|
260
|
+
"service_account.json",
|
|
261
|
+
"serviceaccount.json",
|
|
262
|
+
"gcp-credentials.json",
|
|
263
|
+
"credentials.json",
|
|
264
|
+
"id_rsa",
|
|
265
|
+
"id_dsa",
|
|
266
|
+
"id_ecdsa",
|
|
267
|
+
"id_ed25519",
|
|
268
|
+
".netrc",
|
|
269
|
+
".pgpass",
|
|
270
|
+
".htpasswd",
|
|
271
|
+
"secrets.json",
|
|
272
|
+
"secrets.yaml",
|
|
273
|
+
"secrets.yml",
|
|
242
274
|
]);
|
|
243
|
-
function
|
|
275
|
+
function isSecretFileName(name) {
|
|
276
|
+
const lower = name.toLowerCase();
|
|
277
|
+
if (SECRET_FILE_NAMES.has(lower))
|
|
278
|
+
return true;
|
|
279
|
+
const ext = lower.substring(lower.lastIndexOf("."));
|
|
280
|
+
if (SECRET_FILE_EXTENSIONS.has(ext))
|
|
281
|
+
return true;
|
|
282
|
+
// Terraform state backups: terraform.tfstate.backup, *.tfstate.*
|
|
283
|
+
if (lower.includes(".tfstate"))
|
|
284
|
+
return true;
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Walk a directory collecting deployable source files. Excludes machine-state
|
|
289
|
+
* and secrets, NEVER follows symlinks (a symlink could point at ~/.ssh/id_rsa
|
|
290
|
+
* or outside the project), and honors .overscoreignore at the project root.
|
|
291
|
+
*/
|
|
292
|
+
function collectSourceFiles(dir, prefix, ctx) {
|
|
293
|
+
// Initialize ignore context on the first (root) call.
|
|
294
|
+
const c = ctx ?? { patterns: loadOverscoreIgnore(dir), skipped: [] };
|
|
244
295
|
const files = [];
|
|
245
296
|
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
297
|
+
// Never traverse or upload symlinks — they can escape the project tree
|
|
298
|
+
// or point at credentials. Skip silently-but-noted.
|
|
299
|
+
if (entry.isSymbolicLink()) {
|
|
300
|
+
c.skipped.push((prefix ? `${prefix}/` : "") + entry.name + " (symlink)");
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
246
303
|
if (SOURCE_EXCLUDE.has(entry.name))
|
|
247
304
|
continue;
|
|
248
305
|
if (entry.name.startsWith(".env"))
|
|
249
306
|
continue; // .env, .env.local, .env.production, etc.
|
|
250
307
|
if (entry.name.endsWith(".log"))
|
|
251
308
|
continue;
|
|
252
|
-
|
|
253
|
-
|
|
309
|
+
if (isSecretFileName(entry.name)) {
|
|
310
|
+
c.skipped.push((prefix ? `${prefix}/` : "") + entry.name + " (secret)");
|
|
254
311
|
continue;
|
|
312
|
+
}
|
|
255
313
|
const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
314
|
+
if (isIgnored(relativePath, entry.isDirectory(), c.patterns))
|
|
315
|
+
continue;
|
|
256
316
|
if (entry.isDirectory()) {
|
|
257
|
-
files.push(...collectSourceFiles(path.join(dir, entry.name), relativePath));
|
|
317
|
+
files.push(...collectSourceFiles(path.join(dir, entry.name), relativePath, c));
|
|
258
318
|
}
|
|
259
319
|
else {
|
|
260
320
|
files.push(relativePath);
|
|
261
321
|
}
|
|
262
322
|
}
|
|
323
|
+
// Surface what was withheld so a deploy is never silently lossy on secrets.
|
|
324
|
+
if (!ctx && c.skipped.length > 0) {
|
|
325
|
+
console.log(` Skipped ${c.skipped.length} sensitive/symlinked file(s): ${c.skipped
|
|
326
|
+
.slice(0, 10)
|
|
327
|
+
.join(", ")}${c.skipped.length > 10 ? ", …" : ""}`);
|
|
328
|
+
}
|
|
263
329
|
return files;
|
|
264
330
|
}
|
|
265
331
|
function extractZip(zipBuffer, targetDir) {
|
|
@@ -325,9 +391,16 @@ async function deploy() {
|
|
|
325
391
|
}
|
|
326
392
|
const builtFiles = collectFiles(distDir, "");
|
|
327
393
|
console.log(` Found ${builtFiles.length} built files.`);
|
|
328
|
-
// Collect source files
|
|
329
|
-
|
|
330
|
-
|
|
394
|
+
// Collect source files (skipped entirely with --no-source). Source is saved
|
|
395
|
+
// for versioning/restore; symlinks and secrets are always excluded.
|
|
396
|
+
const noSource = process.argv.includes("--no-source");
|
|
397
|
+
const sourceFiles = noSource ? [] : collectSourceFiles(process.cwd(), "");
|
|
398
|
+
if (noSource) {
|
|
399
|
+
console.log(" Skipping source upload (--no-source).");
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
console.log(` Found ${sourceFiles.length} source files.`);
|
|
403
|
+
}
|
|
331
404
|
// Upload via deploy API
|
|
332
405
|
console.log(" Uploading...");
|
|
333
406
|
const formData = new FormData();
|
|
@@ -1063,7 +1136,7 @@ async function loadAnalysisConfig() {
|
|
|
1063
1136
|
Authorization: `Bearer ${deviceToken}`,
|
|
1064
1137
|
"Content-Type": "application/json",
|
|
1065
1138
|
},
|
|
1066
|
-
body: JSON.stringify({ name:
|
|
1139
|
+
body: JSON.stringify({ name: cliKeyName() }),
|
|
1067
1140
|
});
|
|
1068
1141
|
if (keyGenRes.ok) {
|
|
1069
1142
|
const data = (await keyGenRes.json());
|