safetyvibe-connect 0.3.0 → 0.3.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 (5) hide show
  1. package/AGENTS.md +48 -19
  2. package/README.md +48 -19
  3. package/cli.mjs +34 -14
  4. package/lib.mjs +20 -7
  5. package/package.json +5 -1
package/AGENTS.md CHANGED
@@ -2,41 +2,65 @@
2
2
 
3
3
  SafetyVibe Connect reads package.json, the lockfile, and client-exposed Supabase config (client-prefixed env vars and the client Supabase file). It reports your package list so we can warn you about vulnerable dependencies. The JSON body sends exactly: v, optional name, url, platform and environment, lockfile, versions_source, source, packages, optional truncated, and supabase (null, or project_ref and host with key_kind and key_fingerprint, or secret_key_exposed_client_side). It never sends Supabase keys, secrets or file contents. Requires Node >=18; no dependencies or package install scripts.
4
4
 
5
- Full source: https://safetyvibe.eu/connect/source/0.3.0
5
+ Full source: https://safetyvibe.eu/connect/source/0.3.1
6
6
 
7
7
  ## Run without installing
8
8
 
9
9
  Get a claim token from the dashboard, then run:
10
10
 
11
11
  ```sh
12
- npx -y safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
12
+ npx -y safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
13
13
  # pnpm
14
- pnpm dlx safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
14
+ pnpm dlx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
15
15
  # bun
16
- bunx safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
16
+ bunx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
17
17
  # yarn
18
- yarn dlx safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
18
+ yarn dlx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
19
19
  ```
20
20
 
21
21
  Add `--url <published URL>` if known. Without a token, setup prints a claim link.
22
22
  On Replit/pnpm projects, `npm install` can fail with "Cannot read properties of null (reading 'isDescendantOf')".
23
23
  Use `pnpm add -D safetyvibe-connect`, or run without installing:
24
- `npx -y safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>`.
24
+ `npx -y safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>`.
25
25
  Optional installation commands: `npm install -D safetyvibe-connect`, `pnpm add -D safetyvibe-connect`, `bun add -d safetyvibe-connect`, `yarn add -D safetyvibe-connect`.
26
26
 
27
+ ## For AI agents
28
+
29
+ `setup --dry-run` makes no network call and writes nothing unless you pass `--out`. Check cli.mjs line 176: `return 0;` inside `if (flags['dry-run'])`. This exits before the only `fetch(` call. This describes the connector itself; fetching the package with npx is separate.
30
+
31
+ It reads package.json and one supported lockfile (package-lock.json, npm-shrinkwrap.json, bun.lock, pnpm-lock.yaml or yarn.lock). It may inspect candidate locks and ancestor package.json / pnpm-workspace.yaml files to select a workspace lock, stopping at .git or six parent levels. It checks whether bun.lockb exists, but does not parse it. It reads .safetyviberc.json and .safetyviberc.local.json for local link settings.
32
+
33
+ It reads and parses .env, .env.local, .env.production and .env.production.local locally, not just their key names. Service-role variable names produce local warnings. Client-prefixed values are checked for secret shapes; SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_ANON_KEY with VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_, REACT_APP_ or NUXT_PUBLIC_ prefixes supply the URL and public key used locally for a fingerprint. Process env is inspected the same way. Server-only Supabase values are not selected as connection details. For platform detection it also examines URL values in .env, .env.local and .env.production in the root and frontend/; those values are not sent.
34
+
35
+ Client files scanned for the Supabase URL and key shape, exactly:
36
+
37
+ - src/integrations/supabase/client.ts
38
+ - src/integrations/supabase/client.js
39
+ - src/lib/supabase.ts
40
+ - src/lib/supabase.js
41
+ - lib/supabase.ts
42
+ - utils/supabase.ts
43
+
44
+ Files marked server-only or use server are excluded. Prefix/comparison literals are ignored for secret detection. Only a full sb_secret_ shape (at least 20 body characters), a three-segment service_role JWT, or a JWT with any role other than anon under a publishable/anon key name sets the exposure flag. Unknown keys are never accepted as public.
45
+
46
+ Platform detection also reads metadata.json and vite.config.ts/js/mjs/mts, package names/dependencies, and checks .lovable, .replit, .bolt, .emergent, .softgen, .leap, .rocket and .v0 markers. The CLI uses cwd / INIT_CWD, package manager and agent/CI environment hints, plus SAFETYVIBE_API, SAFETYVIBE_URL, SAFETYVIBE_ENV, NODE_ENV, SAFETYVIBE_KEY, SAFETYVIBE_CLAIM_TOKEN and SAFETYVIBE_TIMEOUT_MS. No source files or raw Supabase keys are sent. The complete JSON field list and authentication headers are documented below under "Exactly what is sent".
47
+
48
+ Source: https://safetyvibe.eu/connect/source/0.3.1
49
+ Repository: https://github.com/kare22/safetyvibe-connect
50
+
27
51
  ## Exactly what is sent
28
52
 
29
53
  Preview the report before sending it:
30
54
 
31
55
  ```sh
32
- npx -y safetyvibe-connect@0.3.0 setup --dry-run --url https://notes.example.com
56
+ npx -y safetyvibe-connect@0.3.1 setup --dry-run --url https://notes.example.com
33
57
  ```
34
58
 
35
- By default dry-run stdout is exactly the pretty JSON body plus a newline. Its stderr carries five summary lines: Platform, Lockfile (package and dev-only counts), Supabase (fingerprint or exposed-secret flag only), Keys sent: 0, and Dry run: nothing sent. The summary is under 400 characters. It is followed by a plain explanation, destination and header names with values hidden, then one-line warnings (including any "Dropped <field>: it looked like a key." notices). Dry-run makes no requests. With --dry-run --out report.json, the body is written to that file without sending; otherwise JSON goes to stdout and the summary to stderr.
59
+ By default non-TTY and agent/CI runs print only five summary lines on stdout: Platform, Lockfile (package and dev-only counts), Supabase (fingerprint or exposed-secret flag only), Keys sent: 0, and Dry run: nothing sent. Warnings go to stderr. Use --json for the full body on stdout, with the summary, explanation and destination/header names (values hidden) on stderr. A regular TTY keeps that JSON behaviour by default. --summary wins over --json. Dry-run makes no requests. --out preserves the export behaviour described below.
36
60
 
37
- Real runs print the summary on stdout, ending with Connected: and the dashboard or validated claim link (printed once). --quiet suppresses success chatter, but safety warnings always appear on stderr, including during dry runs. Accepted reports show the Connected line even if saving local config or installing the hook fails; warnings and errors follow the summary. Rejected or unreachable reports have no Connected line.
61
+ Real runs print the summary on stdout, ending with Connected: and the dashboard or validated claim link (printed once). If setup adds the postinstall hook, one more line says so, because package.json changed. --quiet suppresses success chatter, but safety warnings always appear on stderr, including during dry runs. Accepted reports show the Connected line even if saving local config or installing the hook fails; warnings and errors follow the summary. Rejected or unreachable reports have no Connected line.
38
62
 
