safetyvibe-connect 0.0.0-stage → 0.2.0

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 +8 -0
  2. package/README.md +45 -2
  3. package/cli.mjs +123 -0
  4. package/lib.mjs +211 -0
  5. package/package.json +31 -6
package/AGENTS.md ADDED
@@ -0,0 +1,8 @@
1
+ # For AI coding agents
2
+
3
+ This project uses `safetyvibe-connect` for security monitoring.
4
+
5
+ - To link or relink it: `bunx safetyvibe-connect setup` (`npx` on npm projects). Pass `--claim-token <TOKEN>` if the user gave you one, and `--url <published URL>` if you know it.
6
+ - Keep the postinstall hook: `"postinstall": "safetyvibe-connect scan --quiet || exit 0"`, or after an existing step: `"<existing> && (safetyvibe-connect scan --quiet || exit 0)"`. It re-reports dependencies after each install and never fails a build.
7
+ - Don't edit `.safetyviberc.json` by hand, except to add `{"claimToken": "..."}` when you can't run commands. Never commit `.safetyviberc.local.json`; it holds the report key.
8
+ - It sends only dependency names and versions, the app URL, and the public Supabase URL and anon key. Never put secrets into its config.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # safetyvibe-connect
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Links an app to [SafetyVibe](https://dashboard.safetyvibe.eu) security monitoring.
4
+ Made for AI builders (Lovable, Replit, Bolt, Claude Code…): paste one prompt, the builder's
5
+ agent installs this package, and the app shows up on your SafetyVibe dashboard.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ bun add safetyvibe-connect && bunx safetyvibe-connect setup --claim-token <TOKEN>
11
+ # npm
12
+ npm install --save safetyvibe-connect && npx safetyvibe-connect setup --claim-token <TOKEN>
13
+ ```
14
+
15
+ Get `<TOKEN>` from the dashboard (**Add app → Connect with one prompt**). Without a token,
16
+ `setup` prints a link you open to add the app to your account.
17
+
18
+ ## What it sends — and what it never sends
19
+
20
+ Sent (see it yourself with `safetyvibe-connect setup --dry-run`):
21
+ - package names and versions from your lockfile (to check them against known vulnerabilities)
22
+ - the app name, its public URL (if you pass `--url` or it is set) and a platform label
23
+ - your Supabase project URL and its **public** anon/publishable key — the same values your
24
+ website already ships to every visitor. A `service_role` / `sb_secret_` key is refused, never sent.
25
+
26
+ Never sent: source code, file paths, `.env` secrets, or any other key.
27
+
28
+ ## What it changes in your project
29
+
30
+ - `.safetyviberc.json` (safe to commit) — this app's SafetyVibe id and public URL. No secrets.
31
+ - `.safetyviberc.local.json` (added to `.gitignore`) — this app's report key. It can update this one
32
+ app's package list, link and Supabase details on SafetyVibe; it cannot read anything. Don't commit it.
33
+ - `package.json` → `scripts.postinstall` gets `safetyvibe-connect scan --quiet || exit 0`, so the package
34
+ list is re-reported after each install. It never fails an install or build, even after you remove the
35
+ connector.
36
+ - The one-prompt claim token links one app within 24 hours and is then useless; the CLI removes it.
37
+
38
+ Remove it any time: delete the postinstall entry, `.safetyviberc.json`, `.safetyviberc.local.json`, and the dependency.
39
+
40
+ ## Commands
41
+
42
+ | command | does |
43
+ |---|---|
44
+ | `setup [--claim-token T] [--url URL] [--dry-run]` | report + write `.safetyviberc.json` + add the postinstall hook |
45
+ | `scan [--quiet]` | report again (used by the hook; always exits 0) |
46
+ | `status` | show what is linked |
package/cli.mjs ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ // safetyvibe-connect — link this app to SafetyVibe security monitoring.
3
+ // setup [--claim-token T] [--url https://my-app.lovable.app] [--dry-run]
4
+ // scan [--quiet] re-report (runs from the postinstall hook; never fails a build)
5
+ // status show what is linked
6
+ // Sends ONLY: package names+versions, app name, public URL, platform label, and the
7
+ // Supabase project URL + its public anon/publishable key. Run with --dry-run to see it.
8
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { buildReport } from './lib.mjs';
11
+
12
+ const RC = '.safetyviberc.json'; // committed: app id + URL (no secrets)
13
+ const RC_LOCAL = '.safetyviberc.local.json'; // git-ignored: the app's report key
14
+ const HOOK = 'safetyvibe-connect scan --quiet || exit 0'; // works in sh and cmd.exe; never fails an install
15
+ const DEFAULT_API = 'https://dashboard.safetyvibe.eu/connect/api/v1';
16
+
17
+ const argv = process.argv.slice(2);
18
+ const cmd = argv[0] && !argv[0].startsWith('--') ? argv[0] : 'setup';
19
+ const flag = (name) => { const i = argv.indexOf('--' + name); return i !== -1 ? (argv[i + 1] && !argv[i + 1].startsWith('--') ? argv[i + 1] : true) : undefined; };
20
+ const quiet = !!flag('quiet');
21
+ const root = process.env.INIT_CWD || process.cwd();
22
+ const say = (...a) => { if (!quiet) console.log(...a); };
23
+
24
+ const readJsonFile = (f) => { try { return JSON.parse(readFileSync(join(root, f), 'utf8')) || {}; } catch { return {}; } };
25
+ function readRc() { return { ...readJsonFile(RC), ...readJsonFile(RC_LOCAL) }; }
26
+ function writeRc(rc) {
27
+ const { key, ...pub } = rc;
28
+ try {
29
+ writeFileSync(join(root, RC), JSON.stringify(pub, null, 2) + '\n');
30
+ if (key) { writeFileSync(join(root, RC_LOCAL), JSON.stringify({ key }, null, 2) + '\n'); ignoreLocal(); }
31
+ return true;
32
+ } catch { return false; } // read-only build sandboxes: nothing to persist
33
+ }
34
+ function ignoreLocal() {
35
+ const f = join(root, '.gitignore');
36
+ let t = ''; try { t = readFileSync(f, 'utf8'); } catch {}
37
+ if (t.split(/\r?\n/).some((l) => l.trim() === RC_LOCAL)) return;
38
+ try { writeFileSync(f, t + (t && !t.endsWith('\n') ? '\n' : '') + RC_LOCAL + '\n'); } catch {}
39
+ }
40
+
41
+ function addHook() {
42
+ const f = join(root, 'package.json');
43
+ let pj; try { pj = JSON.parse(readFileSync(f, 'utf8')); } catch { return 'no package.json'; }
44
+ pj.scripts = pj.scripts || {};
45
+ const cur = String(pj.scripts.postinstall || '');
46
+ // Only the connector's own failure is ignored: the user's existing postinstall keeps its exit code.
47
+ const ours = (s) => s.replace(/\s*&&\s*\(?\s*safetyvibe-connect scan[^&()]*\)?\s*$/, '').replace(/^\(?\s*safetyvibe-connect scan[^&()]*\)?\s*$/, '');
48
+ const base = ours(cur).trim();
49
+ const want = base ? base + ' && (' + HOOK + ')' : HOOK;
50
+ if (cur === want) return 'already there';
51
+ pj.scripts.postinstall = want;
52
+ try { writeFileSync(f, JSON.stringify(pj, null, 2) + '\n'); return 'added'; } catch { return 'could not write package.json'; }
53
+ }
54
+
55
+ async function post(api, body, headers) {
56
+ const ctl = new AbortController(); const t = setTimeout(() => ctl.abort(), 10000);
57
+ try {
58
+ const r = await fetch(api.replace(/\/+$/, '') + '/report', {
59
+ method: 'POST', signal: ctl.signal,
60
+ headers: { 'content-type': 'application/json', 'user-agent': 'safetyvibe-connect/0.2.0', ...headers },
61
+ body: JSON.stringify(body),
62
+ });
63
+ let j = {}; try { j = await r.json(); } catch {}
64
+ return { ok: r.ok, status: r.status, body: j };
65
+ } finally { clearTimeout(t); }
66
+ }
67
+
68
+ async function report({ setup }) {
69
+ const rc = readRc();
70
+ const claimToken = typeof flag('claim-token') === 'string' ? flag('claim-token') : (rc.claimToken || process.env.SAFETYVIBE_CLAIM_TOKEN || '');
71
+ const url = typeof flag('url') === 'string' ? flag('url') : (rc.url || '');
72
+ const api = process.env.SAFETYVIBE_API || rc.api || DEFAULT_API;
73
+ const body = buildReport(root, { url });
74
+
75
+ if (flag('dry-run')) { console.log(JSON.stringify(body, null, 2)); return 0; }
76
+
77
+ const key = rc.key || process.env.SAFETYVIBE_KEY || ''; // CI: provide the report key as a secret env var
78
+ // Linked app, but this machine has no key (fresh clone / CI): never create a stray new app.
79
+ if (rc.site && !key && !claimToken) {
80
+ say('SafetyVibe: this app is linked (' + rc.site + '), but its report key is not on this machine. ' +
81
+ 'Set SAFETYVIBE_KEY, or run "safetyvibe-connect setup --claim-token <new prompt token>" to relink.');
82
+ return 0;
83
+ }
84
+ const headers = {};
85
+ if (rc.site) headers['x-safetyvibe-site'] = rc.site;
86
+ if (rc.site && key) headers['x-safetyvibe-key'] = key;
87
+ if (claimToken) headers['x-safetyvibe-claim-token'] = claimToken; // with x-safetyvibe-site: relink that app
88
+
89
+ let res;
90
+ try { res = await post(api, body, headers); }
91
+ catch { say('SafetyVibe: could not reach ' + api + ' (will retry on the next install).'); return setup ? 1 : 0; }
92
+ if (!res.ok) { say('SafetyVibe: ' + (res.body.error || 'report rejected (HTTP ' + res.status + ')')); return setup ? 1 : 0; }
93
+
94
+ const b = res.body;
95
+ const next = { ...rc, site: b.site || rc.site, url: url || rc.url || undefined };
96
+ if (b.key) next.key = b.key;
97
+ delete next.claimToken; // one-shot: never keep a used claim token around
98
+ // Also rewrite when a key still sits in the committed file (older setups): move it to the local file.
99
+ const keyInCommitted = !!readJsonFile(RC).key;
100
+ if (keyInCommitted || JSON.stringify(next) !== JSON.stringify(rc)) writeRc(next);
101
+
102
+ const hook = setup ? addHook() : null;
103
+ const sb = body.supabase ? 'Supabase project ' + new URL(body.supabase.url).hostname : 'no Supabase project found';
104
+ say(`SafetyVibe: reported ${body.packages.length} packages (${body.lockfile}), ${sb}${body.url ? ', ' + body.url : ''}.`);
105
+ if (hook) say('SafetyVibe: postinstall hook ' + hook + ' (re-reports after every install).');
106
+ if (b.claimed) say('SafetyVibe: linked to your dashboard: ' + (b.appUrl || 'https://dashboard.safetyvibe.eu'));
107
+ else if (b.claimUrl) say('SafetyVibe: open this link to add the app to your dashboard: ' + b.claimUrl);
108
+ return 0;
109
+ }
110
+
111
+ async function main() {
112
+ if (cmd === 'status') {
113
+ const rc = readRc();
114
+ console.log(rc.site ? `Linked to SafetyVibe app ${rc.site}${rc.url ? ' (' + rc.url + ')' : ''}.` : (rc.claimToken ? 'Not reported yet (claim token present).' : 'Not linked. Run: safetyvibe-connect setup'));
115
+ return 0;
116
+ }
117
+ if (cmd === 'scan') { try { await report({ setup: false }); } catch {} return 0; } // never fail an install/build
118
+ if (cmd === 'setup') return report({ setup: true });
119
+ console.log('Usage: safetyvibe-connect setup [--claim-token T] [--url URL] [--dry-run] | scan [--quiet] | status');
120
+ return 1;
121
+ }
122
+
123
+ main().then((code) => process.exit(code), () => process.exit(cmd === 'scan' ? 0 : 1));
package/lib.mjs ADDED
@@ -0,0 +1,211 @@
1
+ // safetyvibe-connect — pure helpers (no network, no writes). Tested in connect.test.mjs.
2
+ // What leaves the machine is exactly buildReport()'s return value: package names +
3
+ // versions, app name, public URL, platform/environment labels, and the Supabase project
4
+ // URL + its PUBLIC (anon/publishable) key. Never source code, file paths or secrets.
5
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
6
+ import { join, basename } from 'node:path';
7
+
8
+ const MAX_PACKAGES = 5000;
9
+ const read = (f) => { try { return readFileSync(f, 'utf8'); } catch { return null; } };
10
+ const readJson = (f) => { const t = read(f); if (t == null) return null; try { return JSON.parse(t); } catch { return null; } };
11
+
12
+ // ---- lockfiles -------------------------------------------------------------
13
+ // Each parser returns [{ name, version }] for every installed package (deduped later).
14
+
15
+ export function fromPackageLock(json) {
16
+ const out = [];
17
+ if (json && json.packages && typeof json.packages === 'object') {
18
+ for (const [path, v] of Object.entries(json.packages)) {
19
+ if (!path || !v || typeof v.version !== 'string') continue; // "" = the root project
20
+ const i = path.lastIndexOf('node_modules/');
21
+ if (i === -1) continue;
22
+ out.push({ name: v.name || path.slice(i + 'node_modules/'.length), version: v.version });
23
+ }
24
+ } else if (json && json.dependencies) { // lockfileVersion 1
25
+ const walk = (deps) => { for (const [n, v] of Object.entries(deps || {})) { if (v && v.version) out.push({ name: n, version: v.version }); walk(v && v.dependencies); } };
26
+ walk(json.dependencies);
27
+ }
28
+ return out;
29
+ }
30
+
31
+ // bun.lock (text, JSONC): "packages": { "<key>": ["name@version", registry, {...}, integrity], ... }
32
+ export function fromBunLock(text) {
33
+ const clean = String(text).replace(/^\s*\/\/.*$/gm, '').replace(/,(\s*[}\]])/g, '$1');
34
+ let json; try { json = JSON.parse(clean); } catch { return []; }
35
+ const out = [];
36
+ for (const v of Object.values((json && json.packages) || {})) {
37
+ const spec = Array.isArray(v) ? v[0] : null;
38
+ if (typeof spec !== 'string') continue;
39
+ const at = spec.lastIndexOf('@');
40
+ if (at <= 0) continue;
41
+ const version = spec.slice(at + 1);
42
+ if (/^\d/.test(version)) out.push({ name: spec.slice(0, at), version }); // skip workspace:/git:/file:
43
+ }
44
+ return out;
45
+ }
46
+
47
+ // yarn.lock v1 ("name@range":\n version "x") and berry ("name@npm:range":\n version: x)
48
+ export function fromYarnLock(text) {
49
+ const out = []; let names = null;
50
+ for (const line of String(text).split(/\r?\n/)) {
51
+ if (/^\S.*:\s*$/.test(line) && !line.startsWith('#') && !line.startsWith('__metadata')) {
52
+ names = line.replace(/:\s*$/, '').split(',').map((s) => s.trim().replace(/^"|"$/g, ''))
53
+ .map((s) => s.slice(0, s.lastIndexOf('@') > 0 ? s.lastIndexOf('@') : s.length));
54
+ continue;
55
+ }
56
+ const m = line.match(/^\s+version:?\s+"?([^"\s]+)"?\s*$/);
57
+ if (m && names) { for (const n of new Set(names)) if (n) out.push({ name: n, version: m[1] }); names = null; }
58
+ }
59
+ return out;
60
+ }
61
+
62
+ // pnpm-lock.yaml: package keys like "/name@1.2.3", "name@1.2.3", "/@scope/name@1.2.3(peer@x)"
63
+ export function fromPnpmLock(text) {
64
+ const out = []; let inPackages = false;
65
+ for (const line of String(text).split(/\r?\n/)) {
66
+ if (/^(packages|snapshots):\s*$/.test(line)) { inPackages = line.startsWith('packages'); continue; }
67
+ if (/^\S/.test(line)) { inPackages = false; continue; }
68
+ if (!inPackages) continue;
69
+ const m = line.match(/^ {2}'?\/?((?:@[^/@\s]+\/)?[^@\s/]+)@(\d[^('\s:]*)/);
70
+ if (m) out.push({ name: m[1], version: m[2] });
71
+ }
72
+ return out;
73
+ }
74
+
75
+ // Fallback (bun.lockb is binary): what is actually installed in node_modules.
76
+ export function fromNodeModules(root) {
77
+ const out = []; const nm = join(root, 'node_modules');
78
+ let entries = []; try { entries = readdirSync(nm); } catch { return out; }
79
+ const add = (dir) => { const pj = readJson(join(dir, 'package.json')); if (pj && pj.name && pj.version) out.push({ name: pj.name, version: pj.version }); };
80
+ for (const e of entries) {
81
+ if (e.startsWith('.')) continue;
82
+ if (e.startsWith('@')) { let sub = []; try { sub = readdirSync(join(nm, e)); } catch {} for (const s of sub) add(join(nm, e, s)); }
83
+ else add(join(nm, e));
84
+ }
85
+ return out;
86
+ }
87
+
88
+ export function readPackages(root) {
89
+ let list = []; let source = 'none';
90
+ const tries = [
91
+ ['package-lock.json', (t) => fromPackageLock(JSON.parse(t))],
92
+ ['npm-shrinkwrap.json', (t) => fromPackageLock(JSON.parse(t))],
93
+ ['bun.lock', fromBunLock],
94
+ ['pnpm-lock.yaml', fromPnpmLock],
95
+ ['yarn.lock', fromYarnLock],
96
+ ];
97
+ for (const [file, parse] of tries) {
98
+ const t = read(join(root, file)); if (t == null) continue;
99
+ let ok = true;
100
+ try { list = parse(t); } catch { list = []; ok = false; }
101
+ if (list.length) { source = file; break; }
102
+ // A JSON lockfile that parses and lists nothing really means "no dependencies". The line-based
103
+ // yarn/pnpm parsers can't tell empty from unsupported, so those stay "unknown".
104
+ if (ok && /\.json$|^bun\.lock$/.test(file)) { source = file; break; }
105
+ }
106
+ if (!list.length && source === 'none') { list = fromNodeModules(root); if (list.length) source = 'node_modules'; }
107
+ const seen = new Set(); const pkgs = [];
108
+ for (const p of list) {
109
+ const k = p.name + '@' + p.version;
110
+ // Registry versions only. git/file/link/http specs can carry tokens or local paths: never sent.
111
+ if (seen.has(k) || !/^(@[\w.-]+\/)?[\w.-]+$/.test(p.name) || !/^\d+\.\d+\.\d+[\w.+-]{0,40}$/.test(String(p.version))) continue;
112
+ seen.add(k); pkgs.push({ ecosystem: 'npm', name: p.name, version: p.version });
113
+ if (pkgs.length >= MAX_PACKAGES) break;
114
+ }
115
+ return { packages: pkgs, source };
116
+ }
117
+
118
+ // ---- Supabase (project URL + PUBLIC key only) ------------------------------
119
+ export function keyKind(key) {
120
+ const k = String(key || '').trim();
121
+ if (/^sb_publishable_/.test(k)) return 'publishable';
122
+ if (/^sb_secret_/.test(k)) return 'secret';
123
+ const parts = k.split('.');
124
+ if (parts.length === 3) {
125
+ try {
126
+ const payload = JSON.parse(Buffer.from(parts[1].replace(/-/g, '+').replace(/_/g, '/'), 'base64').toString('utf8'));
127
+ if (payload && payload.role === 'anon') return 'anon';
128
+ if (payload && payload.role) return 'secret'; // service_role or anything else privileged
129
+ } catch { /* not a JWT */ }
130
+ }
131
+ return 'unknown';
132
+ }
133
+
134
+ function parseEnv(text) {
135
+ const env = {};
136
+ for (const line of String(text || '').split(/\r?\n/)) {
137
+ const m = line.match(/^\s*(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
138
+ if (m) env[m[1]] = m[2].replace(/^['"]|['"]$/g, '');
139
+ }
140
+ return env;
141
+ }
142
+
143
+ const URL_VARS = ['VITE_SUPABASE_URL', 'NEXT_PUBLIC_SUPABASE_URL', 'PUBLIC_SUPABASE_URL', 'EXPO_PUBLIC_SUPABASE_URL', 'SUPABASE_URL'];
144
+ const KEY_VARS = ['VITE_SUPABASE_PUBLISHABLE_KEY', 'VITE_SUPABASE_ANON_KEY', 'NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY', 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
145
+ 'PUBLIC_SUPABASE_ANON_KEY', 'EXPO_PUBLIC_SUPABASE_ANON_KEY', 'SUPABASE_PUBLISHABLE_KEY', 'SUPABASE_ANON_KEY'];
146
+
147
+ export function findSupabase(root, procEnv = process.env) {
148
+ const env = {};
149
+ for (const f of ['.env', '.env.local', '.env.production', '.env.production.local']) Object.assign(env, parseEnv(read(join(root, f))));
150
+ for (const k of [...URL_VARS, ...KEY_VARS]) if (procEnv[k]) env[k] = procEnv[k];
151
+ let url = URL_VARS.map((k) => env[k]).find(Boolean) || '';
152
+ let key = KEY_VARS.map((k) => env[k]).find(Boolean) || '';
153
+ // Lovable hard-codes them in src/integrations/supabase/client.ts
154
+ if (!url || !key) {
155
+ for (const f of ['src/integrations/supabase/client.ts', 'src/integrations/supabase/client.js', 'src/lib/supabase.ts', 'src/lib/supabase.js', 'lib/supabase.ts', 'utils/supabase.ts']) {
156
+ const t = read(join(root, f)); if (!t) continue;
157
+ url = url || ((t.match(/["'`](https:\/\/[a-z0-9-]+\.supabase\.co)["'`]/i) || [])[1] || '');
158
+ key = key || ((t.match(/(?:PUBLISHABLE|ANON)_KEY\s*=\s*["'`]([^"'`]+)["'`]/i) || [])[1] || '');
159
+ if (url && key) break;
160
+ }
161
+ }
162
+ if (!/^https:\/\/[a-z0-9-]+\.supabase\.(co|in)\/?$/i.test(url)) url = '';
163
+ const kind = key ? keyKind(key) : 'none';
164
+ // Never send anything that isn't provably a public key.
165
+ if (kind !== 'anon' && kind !== 'publishable') key = '';
166
+ return { url: url.replace(/\/$/, ''), key, kind };
167
+ }
168
+
169
+ // ---- app identity ------------------------------------------------------------
170
+ export function detectPlatform(root, procEnv = process.env) {
171
+ const pj = readJson(join(root, 'package.json')) || {};
172
+ const deps = { ...(pj.dependencies || {}), ...(pj.devDependencies || {}) };
173
+ if (deps['lovable-tagger'] || existsSync(join(root, '.lovable'))) return 'Lovable';
174
+ if (procEnv.REPL_ID || existsSync(join(root, '.replit'))) return 'Replit';
175
+ if (existsSync(join(root, '.bolt'))) return 'Bolt';
176
+ if (deps['@base44/sdk']) return 'Base44';
177
+ if (procEnv.VERCEL) return 'Vercel';
178
+ if (procEnv.NETLIFY) return 'Netlify';
179
+ return '';
180
+ }
181
+
182
+ export function appName(root) {
183
+ const pj = readJson(join(root, 'package.json')) || {};
184
+ const n = String(pj.name || '').trim();
185
+ // Builder templates ship a generic package name; send none so the dashboard can name it properly.
186
+ // Known builder template names only (a real name like "email-template-editor" is kept).
187
+ const generic = /^(vite[_-]react[_-]shadcn[_-]ts|tanstack[_-]start[_-]ts|vite[_-]react[_-]typescript[_-]starter|vite[_-]project|rest[_-]express|my[_-]app|app|react[_-]app|next[_-]?js[_-]app|next[_-]app|nextjs)$/i;
188
+ if (n && !generic.test(n)) return n.slice(0, 80);
189
+ const dir = basename(root);
190
+ return /^(app|project|workspace|src|home|repl|dev-server|code|\.)$/i.test(dir) ? '' : dir.slice(0, 80);
191
+ }
192
+
193
+ export function normPublicUrl(u) {
194
+ try { const x = new URL(String(u)); return x.protocol === 'https:' || x.protocol === 'http:' ? x.origin + (x.pathname === '/' ? '' : x.pathname) : ''; }
195
+ catch { return ''; }
196
+ }
197
+
198
+ export function buildReport(root, opts = {}, procEnv = process.env) {
199
+ const { packages, source } = readPackages(root);
200
+ const sb = findSupabase(root, procEnv);
201
+ return {
202
+ v: 1,
203
+ name: appName(root),
204
+ url: normPublicUrl(opts.url || procEnv.SAFETYVIBE_URL || ''),
205
+ platform: detectPlatform(root, procEnv),
206
+ environment: String(opts.environment || procEnv.SAFETYVIBE_ENV || (procEnv.NODE_ENV === 'production' ? 'production' : '')).slice(0, 20),
207
+ lockfile: source,
208
+ packages,
209
+ supabase: sb.url && sb.key ? { url: sb.url, key: sb.key, kind: sb.kind } : null,
210
+ };
211
+ }
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
- {
2
- "name": "safetyvibe-connect",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "safetyvibe-connect",
3
+ "version": "0.2.0",
4
+ "description": "SafetyVibe security monitoring for AI-built apps (Lovable, Replit, Bolt, Claude Code\u2026). Reports the app's dependency list, public URL and, if used, its Supabase project URL + public anon key. Never source code or secrets.",
5
+ "type": "module",
6
+ "bin": {
7
+ "safetyvibe-connect": "cli.mjs"
8
+ },
9
+ "files": [
10
+ "cli.mjs",
11
+ "lib.mjs",
12
+ "README.md",
13
+ "AGENTS.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=18"
17
+ },
18
+ "license": "UNLICENSED",
19
+ "homepage": "https://safetyvibe.eu",
20
+ "keywords": [
21
+ "security",
22
+ "vulnerabilities",
23
+ "lovable",
24
+ "replit",
25
+ "bolt",
26
+ "supabase",
27
+ "vibe-coding",
28
+ "safetyvibe"
29
+ ],
30
+ "author": "SafetyVibe <safetyvibe@advertis.ee>"
31
+ }