diff --git a/.github/workflows/cli-ci.yml b/.github/workflows/cli-ci.yml index 232165e..71d6ae3 100644 --- a/.github/workflows/cli-ci.yml +++ b/.github/workflows/cli-ci.yml @@ -11,6 +11,26 @@ permissions: contents: read jobs: + secret-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout (full history) + uses: actions/checkout@v5 + with: + # Full history so gitleaks scans every commit, not just the tip. + fetch-depth: 0 + + - name: Run gitleaks + env: + # Pinned release; bump deliberately. Run the binary directly rather than + # gitleaks/gitleaks-action@v2, which requires a paid GITLEAKS_LICENSE for + # organization-owned repos. + GITLEAKS_VERSION: 8.30.1 + run: | + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | tar -xz gitleaks + ./gitleaks git . --redact --verbose --no-banner + lint-and-test: runs-on: ubuntu-latest diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..eb5cdaf --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,20 @@ +# Gitleaks configuration — extends the default ruleset. +# +# The only allowlisted secrets are the two PUBLIC Supabase anon keys committed +# in src/config/environments.ts. Those JWTs are anon-role keys, designed to be +# embedded in client code and gated by RLS (see the doc comment in that file) — +# they are intentionally not secret. +# +# They are allowlisted by EXACT VALUE, deliberately not by file path or by the +# whole `jwt` rule: a Supabase service_role key is also a JWT, so a path/rule +# allowlist would let a genuinely sensitive key pasted into the same file slip +# through. Matching exact values keeps that detection intact. +[extend] +useDefault = true + +[allowlist] +description = "Public Supabase anon keys (safe to commit, gated by RLS)" +regexes = [ + '''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InBneWRucGhiaW1ldGluc2dma2JvIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDc1OTQzNDYsImV4cCI6MjAyMzE3MDM0Nn0\.hAYOMFxxwX1exkQkY9xyQJGC_GhGnyogkj2N-kBkMI8''', + '''eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImxibXNvd2VodGp3bnFsdXJwZW1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MDkyMTg0ODcsImV4cCI6MjAyNDc5NDQ4N30\.zeLTMAuZ_WwYvGdeP0kdvL_Zrs-RQee5APPyxmWq7qQ''', +] diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..aa604fd --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,16 @@ +#!/usr/bin/env sh +# Scan staged changes for secrets before they ever enter git history. +# Mirrors the gitleaks CI gate (.github/workflows/cli-ci.yml) and shares the +# same allowlist (.gitleaks.toml), so the public Supabase anon keys won't trip. +# +# gitleaks is NOT a dependency of this package — contributors install the binary +# themselves. If it's missing we skip (exit 0) rather than block: CI is the +# real enforcement backstop, this hook is just the fast local warning. +if ! command -v gitleaks >/dev/null 2>&1; then + echo "gitleaks not found — skipping local secret scan." + echo " Install it to catch secrets before committing: https://github.com/gitleaks/gitleaks#installing" + echo " (CI still scans on push, so nothing slips through.)" + exit 0 +fi + +gitleaks git --staged --redact --no-banner diff --git a/README.md b/README.md index 7b14505..f3d027a 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,26 @@ $ dcd cloud --apiKey .myFlows/ See full documentation: [Docs](https://docs.devicecloud.dev) +## Development + +Requires Node 22+ and [pnpm](https://pnpm.io). `pnpm install` builds the CLI and installs the git hooks automatically. + +```sh-session +$ pnpm install # install deps, build, set up git hooks +$ pnpm dcd # run the CLI from source +$ pnpm lint # ESLint +$ pnpm typecheck # strict tsc, no emit +$ pnpm test # build + boot mock API + integration/unit tests +``` + +### Secret scanning + +A [gitleaks](https://github.com/gitleaks/gitleaks) scan runs in two places, both sharing the allowlist in `.gitleaks.toml`: + +- **pre-commit hook** (via husky) — scans your staged changes and blocks the commit if a secret is found. Install the binary so it can run; without it the hook skips with a warning: + ```sh-session + $ brew install gitleaks # or see github.com/gitleaks/gitleaks#installing + ``` +- **CI** — the `secret-scan` job scans the full history on every push and pull request, and is the enforced backstop regardless of local setup. + + diff --git a/package.json b/package.json index 64d6dc4..8fbe66e 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unicorn": "^64.0.0", + "husky": "^9.1.7", "mocha": "^11.7.5", "prettier": "^3.3.3", "shx": "^0.4.0", @@ -61,7 +62,7 @@ "build": "shx rm -rf dist && tsc -b && shx chmod +x dist/index.js", "build:binaries": "node scripts/build-binaries.mjs", "lint": "eslint src test --ext .ts", - "prepare": "pnpm build", + "prepare": "pnpm build && husky", "test": "node scripts/test-runner.mjs", "typecheck": "tsc --noEmit -p tsconfig.test.json" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99c37d8..6c345db 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -108,6 +108,9 @@ importers: eslint-plugin-unicorn: specifier: ^64.0.0 version: 64.0.0(eslint@9.39.4) + husky: + specifier: ^9.1.7 + version: 9.1.7 mocha: specifier: ^11.7.5 version: 11.7.5 @@ -1100,6 +1103,11 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + iceberg-js@0.8.1: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} @@ -3010,6 +3018,8 @@ snapshots: he@1.2.0: {} + husky@9.1.7: {} + iceberg-js@0.8.1: {} ignore@5.3.2: {}