vibehacker 4.1.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.
@@ -0,0 +1,287 @@
1
+ 'use strict';
2
+
3
+ // ── Supabase integration for Vibe Hacker ───────────────────────────────────────
4
+ // Handles: user auth (signup/login), API key storage, usage tracking
5
+ // Uses raw HTTP — no SDK dependency needed
6
+
7
+ const axios = require('axios');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ // ── Config ──────────────────────────────────────────────────────────────────
13
+ // Auto-fetched from the Vibe Hacker website, or set via env vars.
14
+ // Public keys — safe to ship in the CLI.
15
+
16
+ const WEBSITE_URL = 'https://vibsecurity.com';
17
+
18
+ // Hardcoded Supabase config — these are PUBLIC keys, safe to ship.
19
+ const SUPABASE_URL = process.env.VH_SUPABASE_URL || 'https://hyouvrekinaczqejszfd.supabase.co';
20
+ const SUPABASE_ANON_KEY = process.env.VH_SUPABASE_ANON_KEY || 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imh5b3V2cmVraW5hY3pxZWpzemZkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzUyMjkxMzMsImV4cCI6MjA5MDgwNTEzM30.q5avvSV3OwnU3gWbF1gS7DnmewULizQmUk8LkONQqmc';
21
+
22
+ // Local session storage
23
+ const SESSION_FILE = path.join(os.homedir(), '.vibehacker', 'session.json');
24
+
25
+ // ── Session persistence ─────────────────────────────────────────────────────
26
+
27
+ function loadSession() {
28
+ try {
29
+ if (fs.existsSync(SESSION_FILE)) {
30
+ return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf8'));
31
+ }
32
+ } catch (_) {}
33
+ return null;
34
+ }
35
+
36
+ function saveSession(session) {
37
+ try {
38
+ const dir = path.dirname(SESSION_FILE);
39
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
40
+ fs.writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2));
41
+ } catch (_) {}
42
+ }
43
+
44
+ function clearSession() {
45
+ try { fs.unlinkSync(SESSION_FILE); } catch (_) {}
46
+ }
47
+
48
+ // ── Check if Supabase is configured ─────────────────────────────────────────
49
+
50
+ function isConfigured() {
51
+ return !!(SUPABASE_URL && SUPABASE_ANON_KEY);
52
+ }
53
+
54
+ // ── Supabase Auth API ───────────────────────────────────────────────────────
55
+
56
+ function headers(accessToken) {
57
+ const h = {
58
+ 'Content-Type': 'application/json',
59
+ 'apikey': SUPABASE_ANON_KEY,
60
+ };
61
+ if (accessToken) h['Authorization'] = `Bearer ${accessToken}`;
62
+ return h;
63
+ }
64
+
65
+ /**
66
+ * Sign up a new user
67
+ * @returns {{ user, session, error }}
68
+ */
69
+ async function signUp(email, password) {
70
+ try {
71
+ const r = await axios.post(`${SUPABASE_URL}/auth/v1/signup`, {
72
+ email,
73
+ password,
74
+ }, { headers: headers(), timeout: 15000 });
75
+
76
+ const session = {
77
+ access_token: r.data.access_token,
78
+ refresh_token: r.data.refresh_token,
79
+ user: r.data.user,
80
+ expires_at: r.data.expires_at,
81
+ };
82
+ if (session.access_token) saveSession(session);
83
+ return { user: r.data.user, session, error: null };
84
+ } catch (err) {
85
+ const msg = err.response?.data?.msg || err.response?.data?.error_description || err.message;
86
+ return { user: null, session: null, error: msg };
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Sign in existing user
92
+ * @returns {{ user, session, error }}
93
+ */
94
+ async function signIn(email, password) {
95
+ try {
96
+ const r = await axios.post(`${SUPABASE_URL}/auth/v1/token?grant_type=password`, {
97
+ email,
98
+ password,
99
+ }, { headers: headers(), timeout: 15000 });
100
+
101
+ const session = {
102
+ access_token: r.data.access_token,
103
+ refresh_token: r.data.refresh_token,
104
+ user: r.data.user,
105
+ expires_at: Date.now() + (r.data.expires_in * 1000),
106
+ };
107
+ saveSession(session);
108
+ return { user: r.data.user, session, error: null };
109
+ } catch (err) {
110
+ const msg = err.response?.data?.msg || err.response?.data?.error_description || err.message;
111
+ return { user: null, session: null, error: msg };
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Refresh the access token
117
+ */
118
+ async function refreshSession() {
119
+ const session = loadSession();
120
+ if (!session || !session.refresh_token) return null;
121
+
122
+ try {
123
+ const r = await axios.post(`${SUPABASE_URL}/auth/v1/token?grant_type=refresh_token`, {
124
+ refresh_token: session.refresh_token,
125
+ }, { headers: headers(), timeout: 15000 });
126
+
127
+ const newSession = {
128
+ access_token: r.data.access_token,
129
+ refresh_token: r.data.refresh_token,
130
+ user: r.data.user,
131
+ expires_at: Date.now() + (r.data.expires_in * 1000),
132
+ };
133
+ saveSession(newSession);
134
+ return newSession;
135
+ } catch (_) {
136
+ clearSession();
137
+ return null;
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Get current authenticated session (auto-refresh if expired)
143
+ */
144
+ async function getSession() {
145
+ const session = loadSession();
146
+ if (!session) return null;
147
+
148
+ // If expired, try to refresh
149
+ if (session.expires_at && Date.now() > session.expires_at - 60000) {
150
+ return await refreshSession();
151
+ }
152
+ return session;
153
+ }
154
+
155
+ /**
156
+ * Logout — clear local session
157
+ */
158
+ function logout() {
159
+ clearSession();
160
+ }
161
+
162
+ // ── API key management (via Supabase database) ─────────────────────────────
163
+
164
+ /**
165
+ * Fetch user's stored API key from Supabase `api_keys` table
166
+ */
167
+ async function getApiKey(session) {
168
+ if (!session || !session.access_token) return null;
169
+
170
+ try {
171
+ const r = await axios.get(
172
+ `${SUPABASE_URL}/rest/v1/api_keys?user_id=eq.${session.user.id}&select=provider_key,provider_type&limit=1`,
173
+ { headers: headers(session.access_token), timeout: 10000 }
174
+ );
175
+ if (r.data && r.data.length > 0) {
176
+ return r.data[0];
177
+ }
178
+ } catch (_) {}
179
+ return null;
180
+ }
181
+
182
+ /**
183
+ * Store user's API key in Supabase `api_keys` table
184
+ */
185
+ async function saveApiKey(session, providerKey, providerType) {
186
+ if (!session || !session.access_token) return false;
187
+
188
+ try {
189
+ // Upsert — update if exists, insert if not
190
+ await axios.post(
191
+ `${SUPABASE_URL}/rest/v1/api_keys`,
192
+ {
193
+ user_id: session.user.id,
194
+ provider_key: providerKey,
195
+ provider_type: providerType || 'openrouter',
196
+ },
197
+ {
198
+ headers: {
199
+ ...headers(session.access_token),
200
+ 'Prefer': 'resolution=merge-duplicates',
201
+ },
202
+ timeout: 10000,
203
+ }
204
+ );
205
+ return true;
206
+ } catch (_) {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Log usage (optional — for dashboard analytics)
213
+ */
214
+ async function logUsage(session, { tokens, model, provider }) {
215
+ if (!session || !session.access_token) return;
216
+
217
+ try {
218
+ await axios.post(
219
+ `${SUPABASE_URL}/rest/v1/usage_logs`,
220
+ {
221
+ user_id: session.user.id,
222
+ tokens: tokens || 0,
223
+ model: model || '',
224
+ provider: provider || '',
225
+ },
226
+ { headers: headers(session.access_token), timeout: 5000 }
227
+ );
228
+ } catch (_) {
229
+ // Non-critical — don't error out
230
+ }
231
+ }
232
+
233
+ // ── Tier management (via Supabase RPC functions) ────────────────────────────
234
+
235
+ /**
236
+ * Check API key tier and remaining requests.
237
+ * Calls the check_api_key Supabase function.
238
+ * @returns {{ valid, tier, daily_limit, requests_today, remaining, email } | null}
239
+ */
240
+ async function checkApiKey(apiKey) {
241
+ if (!apiKey) return null;
242
+ try {
243
+ const r = await axios.post(
244
+ `${SUPABASE_URL}/rest/v1/rpc/check_api_key`,
245
+ { p_key: apiKey },
246
+ { headers: headers(), timeout: 5000 }
247
+ );
248
+ return r.data || null;
249
+ } catch (_) {
250
+ return null; // Network error — don't block the user
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Use a request — increments counter, checks limit.
256
+ * Calls the use_request Supabase function.
257
+ * @returns {{ allowed, tier, remaining, error, upgrade_url } | null}
258
+ */
259
+ async function useRequest(apiKey) {
260
+ if (!apiKey) return null;
261
+ try {
262
+ const r = await axios.post(
263
+ `${SUPABASE_URL}/rest/v1/rpc/use_request`,
264
+ { p_key: apiKey },
265
+ { headers: headers(), timeout: 5000 }
266
+ );
267
+ return r.data || null;
268
+ } catch (_) {
269
+ return null; // Network error — allow request to proceed
270
+ }
271
+ }
272
+
273
+ module.exports = {
274
+ isConfigured,
275
+ signUp,
276
+ signIn,
277
+ getSession,
278
+ refreshSession,
279
+ logout,
280
+ getApiKey,
281
+ saveApiKey,
282
+ logUsage,
283
+ loadSession,
284
+ checkApiKey,
285
+ useRequest,
286
+ WEBSITE_URL,
287
+ };