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 +259 -0
- package/entitlements.js +123 -0
- package/index.js +2353 -0
- package/knowledge.js +228 -0
- package/omni/config.js +51 -0
- package/package.json +49 -0
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
|
+
};
|
package/entitlements.js
ADDED
|
@@ -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 };
|