rampup 0.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.
package/auth.js ADDED
@@ -0,0 +1,259 @@
1
+ /**
2
+ * Ramp CLI Authentication
3
+ * Handles Firebase auth for CLI users
4
+ */
5
+
6
+ import fs from 'fs/promises';
7
+ import path from 'path';
8
+ import os from 'os';
9
+ import open from 'open';
10
+ import http from 'http';
11
+
12
+ const CONFIG_DIR = path.join(os.homedir(), '.ramp');
13
+ const TOKEN_FILE = path.join(CONFIG_DIR, 'credentials.json');
14
+
15
+ // Firebase config (same as web app)
16
+ const FIREBASE_CONFIG = {
17
+ apiKey: 'AIzaSyBVUBNJXtlhuMAOMh-dcqZ-4MwDhgnQBWA',
18
+ authDomain: 'ramp-a2138.firebaseapp.com',
19
+ projectId: 'ramp-a2138',
20
+ };
21
+
22
+ /**
23
+ * Ensure config directory exists
24
+ */
25
+ async function ensureConfigDir() {
26
+ try {
27
+ await fs.mkdir(CONFIG_DIR, { recursive: true });
28
+ } catch (err) {
29
+ if (err.code !== 'EEXIST') throw err;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Save credentials to disk
35
+ */
36
+ async function saveCredentials(credentials) {
37
+ await ensureConfigDir();
38
+ await fs.writeFile(TOKEN_FILE, JSON.stringify(credentials, null, 2), 'utf8');
39
+ await fs.chmod(TOKEN_FILE, 0o600); // Only owner can read/write
40
+ }
41
+
42
+ /**
43
+ * Load saved credentials
44
+ */
45
+ export async function loadCredentials() {
46
+ try {
47
+ const data = await fs.readFile(TOKEN_FILE, 'utf8');
48
+ return JSON.parse(data);
49
+ } catch (err) {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Clear saved credentials
56
+ */
57
+ export async function clearCredentials() {
58
+ try {
59
+ await fs.unlink(TOKEN_FILE);
60
+ } catch (err) {
61
+ // Ignore if file doesn't exist
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if token is expired
67
+ */
68
+ function isTokenExpired(credentials) {
69
+ if (!credentials || !credentials.expiresAt) return true;
70
+ // Add 5 minute buffer
71
+ return Date.now() > credentials.expiresAt - 5 * 60 * 1000;
72
+ }
73
+
74
+ /**
75
+ * Refresh the ID token using refresh token
76
+ */
77
+ async function refreshToken(refreshToken) {
78
+ const response = await fetch(
79
+ `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_CONFIG.apiKey}`,
80
+ {
81
+ method: 'POST',
82
+ headers: { 'Content-Type': 'application/json' },
83
+ body: JSON.stringify({
84
+ grant_type: 'refresh_token',
85
+ refresh_token: refreshToken,
86
+ }),
87
+ }
88
+ );
89
+
90
+ if (!response.ok) {
91
+ throw new Error('Failed to refresh token');
92
+ }
93
+
94
+ const data = await response.json();
95
+ return {
96
+ idToken: data.id_token,
97
+ refreshToken: data.refresh_token,
98
+ expiresAt: Date.now() + parseInt(data.expires_in) * 1000,
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Get a valid ID token, refreshing if necessary
104
+ */
105
+ export async function getIdToken() {
106
+ const credentials = await loadCredentials();
107
+
108
+ if (!credentials) {
109
+ return null;
110
+ }
111
+
112
+ // Check if token needs refresh
113
+ if (isTokenExpired(credentials)) {
114
+ try {
115
+ const newCredentials = await refreshToken(credentials.refreshToken);
116
+ await saveCredentials({
117
+ ...credentials,
118
+ ...newCredentials,
119
+ });
120
+ return newCredentials.idToken;
121
+ } catch (err) {
122
+ // Refresh failed, user needs to login again
123
+ await clearCredentials();
124
+ return null;
125
+ }
126
+ }
127
+
128
+ return credentials.idToken;
129
+ }
130
+
131
+ /**
132
+ * Get user info from saved credentials
133
+ */
134
+ export async function getUserInfo() {
135
+ const credentials = await loadCredentials();
136
+ if (!credentials) return null;
137
+
138
+ return {
139
+ email: credentials.email,
140
+ displayName: credentials.displayName,
141
+ uid: credentials.uid,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Login via browser OAuth flow
147
+ * Opens browser for Google sign-in, receives callback on local server
148
+ */
149
+ export async function loginWithBrowser() {
150
+ return new Promise((resolve, reject) => {
151
+ const PORT = 9876;
152
+
153
+ // Create a simple HTTP server to receive the OAuth callback
154
+ const server = http.createServer(async (req, res) => {
155
+ const url = new URL(req.url, `http://localhost:${PORT}`);
156
+
157
+ if (url.pathname === '/callback') {
158
+ const idToken = url.searchParams.get('idToken');
159
+ const refreshToken = url.searchParams.get('refreshToken');
160
+ const email = url.searchParams.get('email');
161
+ const displayName = url.searchParams.get('displayName');
162
+ const uid = url.searchParams.get('uid');
163
+ const expiresIn = url.searchParams.get('expiresIn');
164
+
165
+ if (idToken && refreshToken) {
166
+ // Save credentials
167
+ await saveCredentials({
168
+ idToken,
169
+ refreshToken,
170
+ email,
171
+ displayName,
172
+ uid,
173
+ expiresAt: Date.now() + parseInt(expiresIn || '3600') * 1000,
174
+ });
175
+
176
+ // Send success page
177
+ res.writeHead(200, { 'Content-Type': 'text/html' });
178
+ res.end(`
179
+ <!DOCTYPE html>
180
+ <html>
181
+ <head>
182
+ <title>Ramp - Login Successful</title>
183
+ <style>
184
+ body {
185
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
186
+ display: flex;
187
+ justify-content: center;
188
+ align-items: center;
189
+ height: 100vh;
190
+ margin: 0;
191
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
192
+ color: white;
193
+ }
194
+ .container {
195
+ text-align: center;
196
+ padding: 40px;
197
+ }
198
+ h1 { color: #4ade80; margin-bottom: 10px; }
199
+ p { color: #94a3b8; }
200
+ .close-hint { margin-top: 20px; font-size: 14px; }
201
+ </style>
202
+ </head>
203
+ <body>
204
+ <div class="container">
205
+ <h1>✓ Login Successful</h1>
206
+ <p>Welcome, ${displayName || email}!</p>
207
+ <p class="close-hint">You can close this window and return to the terminal.</p>
208
+ </div>
209
+ </body>
210
+ </html>
211
+ `);
212
+
213
+ // Close server and resolve
214
+ server.close();
215
+ resolve({ email, displayName, uid });
216
+ } else {
217
+ res.writeHead(400, { 'Content-Type': 'text/html' });
218
+ res.end('<h1>Login Failed</h1><p>Missing credentials</p>');
219
+ server.close();
220
+ reject(new Error('Missing credentials in callback'));
221
+ }
222
+ } else if (url.pathname === '/') {
223
+ // Redirect to Firebase auth
224
+ res.writeHead(302, {
225
+ Location: `https://rampup.dev/cli-auth?redirect=http://localhost:${PORT}/callback`,
226
+ });
227
+ res.end();
228
+ } else {
229
+ res.writeHead(404);
230
+ res.end('Not found');
231
+ }
232
+ });
233
+
234
+ server.listen(PORT, async () => {
235
+ console.log(`\nOpening browser for authentication...`);
236
+ console.log(`If browser doesn't open, visit: http://localhost:${PORT}\n`);
237
+
238
+ try {
239
+ await open(`http://localhost:${PORT}`);
240
+ } catch (err) {
241
+ console.log(`Could not open browser automatically.`);
242
+ }
243
+ });
244
+
245
+ // Timeout after 5 minutes
246
+ setTimeout(() => {
247
+ server.close();
248
+ reject(new Error('Login timed out'));
249
+ }, 5 * 60 * 1000);
250
+ });
251
+ }
252
+
253
+ export default {
254
+ loadCredentials,
255
+ clearCredentials,
256
+ getIdToken,
257
+ getUserInfo,
258
+ loginWithBrowser,
259
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Entitlement Service Client for CLI
3
+ * Checks and burns tokens before AI calls
4
+ */
5
+
6
+ import { getIdToken } from './auth.js';
7
+
8
+ const ENTITLEMENT_API_URL = process.env.ENTITLEMENT_API_URL ||
9
+ 'https://entitlement-service.rian-19c.workers.dev';
10
+
11
+ // Action costs mapping
12
+ const ACTION_COSTS = {
13
+ 'learn': { action: 'chat', credits: 1 },
14
+ 'ask': { action: 'chat', credits: 1 },
15
+ 'guide': { action: 'generate', credits: 2 },
16
+ 'voice': { action: 'chat', credits: 2 },
17
+ 'architect': { action: 'debug', credits: 3 },
18
+ };
19
+
20
+ /**
21
+ * Check and burn tokens for a CLI action
22
+ * Uses saved credentials from ~/.ramp/credentials.json
23
+ */
24
+ export async function checkAndBurnTokens(action, idempotencyKey) {
25
+ // Get token from saved credentials (auto-refreshes if expired)
26
+ const token = await getIdToken();
27
+
28
+ // If no token, allow (user not logged in)
29
+ if (!token) {
30
+ return { allowed: true, reason: 'Not logged in - run `ramp login` for credit tracking' };
31
+ }
32
+
33
+ const mapping = ACTION_COSTS[action] || ACTION_COSTS['ask'];
34
+
35
+ try {
36
+ // Check tokens first
37
+ const checkResponse = await fetch(
38
+ `${ENTITLEMENT_API_URL}/tokens/check?product=ramp&action=${mapping.action}`,
39
+ {
40
+ headers: {
41
+ 'Authorization': `Bearer ${token}`,
42
+ 'Content-Type': 'application/json',
43
+ },
44
+ }
45
+ );
46
+
47
+ if (!checkResponse.ok) {
48
+ const error = await checkResponse.json().catch(() => ({}));
49
+ console.warn('[Entitlement] Check failed:', error.message || checkResponse.status);
50
+ return { allowed: true }; // Fail open
51
+ }
52
+
53
+ const checkResult = await checkResponse.json();
54
+
55
+ if (!checkResult.hasTokens) {
56
+ return {
57
+ allowed: false,
58
+ reason: `Insufficient credits. Need ${checkResult.required}, have ${checkResult.available}. Visit rampup.dev to add more.`,
59
+ };
60
+ }
61
+
62
+ // Burn tokens
63
+ const burnResponse = await fetch(`${ENTITLEMENT_API_URL}/tokens/burn`, {
64
+ method: 'POST',
65
+ headers: {
66
+ 'Authorization': `Bearer ${token}`,
67
+ 'Content-Type': 'application/json',
68
+ },
69
+ body: JSON.stringify({
70
+ product: 'ramp',
71
+ action: mapping.action,
72
+ idempotencyKey,
73
+ }),
74
+ });
75
+
76
+ if (!burnResponse.ok) {
77
+ console.warn('[Entitlement] Burn failed, allowing request');
78
+ return { allowed: true };
79
+ }
80
+
81
+ const burnResult = await burnResponse.json();
82
+
83
+ return {
84
+ allowed: true,
85
+ burned: burnResult.burned,
86
+ balance: burnResult.newBalance,
87
+ };
88
+ } catch (error) {
89
+ console.warn('[Entitlement] Error:', error.message);
90
+ return { allowed: true }; // Fail open
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get current token balance
96
+ */
97
+ export async function getTokenBalance() {
98
+ // Get token from saved credentials (auto-refreshes if expired)
99
+ const token = await getIdToken();
100
+
101
+ if (!token) {
102
+ return null;
103
+ }
104
+
105
+ try {
106
+ const response = await fetch(`${ENTITLEMENT_API_URL}/tokens`, {
107
+ headers: {
108
+ 'Authorization': `Bearer ${token}`,
109
+ 'Content-Type': 'application/json',
110
+ },
111
+ });
112
+
113
+ if (!response.ok) {
114
+ return null;
115
+ }
116
+
117
+ return await response.json();
118
+ } catch (error) {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ export default { checkAndBurnTokens, getTokenBalance };