39
- --summary prints only the summary on stdout in dry and real runs, with warnings on stderr and no JSON or follow-up chatter. --out <file.json> (also --out=file.json) writes the exact JSON body plus newline instead of printing JSON; in dry runs stdout stays empty and the summary stays on stderr unless --summary is also used. It works for real runs too. The file must be new, end in .json, and resolve inside the project root with an existing parent directory; parent symlink escapes are refused. It uses exclusive creation and never overwrites files. Connector config filenames and package.json are reserved so later setup writes cannot replace the exported body. Invalid output paths exit 2 for setup/dry run; scan always exits 0 with an error on stderr.
63
+ --summary prints only the summary on stdout in dry and real runs, with warnings on stderr and no JSON or follow-up chatter. --out <file.json> (also --out=file.json) writes the exact JSON body plus newline instead of printing JSON; in dry runs stdout stays empty and the summary stays on stderr unless --summary is also used. It works for real runs too. The file must be new, end in .json, and resolve inside the project root with an existing parent directory; parent symlink escapes are refused. It uses exclusive creation and never overwrites files. Connector config filenames and package.json are reserved so later setup writes cannot replace the exported body. Invalid output paths exit 2, including scan, with an error on stderr.
40
64
  Example (optional `truncated` appears only when the inventory exceeds 5000):
41
65
 
42
66
  ```json
@@ -61,31 +85,36 @@ Example (optional `truncated` appears only when the inventory exceeds 5000):
61
85
  - `lockfile`: selected lockfile filename, or `none` when no readable lockfile exists. Parent locks are used only at workspace roots when the project matches a workspace glob in package.json workspaces (array or workspaces.packages) or the pnpm-workspace.yaml packages list. Literal paths, * and ** are supported; negated globs exclude matching paths from membership. Search stops at .git or six parent levels.
62
86
  - `packages`: registry package names and versions, npm ecosystem, and optional `dev: true` for dev-only dependencies when recorded in the lockfile. Workspace entries are excluded; incompatible optional npm binaries are excluded. Names that a lockfile declares with a git/tarball/file/link spec are excluded, including hoisted entries. Recognized empty or fully excluded locks remain authoritative. Supported formats are npm v1/v2/v3, Yarn v1/Berry, pnpm v5.4/v6/v9 and text bun.lock; installed node_modules folders are never read. Without a readable lockfile (none, only binary bun.lockb, or an unparseable one) no packages are sent, `lockfile` is `none` (the dashboard keeps the last known list) and the CLI prints: "No readable lockfile found (package-lock.json, pnpm-lock.yaml, yarn.lock or bun.lock); dependency check unavailable." For bun.lockb run `bun install --save-text-lockfile` to create bun.lock.
63
87
  - `truncated`: optional boolean; only the first 5000 packages are sent.
64
- - `versions_source`: always `lockfile`; `source`: always `connector`.
88
+ - `versions_source`: always `lockfile`.
89
+ - `source`: always `connector`.
65
90
  - `supabase`: null unless a validated URL and public key are found, or a client-exposed secret is detected. Public keys yield only `project_ref`, `host`, `key_kind` (`anon` or `publishable`) and `key_fingerprint` (first eight lowercase hex characters of SHA-256 of the UTF-8 key). `host` is the URL host only; `project_ref` is the lowercase 20-character alphanumeric ref of a .supabase.co host, otherwise null. An exposed secret always wins: only `{ "project_ref": null, "host": null, "secret_key_exposed_client_side": true }` is sent, with project_ref/host filled when a validated URL is known. No key kind, fingerprint or length is sent for secrets.
66
91
 
92
+ Nested fields: `packages[].ecosystem` is `npm`; `packages[].name` is the registry name; `packages[].version` is the exact lockfile version; optional `packages[].dev` is true for dev-only packages. `supabase.project_ref` identifies the project (or null); `supabase.host` is the validated host (or null); `supabase.key_kind` is anon or publishable; `supabase.key_fingerprint` is the eight-character SHA-256 prefix; `supabase.secret_key_exposed_client_side` is true only for a detected client-exposed secret and replaces kind/fingerprint.
93
+
67
94
  All report strings are screened before sending or printing: key-like values (including encoded and Unicode-disguised keys) are dropped, with a local warning naming only the field. A key-like URL is dropped entirely.
68
95
 
69
96
  Never sent: source code, file paths, full lockfiles, .env files, Supabase keys (including anon/publishable), secrets, URL credentials or query strings.
70
97
  The request headers x-safetyvibe-site, x-safetyvibe-key and x-safetyvibe-claim-token carry the validated UUID site ID, validated report key and validated one-shot claim token when available; these are not in the JSON body. The server validates public keys too for older clients; this connector sends no Supabase keys.
71
- Public keys are read only from client-exposed variables (prefixes VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_, REACT_APP_, NUXT_PUBLIC_; from the environment or .env files) or a client-side Supabase source file (src/integrations/supabase/client.*, src/lib/supabase.*, lib/supabase.ts, utils/supabase.ts). Files that import or require server-only, start with "use server", are named *.server.*, or sit under server/, api/, app/api/, pages/api/, functions/, supabase/functions/, netlify/functions/ or are middleware.* are skipped. Server-only names such as SUPABASE_URL or SUPABASE_ANON_KEY are not read. Every client-prefixed variable in process env and .env/.env.local/.env.production/.env.production.local is checked for exposed secrets, as is every quoted string in the scanned client files. Server-only names remain unflagged. Local warnings name only the variable or relative client file, never the value, and tell you to rotate exposed secrets in Supabase.
98
+ Public keys are read only from client-exposed variables (prefixes VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_, REACT_APP_, NUXT_PUBLIC_; from the environment or .env files) or a client-side Supabase source file (src/integrations/supabase/client.*, src/lib/supabase.*, lib/supabase.ts, utils/supabase.ts). Files that contain the quoted text server-only or use server anywhere (even in a comment), are named *.server.*, or sit under server/, api/, app/api/, pages/api/, functions/, supabase/functions/, netlify/functions/ or are middleware.* are skipped. Server-only names such as SUPABASE_URL or SUPABASE_ANON_KEY are not selected as connection details. Every client-prefixed variable in process env and .env/.env.local/.env.production/.env.production.local is checked for exposed secrets, as are quoted strings in the scanned client files (each key-shaped quoted value is also checked on its own, so a stray apostrophe in a comment cannot hide it), excluding prefix checks and comparisons. A key split across concatenated strings is not detected. Any service-role variable name in a .env* file, including SUPABASE_SERVICE_ROLE_KEY and SUPABASE_SERVICE_KEY, produces a separate local warning; its presence alone never sets an exposure flag or adds anything to the body. Local warnings name only the variable or relative client file, never the value, and tell you to rotate exposed secrets in Supabase.
72
99
 
73
- Review the entire implementation in `cli.mjs` and `lib.mjs` (approximately 633 lines total). There are no dependencies and no install scripts in this package itself.
100
+ Review the entire implementation in `cli.mjs` and `lib.mjs` (approximately 666 lines total). There are no dependencies and no install scripts in this package itself.
74
101
  By default reports go only to `https://dashboard.safetyvibe.eu/connect/api/v1/report`. The committed rc cannot change the destination.
75
102
  An explicit `SAFETYVIBE_API` environment override supports HTTPS, or HTTP on localhost/127.0.0.1/[::1] for local testing. Redirects are refused.
76
103
 
77
104
  ## Changes
78
105
 
