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.
- package/AGENTS.md +48 -19
- package/README.md +48 -19
- package/cli.mjs +34 -14
- package/lib.mjs +20 -7
- 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.
|
|
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.
|
|
12
|
+
npx -y safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
|
|
13
13
|
# pnpm
|
|
14
|
-
pnpm dlx safetyvibe-connect@0.3.
|
|
14
|
+
pnpm dlx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
|
|
15
15
|
# bun
|
|
16
|
-
bunx safetyvibe-connect@0.3.
|
|
16
|
+
bunx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
|
|
17
17
|
# yarn
|
|
18
|
-
yarn dlx safetyvibe-connect@0.3.
|
|
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.
|
|
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.
|
|
56
|
+
npx -y safetyvibe-connect@0.3.1 setup --dry-run --url https://notes.example.com
|
|
33
57
|
```
|
|
34
58
|
|
|
35
|
-
By default
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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` |
|
|
107
|
-
| `scan [--quiet]` | Re-report;
|
|
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.
|
|
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.
|
|
12
|
+
npx -y safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
|
|
13
13
|
# pnpm
|
|
14
|
-
pnpm dlx safetyvibe-connect@0.3.
|
|
14
|
+
pnpm dlx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
|
|
15
15
|
# bun
|
|
16
|
-
bunx safetyvibe-connect@0.3.
|
|
16
|
+
bunx safetyvibe-connect@0.3.1 setup --no-hook --claim-token <TOKEN>
|
|
17
17
|
# yarn
|
|
18
|
-
yarn dlx safetyvibe-connect@0.3.
|
|
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.
|
|
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.
|
|
56
|
+
npx -y safetyvibe-connect@0.3.1 setup --dry-run --url https://notes.example.com
|
|
33
57
|
```
|
|
34
58
|
|
|
35
|
-
By default
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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` |
|
|
107
|
-
| `scan [--quiet]` | Re-report;
|
|
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
|
|
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
|
|
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'] && !
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) ?
|
|
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 (
|
|
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.
|
|
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
|
}
|