@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.
Files changed (2) hide show
  1. package/dist/index.js +82 -9
  2. 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: `cli-${Date.now()}` }),
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 collectSourceFiles(dir, prefix) {
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
- const ext = entry.name.substring(entry.name.lastIndexOf(".")).toLowerCase();
253
- if (SECRET_FILE_EXTENSIONS.has(ext))
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
- const sourceFiles = collectSourceFiles(process.cwd(), "");
330
- console.log(` Found ${sourceFiles.length} source files.`);
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: `cli-${Date.now()}` }),
1139
+ body: JSON.stringify({ name: cliKeyName() }),
1067
1140
  });
1068
1141
  if (keyGenRes.ok) {
1069
1142
  const data = (await keyGenRes.json());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@overscore/cli",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "CLI for deploying Overscore dashboards and publishing analyses",
5
5
  "bin": {
6
6
  "overscore": "dist/index.js"