79
- 0.3.0 never sends Supabase keys: it reports public-key fingerprints or exposed-secret flags, adds a five-line summary, --summary and safe --out JSON export, and ships MIT licensing and security contact details. 0.2.3 prints a one-line summary first (dry run: package count, Supabase yes/no, destination; setup/scan: result and the dashboard link, printed once) so terminals that cut off long output still show it; in a dry run the full JSON follows. 0.2.2 improves response deadlines, registry-only inventory, project selection and app labels, restores flag-only setup, and rejects ambiguous JWT payloads. It reads pnpm v5.4 locks and no longer reads installed node_modules folders: without a readable lockfile no packages are sent. 0.2.1 fixed a bug where a key on a commented .env line could be included; upgrade from 0.2.0.
106
+ 0.3.1 fixes false secret alarms from Supabase prefix checks while keeping real secret warnings and public fingerprints. Non-TTY and agent/CI runs default to a five-line summary; --json opts into the body. It checks config writability before sending, stops re-sending env claim tokens for linked projects, warns locally about service-role variable names, and exits 2 for bad --out paths even on scan. It adds AI-agent source guidance and the public repository link.
107
+
108
+ 0.3.0 never sends Supabase keys: it reports public-key fingerprints or exposed-secret flags, puts a five-line summary first, adds --summary and safe --out JSON export, and ships MIT licensing and security contact details. 0.2.2 improves response deadlines, registry-only inventory, project selection and app labels, restores flag-only setup, and rejects ambiguous JWT payloads. It reads pnpm v5.4 locks and no longer reads installed node_modules folders: without a readable lockfile no packages are sent. 0.2.1 fixed a bug where a key on a commented .env line could be included; upgrade from 0.2.0.
80
109
 
81
110
  [Documentation](https://safetyvibe.eu/connect) and [changelog / what is sent](https://safetyvibe.eu/connect/changelog).
82
111
 
83
112
  ## Files written and removal
84
113
 
85
- Before a setup/scan request, legacy credentials are migrated best-effort into the local file, the committed api field is dropped and its URL normalized. Dry-run writes only an explicitly requested --out file; status and help never write. After a successful report:
114
+ Before a setup/scan request, the connector probes directory writability and checks existing config files; a failure sends nothing and exits 1 for setup (0 for scan). Then legacy credentials are migrated best-effort into the local file, the committed api field is dropped and its URL normalized. Dry-run writes only an explicitly requested --out file; status and help never write. After a successful report:
86
115
 
87
116
  - `.safetyviberc.json`: app ID, normalized public URL, optional name and `hook: false`.
88
- - `.safetyviberc.local.json`: report key and optional fallback claim token; never commit this file. The token is removed once used or rejected as expired/used. Only stored credentials require the local file and ignore entry. The ignore entry is attempted first, then this file is written with mode 0600. An ignore failure warns and fails setup; scan still exits 0. Empty local credentials are removed.
117
+ - `.safetyviberc.local.json`: report key and optional fallback claim token; never commit this file. The token is removed once used or rejected as expired/used. Once a valid site ID and report key are present, SAFETYVIBE_CLAIM_TOKEN is not re-sent. Only stored credentials require the local file and ignore entry. The ignore entry is attempted first, then this file is written with mode 0600. An ignore failure warns and fails setup; scan still exits 0. Empty local credentials are removed.
89
118
  - `.gitignore`: adds the local config filename.
90
119
  - `package.json`: setup normally adds a postinstall report hook. `--no-hook` leaves package.json untouched and persists this preference. Existing unsafe shell scripts are left untouched with a manual instruction.
91
120
 
@@ -103,12 +132,12 @@ Remove the connector dependency if installed, its postinstall command, both rc f
103
132
  | Command | Effect |
104
133
  |---|---|
105
134
  | `setup [--claim-token T] [--url URL] [--name NAME] [--no-hook]` | Send report, save config, optionally add hook |
106
- | `setup --dry-run` or `--dry-run` | Print exact report without network; only --out writes a file |
107
- | `scan [--quiet]` | Re-report; always exits 0 for install hooks |
135
+ | `setup --dry-run` or `--dry-run` | Preview summary (--json for the body) without network; only --out writes a file |
136
+ | `scan [--quiet]` | Re-report; exits 0 for install hooks, except bad --out exits 2 |
108
137
  | `status` | Show local link state |
109
138
  | `help`, `--help`, `-h`, no arguments | Usage only |
110
139
  | `--version`, `-v` | Package version only |
111
140
 
112
141
  Flags without a command imply setup. Unknown name, URL, platform and environment fields are omitted, never empty strings.
113
142
 
114
- Value flags accept `--flag=value` or `--flag value`, including values starting with a dash. In the space form, a known flag (also with `=...`) is treated as a missing value. Unknown commands/flags and missing values exit 2, except scan exits 0.
143
+ Value flags accept `--flag=value` or `--flag value`, including values starting with a dash. In the space form, a known flag (also with `=...`) is treated as a missing value. Unknown commands/flags and missing values exit 2, except scan exits 0 (a missing --out value still exits 2).
package/README.md CHANGED
@@ -2,41 +2,65 @@
2
2
 
3
3
  SafetyVibe Connect reads package.json, the lockfile, and client-exposed Supabase config (client-prefixed env vars and the client Supabase file). It reports your package list so we can warn you about vulnerable dependencies. The JSON body sends exactly: v, optional name, url, platform and environment, lockfile, versions_source, source, packages, optional truncated, and supabase (null, or project_ref and host with key_kind and key_fingerprint, or secret_key_exposed_client_side). It never sends Supabase keys, secrets or file contents. Requires Node >=18; no dependencies or package install scripts.
4
4
 
5
- Full source: https://safetyvibe.eu/connect/source/0.3.0
5
+ Full source: https://safetyvibe.eu/connect/source/0.3.1
6
6
 
7
7
  ## Run without installing
8
8
 
9
9
  Get a claim token from the dashboard, then run:
10
10
 
11
11
  ```sh
12
- npx -y safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
12
+ npx -y safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
13
13
  # pnpm
14
- pnpm dlx safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
14
+ pnpm dlx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
15
15
  # bun
16
- bunx safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
16
+ bunx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
17
17
  # yarn
18
- yarn dlx safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>
18
+ yarn dlx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
19
19
  ```
20
20
 
21
21
  Add `--url <published URL>` if known. Without a token, setup prints a claim link.
22
22
  On Replit/pnpm projects, `npm install` can fail with "Cannot read properties of null (reading 'isDescendantOf')".
23
23
  Use `pnpm add -D safetyvibe-connect`, or run without installing:
24
- `npx -y safetyvibe-connect@0.3.0 setup --no-hook --claim-token <TOKEN>`.
24
+ `npx -y safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>`.
25
25
  Optional installation commands: `npm install -D safetyvibe-connect`, `pnpm add -D safetyvibe-connect`, `bun add -d safetyvibe-connect`, `yarn add -D safetyvibe-connect`.
26
26
 
27
+ ## For AI agents
28
+
29
+ `setup --dry-run` makes no network call and writes nothing unless you pass `--out`. Check cli.mjs line 176: `return 0;` inside `if (flags['dry-run'])`. This exits before the only `fetch(` call. This describes the connector itself; fetching the package with npx is separate.
30
+
31
+ It reads package.json and one supported lockfile (package-lock.json, npm-shrinkwrap.json, bun.lock, pnpm-lock.yaml or yarn.lock). It may inspect candidate locks and ancestor package.json / pnpm-workspace.yaml files to select a workspace lock, stopping at .git or six parent levels. It checks whether bun.lockb exists, but does not parse it. It reads .safetyviberc.json and .safetyviberc.local.json for local link settings.
32
+
33
+ It reads and parses .env, .env.local, .env.production and .env.production.local locally, not just their key names. Service-role variable names produce local warnings. Client-prefixed values are checked for secret shapes; SUPABASE_URL, SUPABASE_PUBLISHABLE_KEY and SUPABASE_ANON_KEY with VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_, REACT_APP_ or NUXT_PUBLIC_ prefixes supply the URL and public key used locally for a fingerprint. Process env is inspected the same way. Server-only Supabase values are not selected as connection details. For platform detection it also examines URL values in .env, .env.local and .env.production in the root and frontend/; those values are not sent.
34
+
35
+ Client files scanned for the Supabase URL and key shape, exactly:
36
+
37
+ - src/integrations/supabase/client.ts
38
+ - src/integrations/supabase/client.js
39
+ - src/lib/supabase.ts
40
+ - src/lib/supabase.js
41
+ - lib/supabase.ts
42
+ - utils/supabase.ts
43
+
44
+ Files marked server-only or use server are excluded. Prefix/comparison literals are ignored for secret detection. Only a full sb_secret_ shape (at least 20 body characters), a three-segment service_role JWT, or a JWT with any role other than anon under a publishable/anon key name sets the exposure flag. Unknown keys are never accepted as public.
45
+
46
+ Platform detection also reads metadata.json and vite.config.ts/js/mjs/mts, package names/dependencies, and checks .lovable, .replit, .bolt, .emergent, .softgen, .leap, .rocket and .v0 markers. The CLI uses cwd / INIT_CWD, package manager and agent/CI environment hints, plus SAFETYVIBE_API, SAFETYVIBE_URL, SAFETYVIBE_ENV, NODE_ENV, SAFETYVIBE_KEY, SAFETYVIBE_CLAIM_TOKEN and SAFETYVIBE_TIMEOUT_MS. No source files or raw Supabase keys are sent. The complete JSON field list and authentication headers are documented below under "Exactly what is sent".
47
+
48
+ Source: https://safetyvibe.eu/connect/source/0.3.1
49
+ Repository: https://github.com/kare22/safetyvibe-connect
50
+
27
51
  ## Exactly what is sent
28
52
 
29
53
  Preview the report before sending it:
30
54
 
31
55
  ```sh
32
- npx -y safetyvibe-connect@0.3.0 setup --dry-run --url https://notes.example.com
56
+ npx -y safetyvibe-connect@0.3.1 setup --dry-run --url https://notes.example.com
33
57
  ```
34
58
 
35
- By default dry-run stdout is exactly the pretty JSON body plus a newline. Its stderr carries five summary lines: Platform, Lockfile (package and dev-only counts), Supabase (fingerprint or exposed-secret flag only), Keys sent: 0, and Dry run: nothing sent. The summary is under 400 characters. It is followed by a plain explanation, destination and header names with values hidden, then one-line warnings (including any "Dropped <field>: it looked like a key." notices). Dry-run makes no requests. With --dry-run --out report.json, the body is written to that file without sending; otherwise JSON goes to stdout and the summary to stderr.
59
+ By default non-TTY and agent/CI runs print only five summary lines on stdout: Platform, Lockfile (package and dev-only counts), Supabase (fingerprint or exposed-secret flag only), Keys sent: 0, and Dry run: nothing sent. Warnings go to stderr. Use --json for the full body on stdout, with the summary, explanation and destination/header names (values hidden) on stderr. A regular TTY keeps that JSON behaviour by default. --summary wins over --json. Dry-run makes no requests. --out preserves the export behaviour described below.
36
60
 
37
- Real runs print the summary on stdout, ending with Connected: and the dashboard or validated claim link (printed once). --quiet suppresses success chatter, but safety warnings always appear on stderr, including during dry runs. Accepted reports show the Connected line even if saving local config or installing the hook fails; warnings and errors follow the summary. Rejected or unreachable reports have no Connected line.
61
+ Real runs print the summary on stdout, ending with Connected: and the dashboard or validated claim link (printed once). If setup adds the postinstall hook, one more line says so, because package.json changed. --quiet suppresses success chatter, but safety warnings always appear on stderr, including during dry runs. Accepted reports show the Connected line even if saving local config or installing the hook fails; warnings and errors follow the summary. Rejected or unreachable reports have no Connected line.
38
62
 
39
- --summary prints only the summary on stdout in dry and real runs, with warnings on stderr and no JSON or follow-up chatter. --out <file.json> (also --out=file.json) writes the exact JSON body plus newline instead of printing JSON; in dry runs stdout stays empty and the summary stays on stderr unless --summary is also used. It works for real runs too. The file must be new, end in .json, and resolve inside the project root with an existing parent directory; parent symlink escapes are refused. It uses exclusive creation and never overwrites files. Connector config filenames and package.json are reserved so later setup writes cannot replace the exported body. Invalid output paths exit 2 for setup/dry run; scan always exits 0 with an error on stderr.
63
+ --summary prints only the summary on stdout in dry and real runs, with warnings on stderr and no JSON or follow-up chatter. --out <file.json> (also --out=file.json) writes the exact JSON body plus newline instead of printing JSON; in dry runs stdout stays empty and the summary stays on stderr unless --summary is also used. It works for real runs too. The file must be new, end in .json, and resolve inside the project root with an existing parent directory; parent symlink escapes are refused. It uses exclusive creation and never overwrites files. Connector config filenames and package.json are reserved so later setup writes cannot replace the exported body. Invalid output paths exit 2, including scan, with an error on stderr.
40
64
  Example (optional `truncated` appears only when the inventory exceeds 5000):
41
65
 
42
66
  ```json
@@ -61,31 +85,36 @@ Example (optional `truncated` appears only when the inventory exceeds 5000):
61
85
  - `lockfile`: selected lockfile filename, or `none` when no readable lockfile exists. Parent locks are used only at workspace roots when the project matches a workspace glob in package.json workspaces (array or workspaces.packages) or the pnpm-workspace.yaml packages list. Literal paths, * and ** are supported; negated globs exclude matching paths from membership. Search stops at .git or six parent levels.
62
86
  - `packages`: registry package names and versions, npm ecosystem, and optional `dev: true` for dev-only dependencies when recorded in the lockfile. Workspace entries are excluded; incompatible optional npm binaries are excluded. Names that a lockfile declares with a git/tarball/file/link spec are excluded, including hoisted entries. Recognized empty or fully excluded locks remain authoritative. Supported formats are npm v1/v2/v3, Yarn v1/Berry, pnpm v5.4/v6/v9 and text bun.lock; installed node_modules folders are never read. Without a readable lockfile (none, only binary bun.lockb, or an unparseable one) no packages are sent, `lockfile` is `none` (the dashboard keeps the last known list) and the CLI prints: "No readable lockfile found (package-lock.json, pnpm-lock.yaml, yarn.lock or bun.lock); dependency check unavailable." For bun.lockb run `bun install --save-text-lockfile` to create bun.lock.
63
87
  - `truncated`: optional boolean; only the first 5000 packages are sent.
64
- - `versions_source`: always `lockfile`; `source`: always `connector`.
88
+ - `versions_source`: always `lockfile`.
89
+ - `source`: always `connector`.
65
90
  - `supabase`: null unless a validated URL and public key are found, or a client-exposed secret is detected. Public keys yield only `project_ref`, `host`, `key_kind` (`anon` or `publishable`) and `key_fingerprint` (first eight lowercase hex characters of SHA-256 of the UTF-8 key). `host` is the URL host only; `project_ref` is the lowercase 20-character alphanumeric ref of a .supabase.co host, otherwise null. An exposed secret always wins: only `{ "project_ref": null, "host": null, "secret_key_exposed_client_side": true }` is sent, with project_ref/host filled when a validated URL is known. No key kind, fingerprint or length is sent for secrets.
66
91
 
92
+ Nested fields: `packages[].ecosystem` is `npm`; `packages[].name` is the registry name; `packages[].version` is the exact lockfile version; optional `packages[].dev` is true for dev-only packages. `supabase.project_ref` identifies the project (or null); `supabase.host` is the validated host (or null); `supabase.key_kind` is anon or publishable; `supabase.key_fingerprint` is the eight-character SHA-256 prefix; `supabase.secret_key_exposed_client_side` is true only for a detected client-exposed secret and replaces kind/fingerprint.
93
+
67
94
  All report strings are screened before sending or printing: key-like values (including encoded and Unicode-disguised keys) are dropped, with a local warning naming only the field. A key-like URL is dropped entirely.
68
95
 
69
96
  Never sent: source code, file paths, full lockfiles, .env files, Supabase keys (including anon/publishable), secrets, URL credentials or query strings.
70
97
  The request headers x-safetyvibe-site, x-safetyvibe-key and x-safetyvibe-claim-token carry the validated UUID site ID, validated report key and validated one-shot claim token when available; these are not in the JSON body. The server validates public keys too for older clients; this connector sends no Supabase keys.
71
- Public keys are read only from client-exposed variables (prefixes VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_, REACT_APP_, NUXT_PUBLIC_; from the environment or .env files) or a client-side Supabase source file (src/integrations/supabase/client.*, src/lib/supabase.*, lib/supabase.ts, utils/supabase.ts). Files that import or require server-only, start with "use server", are named *.server.*, or sit under server/, api/, app/api/, pages/api/, functions/, supabase/functions/, netlify/functions/ or are middleware.* are skipped. Server-only names such as SUPABASE_URL or SUPABASE_ANON_KEY are not read. Every client-prefixed variable in process env and .env/.env.local/.env.production/.env.production.local is checked for exposed secrets, as is every quoted string in the scanned client files. Server-only names remain unflagged. Local warnings name only the variable or relative client file, never the value, and tell you to rotate exposed secrets in Supabase.
98
+ Public keys are read only from client-exposed variables (prefixes VITE_, NEXT_PUBLIC_, EXPO_PUBLIC_, PUBLIC_, REACT_APP_, NUXT_PUBLIC_; from the environment or .env files) or a client-side Supabase source file (src/integrations/supabase/client.*, src/lib/supabase.*, lib/supabase.ts, utils/supabase.ts). Files that contain the quoted text server-only or use server anywhere (even in a comment), are named *.server.*, or sit under server/, api/, app/api/, pages/api/, functions/, supabase/functions/, netlify/functions/ or are middleware.* are skipped. Server-only names such as SUPABASE_URL or SUPABASE_ANON_KEY are not selected as connection details. Every client-prefixed variable in process env and .env/.env.local/.env.production/.env.production.local is checked for exposed secrets, as are quoted strings in the scanned client files (each key-shaped quoted value is also checked on its own, so a stray apostrophe in a comment cannot hide it), excluding prefix checks and comparisons. A key split across concatenated strings is not detected. Any service-role variable name in a .env* file, including SUPABASE_SERVICE_ROLE_KEY and SUPABASE_SERVICE_KEY, produces a separate local warning; its presence alone never sets an exposure flag or adds anything to the body. Local warnings name only the variable or relative client file, never the value, and tell you to rotate exposed secrets in Supabase.
72
99
 
73
- Review the entire implementation in `cli.mjs` and `lib.mjs` (approximately 633 lines total). There are no dependencies and no install scripts in this package itself.
100
+ Review the entire implementation in `cli.mjs` and `lib.mjs` (approximately 666 lines total). There are no dependencies and no install scripts in this package itself.
74
101
  By default reports go only to `https://dashboard.safetyvibe.eu/connect/api/v1/report`. The committed rc cannot change the destination.
75
102
  An explicit `SAFETYVIBE_API` environment override supports HTTPS, or HTTP on localhost/127.0.0.1/[::1] for local testing. Redirects are refused.
76
103
 
77
104
  ## Changes
78
105
 
79
- 0.3.0 never sends Supabase keys: it reports public-key fingerprints or exposed-secret flags, adds a five-line summary, --summary and safe --out JSON export, and ships MIT licensing and security contact details. 0.2.3 prints a one-line summary first (dry run: package count, Supabase yes/no, destination; setup/scan: result and the dashboard link, printed once) so terminals that cut off long output still show it; in a dry run the full JSON follows. 0.2.2 improves response deadlines, registry-only inventory, project selection and app labels, restores flag-only setup, and rejects ambiguous JWT payloads. It reads pnpm v5.4 locks and no longer reads installed node_modules folders: without a readable lockfile no packages are sent. 0.2.1 fixed a bug where a key on a commented .env line could be included; upgrade from 0.2.0.
106
+ 0.3.1 fixes false secret alarms from Supabase prefix checks while keeping real secret warnings and public fingerprints. Non-TTY and agent/CI runs default to a five-line summary; --json opts into the body. It checks config writability before sending, stops re-sending env claim tokens for linked projects, warns locally about service-role variable names, and exits 2 for bad --out paths even on scan. It adds AI-agent source guidance and the public repository link.
107
+
108
+ 0.3.0 never sends Supabase keys: it reports public-key fingerprints or exposed-secret flags, puts a five-line summary first, adds --summary and safe --out JSON export, and ships MIT licensing and security contact details. 0.2.2 improves response deadlines, registry-only inventory, project selection and app labels, restores flag-only setup, and rejects ambiguous JWT payloads. It reads pnpm v5.4 locks and no longer reads installed node_modules folders: without a readable lockfile no packages are sent. 0.2.1 fixed a bug where a key on a commented .env line could be included; upgrade from 0.2.0.
80
109
 
81
110
  [Documentation](https://safetyvibe.eu/connect) and [changelog / what is sent](https://safetyvibe.eu/connect/changelog).
82
111
 
83
112
  ## Files written and removal
84
113
 
85
- Before a setup/scan request, legacy credentials are migrated best-effort into the local file, the committed api field is dropped and its URL normalized. Dry-run writes only an explicitly requested --out file; status and help never write. After a successful report:
114
+ Before a setup/scan request, the connector probes directory writability and checks existing config files; a failure sends nothing and exits 1 for setup (0 for scan). Then legacy credentials are migrated best-effort into the local file, the committed api field is dropped and its URL normalized. Dry-run writes only an explicitly requested --out file; status and help never write. After a successful report:
86
115
 
87
116
  - `.safetyviberc.json`: app ID, normalized public URL, optional name and `hook: false`.
88
- - `.safetyviberc.local.json`: report key and optional fallback claim token; never commit this file. The token is removed once used or rejected as expired/used. Only stored credentials require the local file and ignore entry. The ignore entry is attempted first, then this file is written with mode 0600. An ignore failure warns and fails setup; scan still exits 0. Empty local credentials are removed.
117
+ - `.safetyviberc.local.json`: report key and optional fallback claim token; never commit this file. The token is removed once used or rejected as expired/used. Once a valid site ID and report key are present, SAFETYVIBE_CLAIM_TOKEN is not re-sent. Only stored credentials require the local file and ignore entry. The ignore entry is attempted first, then this file is written with mode 0600. An ignore failure warns and fails setup; scan still exits 0. Empty local credentials are removed.
89
118
  - `.gitignore`: adds the local config filename.
90
119
  - `package.json`: setup normally adds a postinstall report hook. `--no-hook` leaves package.json untouched and persists this preference. Existing unsafe shell scripts are left untouched with a manual instruction.
91
120
 
@@ -103,12 +132,12 @@ Remove the connector dependency if installed, its postinstall command, both rc f
103
132
  | Command | Effect |
104
133
  |---|---|
105
134
  | `setup [--claim-token T] [--url URL] [--name NAME] [--no-hook]` | Send report, save config, optionally add hook |
106
- | `setup --dry-run` or `--dry-run` | Print exact report without network; only --out writes a file |
107
- | `scan [--quiet]` | Re-report; always exits 0 for install hooks |
135
+ | `setup --dry-run` or `--dry-run` | Preview summary (--json for the body) without network; only --out writes a file |
136
+ | `scan [--quiet]` | Re-report; exits 0 for install hooks, except bad --out exits 2 |
108
137
  | `status` | Show local link state |
109
138
  | `help`, `--help`, `-h`, no arguments | Usage only |
110
139
  | `--version`, `-v` | Package version only |
111
140
 
112
141
  Flags without a command imply setup. Unknown name, URL, platform and environment fields are omitted, never empty strings.
113
142
 
114
- Value flags accept `--flag=value` or `--flag value`, including values starting with a dash. In the space form, a known flag (also with `=...`) is treated as a missing value. Unknown commands/flags and missing values exit 2, except scan exits 0.
143
+ Value flags accept `--flag=value` or `--flag value`, including values starting with a dash. In the space form, a known flag (also with `=...`) is treated as a missing value. Unknown commands/flags and missing values exit 2, except scan exits 0 (a missing --out value still exits 2).
package/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync, writeFileSync, chmodSync, unlinkSync, realpathSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync, chmodSync, unlinkSync, realpathSync, accessSync, statSync, constants } from 'node:fs';
3
3
  import { join, resolve, relative, dirname, isAbsolute, sep } from 'node:path';
4
4
  import { buildReport, findSupabase, normPublicUrl, urlNeedsCleaning, withHook, HOOK, packageManager, readPackages, screenString, secretLike, takeScreenWarnings } from './lib.mjs';
5
5
 
@@ -9,7 +9,7 @@ const DEFAULT_API = 'https://dashboard.safetyvibe.eu/connect/api/v1';
9
9
  const REPORT_KEY = /^svk_[A-Za-z0-9_-]{32}$/;
10
10
  const CLAIM_TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
11
11
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
12
- const USAGE = 'Usage: safetyvibe-connect setup|scan|status|help [--claim-token TOKEN] [--url URL] [--name NAME] [--dry-run] [--summary] [--out FILE.json] [--quiet] [--no-hook] [--help/-h] [--version/-v]\n--dry-run: summary on stderr, JSON on stdout; --out FILE.json writes the body without sending.';
12
+ const USAGE = 'Usage: safetyvibe-connect setup|scan|status|help [--claim-token TOKEN] [--url URL] [--name NAME] [--dry-run] [--summary] [--json] [--out FILE.json] [--quiet] [--no-hook] [--help/-h] [--version/-v]\n--dry-run: no network; non-TTY/agent default is summary. --json: full body on stdout, summary on stderr; --summary wins; --dry-run --out FILE.json writes the body without sending.';
13
13
  const argv = process.argv.slice(2);
14
14
  const flags = {};
15
15
  let cmd = ''; let error = '';
@@ -24,15 +24,15 @@ for (let i = 0; i < argv.length; i++) {
24
24
  const name = ({ '-h': 'help', '-v': 'version' })[raw] || raw.replace(/^--/, '');
25
25
  if (['claim-token', 'url', 'name', 'out'].includes(name)) {
26
26
  const value = rest.length ? rest.join('=') : argv[++i];
27
- if (!value || (!rest.length && /^(?:--(?:dry-run|quiet|no-hook|help|version|claim-token|url|name|summary|out)|-h|-v)(?:=|$)/.test(value))) { error = 'Missing value for --' + name + '.'; break; }
27
+ if (!value || (!rest.length && /^(?:--(?:dry-run|quiet|no-hook|help|version|claim-token|url|name|summary|json|out)|-h|-v)(?:=|$)/.test(value))) { error = 'Missing value for --' + name + '.'; break; }
28
28
  flags[name] = value;
29
- } else if (['dry-run', 'quiet', 'no-hook', 'help', 'version', 'summary'].includes(name) && !rest.length) flags[name] = true;
29
+ } else if (['dry-run', 'quiet', 'no-hook', 'help', 'version', 'summary', 'json'].includes(name) && !rest.length) flags[name] = true;
30
30
  else { error = 'Unknown flag.'; break; }
31
31
  }
32
32
  if (error) {
33
33
  const scan = cmd === 'scan' || argv[0] === 'scan';
34
34
  if (!(scan && argv.includes('--quiet'))) console.error(USAGE + '\n' + error);
35
- process.exit(scan ? 0 : 2);
35
+ process.exit(scan && !error.startsWith('Missing value for --out') ? 0 : 2);
36
36
  }
37
37
  if (!argv.length || cmd === 'help' || flags.help) { console.log(USAGE); process.exit(0); }
38
38
  if (!cmd) cmd = 'setup';
@@ -47,7 +47,8 @@ function workspaceRoot(dir) {
47
47
  try { return /^packages\s*:/m.test(readFileSync(join(dir, 'pnpm-workspace.yaml'), 'utf8')); } catch { return false; }
48
48
  }
49
49
  const root = process.env.INIT_CWD && process.env.INIT_CWD !== process.cwd() && !workspaceRoot(process.env.INIT_CWD) && installed(process.env.INIT_CWD, true) ? process.env.INIT_CWD : process.cwd();
50
- const say = (...args) => { if (!flags.quiet && !flags['dry-run'] && !flags.summary) console.log(...args); };
50
+ const summaryOnly = flags.summary || (!flags.json && !flags.out && (!process.stdout.isTTY || process.env.TERM === 'dumb' || (process.env.CI && !/^(false|0)$/i.test(process.env.CI)) || Object.keys(process.env).some(k => /^(CLAUDECODE|REPLIT.*|REPL_ID|LOVABLE.*|BOLT.*|CODEX.*|EMERGENT.*|GITHUB_ACTIONS|GITLAB_CI|BUILD_ID|CONTINUOUS_INTEGRATION)$/.test(k))));
51
+ const say = (...args) => { if (!flags.quiet && !flags['dry-run'] && !flags.summary && (cmd === 'status' || !summaryOnly)) console.log(...args); };
51
52
  const warnings = [];
52
53
  const alert = message => warnings.push(message);
53
54
  const flushWarnings = () => { for (const message of [...takeScreenWarnings(), ...warnings.splice(0)]) console.error(message); };
@@ -80,6 +81,20 @@ function writeRc(rc) {
80
81
  return ignored ? 'saved' : 'unignored';
81
82
  }
82
83
 
84
+ function writableConfig() {
85
+ const probe = join(root, '.safetyvibe-write-' + process.pid); let created = false;
86
+ try {
87
+ for (const file of [RC, LOCAL]) {
88
+ try { if (!statSync(join(root, file)).isFile()) return false; accessSync(join(root, file), constants.W_OK); }
89
+ catch (e) { if (e.code !== 'ENOENT') return false; }
90
+ }
91
+ writeFileSync(probe, '', { flag: 'wx', mode: 0o600 }); created = true;
92
+ unlinkSync(probe); created = false;
93
+ return true;
94
+ } catch { return false; }
95
+ finally { if (created) try { unlinkSync(probe); } catch {} }
96
+ }
97
+
83
98
  function addHook() {
84
99
  let pj; let original;
85
100
  try { original = readFileSync(join(root, 'package.json'), 'utf8'); pj = JSON.parse(original); } catch { return 'skipped (no readable package.json)'; }
@@ -107,7 +122,7 @@ function summary(body, last) {
107
122
  'Keys sent: 0', last,
108
123
  ];
109
124
  if (lines.join('\n').length >= 400) lines[4] = 'Connected: open your SafetyVibe dashboard';
110
- if (flags['dry-run'] || !flags.quiet) (flags['dry-run'] && !flags.summary ? console.error : console.log)(lines.join('\n'));
125
+ if (flags['dry-run'] || !flags.quiet) (flags['dry-run'] && !summaryOnly ? console.error : console.log)(lines.join('\n'));
111
126
  flushWarnings();
112
127
  }
113
128
  function writeBody(json) {
@@ -125,7 +140,8 @@ async function report() {
125
140
  let destination;
126
141
  try { destination = apiEndpoint(); } catch { alert('Invalid SAFETYVIBE_API: use HTTPS or local HTTP.'); return 1; }
127
142
  const rc = readRc();
128
- const rawToken = flags['claim-token'] || rc.claimToken || process.env.SAFETYVIBE_CLAIM_TOKEN || '';
143
+ const linked = typeof rc.site === 'string' && UUID.test(rc.site) && REPORT_KEY.test(rc.key || process.env.SAFETYVIBE_KEY || '');
144
+ const rawToken = flags['claim-token'] || rc.claimToken || (!linked && process.env.SAFETYVIBE_CLAIM_TOKEN) || '';
129
145
  const token = typeof rawToken === 'string' && CLAIM_TOKEN.test(rawToken) ? rawToken : '';
130
146
  if (rawToken && !token) alert('Invalid claim token in --claim-token, SAFETYVIBE_CLAIM_TOKEN or .safetyviberc.local.json/.safetyviberc.json.');
131
147
  const givenUrl = flags.url || process.env.SAFETYVIBE_URL || rc.url || '';
@@ -139,7 +155,9 @@ async function report() {
139
155
  if (typeof rc.site === 'string' && UUID.test(rc.site)) headers['x-safetyvibe-site'] = rc.site;
140
156
  if (rc.site && key) headers['x-safetyvibe-key'] = key;
141
157
  if (token) headers['x-safetyvibe-claim-token'] = token;
142
- for (const finding of findSupabase(root).warnings) alert(`WARNING: ${safeName(finding)} holds a Supabase secret/service_role key in client-exposed config; every visitor can read it. Rotate it in Supabase now. Only a flag is sent, never the key.`);
158
+ const supabase = findSupabase(root);
159
+ for (const name of supabase.serviceVariables) alert(`found a service-role key variable (${safeName(name)}); make sure it is never in client code`);
160
+ for (const finding of supabase.warnings) alert(`WARNING: ${safeName(finding)} holds a Supabase secret/service_role key in client-exposed config; every visitor can read it. Rotate it in Supabase now. Only a flag is sent, never the key.`);
143
161
  const lockState = readPackages(root);
144
162
  if (lockState.noLockfile && !flags.quiet) alert('No readable lockfile found (package-lock.json, pnpm-lock.yaml, yarn.lock or bun.lock); dependency check unavailable.'
145
163
  + (lockState.binaryBun ? ' Run `bun install --save-text-lockfile` to create bun.lock.' : ''));
@@ -149,17 +167,18 @@ async function report() {
149
167
  // Keep the five summary lines first; warnings follow the destination details.
150
168
  const pending = [...takeScreenWarnings(), ...warnings.splice(0)];
151
169
  summary(body, 'Dry run: nothing sent');
152
- if (!flags.summary) {
170
+ if (!summaryOnly) {
153
171
  console.error(body.supabase?.secret_key_exposed_client_side ? 'A secret key is exposed in client code; only that fact is reported, never the key.' : 'No keys or secrets are sent. The anon key is public by design. Only a fingerprint is sent.');
154
172
  console.error(safeText(destination) + ' headers: ' + Object.keys(headers).join(', ') + ' (values hidden)');
155
173
  }
156
174
  warnings.push(...pending); flushWarnings();
157
- if (!flags.summary && !flags.out) process.stdout.write(json);
175
+ if (!summaryOnly && !flags.out) process.stdout.write(json);
158
176
  return 0;
159
177
  }
160
178
  if (!setup && !rc.site && !key && !token && !installed(root)) {
161
179
  say('SafetyVibe: no project connection or connector dependency; skipping report.'); return 0;
162
180
  }
181
+ if (!writableConfig()) { alert('SafetyVibe: project config is not writable (.safetyviberc.json / .safetyviberc.local.json); nothing sent. Run setup in a writable checkout.'); return 1; }
163
182
  let saveFailed = false;
164
183
  const legacy = readJson(RC);
165
184
  if (['key', 'claimToken', 'api'].some(k => Object.hasOwn(legacy, k)) || (legacy.url && legacy.url !== normPublicUrl(legacy.url))) {
@@ -170,7 +189,7 @@ async function report() {
170
189
  const manager = packageManager(root);
171
190
  const runner = { npm: 'npx', pnpm: 'pnpm dlx', yarn: 'yarn dlx', bun: 'bunx' }[manager];
172
191
  if (rc.site && !key && !token) {
173
- say('SafetyVibe: report key is not on this machine. Set SAFETYVIBE_KEY or get a new prompt and run ' + runner + ' safetyvibe-connect setup --claim-token <TOKEN>.');
192
+ alert('SafetyVibe: report key is not on this machine. Set SAFETYVIBE_KEY or get a new prompt and run ' + runner + ' safetyvibe-connect setup --claim-token <TOKEN>.');
174
193
  return setup ? 1 : 0;
175
194
  }
176
195
  let response; let b;
@@ -237,11 +256,12 @@ async function report() {
237
256
  followup = 'SafetyVibe: postinstall hook ' + hook + '.';
238
257
  } else if (setup) followup = 'Re-report manually: ' + runner + ' safetyvibe-connect scan';
239
258
  if (saveFailed || saved === 'unignored') return 1;
240
- if (followup) say(followup);
259
+ // A changed package.json is always reported, even in summary mode.
260
+ if (followup) (/hook added/.test(followup) && !flags.quiet && !flags.summary ? console.log : say)(followup);
241
261
  return 0;
242
262
  }
243
263
  async function main() {
244
264
  if (cmd === 'status') { const rc = readRc(); say(rc.site ? 'Linked to SafetyVibe app ' + safeText(rc.site) + (normPublicUrl(rc.url) ? ' (' + normPublicUrl(rc.url) + ')' : '') + '.' : 'Not linked.'); return 0; }
245
265
  return report();
246
266
  }
247
- main().then(code => { flushWarnings(); process.exit(cmd === 'scan' ? 0 : code); }, () => { alert('SafetyVibe: could not complete report.'); flushWarnings(); process.exit(cmd === 'scan' ? 0 : 1); });
267
+ main().then(code => { flushWarnings(); process.exit(cmd === 'scan' && code !== 2 ? 0 : code); }, () => { alert('SafetyVibe: could not complete report.'); flushWarnings(); process.exit(cmd === 'scan' ? 0 : 1); });
package/lib.mjs CHANGED
@@ -211,7 +211,9 @@ export function parseEnv(text) {
211
211
  const CLIENT_PREFIXES = ['VITE_', 'NEXT_PUBLIC_', 'EXPO_PUBLIC_', 'PUBLIC_', 'REACT_APP_', 'NUXT_PUBLIC_'];
212
212
  const URL_VARS = CLIENT_PREFIXES.map(p => p + 'SUPABASE_URL');
213
213
  const KEY_VARS = CLIENT_PREFIXES.flatMap(p => [p + 'SUPABASE_PUBLISHABLE_KEY', p + 'SUPABASE_ANON_KEY']);
214
- const exposedSupabaseSecret = value => typeof value === 'string' && (value.startsWith('sb_secret_') || jwtPayload(value)?.role === 'service_role');
214
+ const exposedSupabaseSecret = value => typeof value === 'string' && (/^sb_secret_[A-Za-z0-9_-]{20,}$/.test(value) || (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value) && jwtPayload(value)?.role === 'service_role'));
215
+ // A value assigned to a publishable/anon key name must be public: any other JWT role there is a secret.
216
+ const secretInKeySlot = value => exposedSupabaseSecret(value) || (typeof value === 'string' && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(value) && !publicKeyKind(value) && typeof jwtPayload(value)?.role === 'string' && jwtPayload(value).role !== 'anon');
215
217
 
216
218
  // Only clearly client-side source files: what the browser bundle already ships to every visitor.
217
219
  const SERVER_PATH = /(^|\/)(server|api|functions)\/|(^|\/)app\/api\/|(^|\/)pages\/api\/|(^|\/)supabase\/functions\/|(^|\/)netlify\/functions\/|(^|\/)middleware\.[cm]?[jt]sx?$|\.server\.[cm]?[jt]sx?$/i;
@@ -222,16 +224,18 @@ export function clientSource(path, text) {
222
224
  return !/['"`]server-only['"`]/.test(text) && !/['"`]use server['"`]/.test(text);
223
225
  }
224
226
  export function findSupabase(root, procEnv = process.env) {
225
- const env = {}; const warnings = new Set();
226
- const collect = values => {
227
+ const env = {}; const warnings = new Set(); const serviceVariables = new Set();
228
+ const collect = (values, file = false) => {
227
229
  for (const k of Object.keys(values)) {
230
+ // Server-side env vars are normal; only names written into .env* files get the local reminder.
231
+ if (file && /SERVICE_ROLE|(?:^|_)SUPABASE_SERVICE_KEY$/i.test(k)) serviceVariables.add(k);
228
232
  if (!CLIENT_PREFIXES.some(p => k.startsWith(p))) continue;
229
233
  const value = values[k];
230
- if (KEY_VARS.includes(k) ? keyKind(value) === 'secret' : exposedSupabaseSecret(value)) warnings.add(k);
234
+ if (KEY_VARS.includes(k) ? secretInKeySlot(value) : exposedSupabaseSecret(value)) warnings.add(k);
231
235
  if (value) env[k] = value;
232
236
  }
233
237
  };
234
- for (const f of ['.env', '.env.local', '.env.production', '.env.production.local']) collect(parseEnv(read(join(root, f))));
238
+ for (const f of ['.env', '.env.local', '.env.production', '.env.production.local']) collect(parseEnv(read(join(root, f))), true);
235
239
  collect(procEnv);
236
240
  let url = URL_VARS.map(k => env[k]).find(value => /^https:\/\/[a-z0-9-]+\.supabase\.(co|in)\/?$/i.test(value || '')) || '';
237
241
  let key = KEY_VARS.map(k => env[k]).find(value => publicKeyKind(value)) || '';
@@ -240,10 +244,19 @@ export function findSupabase(root, procEnv = process.env) {
240
244
  const t = read(join(root, f)); if (!t || !clientSource(f, t)) continue;
241
245
  url = url || ((t.match(/["'`](https:\/\/[a-z0-9-]+\.supabase\.co)["'`]/i) || [])[1] || '');
242
246
  for (const match of t.matchAll(/(["'`])((?:\\.|(?!\1)[^\\])*?)\1/g)) {
247
+ const before = t.slice(0, match.index); const after = t.slice(match.index + match[0].length);
248
+ if (/\.(startsWith|endsWith|includes|indexOf)\(\s*$/.test(before) || /[!=]==?\s*$/.test(before) || /^\s*[!=]==?/.test(after)) continue;
249
+ if (exposedSupabaseSecret(match[2])) warnings.add(f);
250
+ }
251
+ // Quote pairing above can drift (an apostrophe in a comment); also check each key-shaped run that is
252
+ // directly quoted on both sides, with the same comparison/prefix-check skips.
253
+ for (const match of t.matchAll(/(["'`])(sb_secret_[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)\1/g)) {
254
+ const before = t.slice(0, match.index); const after = t.slice(match.index + match[0].length);
255
+ if (/\.(startsWith|endsWith|includes|indexOf)\(\s*$/.test(before) || /[!=]==?\s*$/.test(before) || /^\s*[!=]==?/.test(after)) continue;
243
256
  if (exposedSupabaseSecret(match[2])) warnings.add(f);
244
257
  }
245
258
  for (const match of t.matchAll(/([\w]*(?:PUBLISHABLE|ANON)_KEY)\s*=\s*["'`]([^"'`]+)["'`]/gi)) {
246
- if (keyKind(match[2]) === 'secret') warnings.add(f);
259
+ if (secretInKeySlot(match[2])) warnings.add(f);
247
260
  if (!key && publicKeyKind(match[2])) key = match[2];
248
261
  }
249
262
  }
@@ -251,7 +264,7 @@ export function findSupabase(root, procEnv = process.env) {
251
264
  const kind = key ? keyKind(key) : 'none';
252
265
  // Never send anything that isn't provably a public key.
253
266
  if (!publicKeyKind(key)) key = '';
254
- return { url: url.replace(/\/$/, ''), key, kind, warnings: [...warnings] };
267
+ return { url: url.replace(/\/$/, ''), key, kind, warnings: [...warnings], serviceVariables: [...serviceVariables] };
255
268
  }
256
269
 
257
270
  // ---- app identity ------------------------------------------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safetyvibe-connect",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Checks your app's dependencies for known vulnerabilities and its public configuration for common mistakes. Sends the package list and public settings only; preview with --dry-run.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,5 +31,9 @@
31
31
  "bugs": {
32
32
  "email": "safetyvibe@advertis.ee",
33
33
  "url": "https://safetyvibe.eu/connect"
34
+ },
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/kare22/safetyvibe-connect.git"
34
38
  }
35
39
  }