robot-resources 1.10.5 → 1.10.6
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/lib/auth.mjs +261 -0
- package/lib/config.mjs +55 -0
- package/lib/health-report.js +1 -1
- package/lib/login.mjs +54 -0
- package/lib/wizard.js +1 -1
- package/package.json +1 -2
package/lib/auth.mjs
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
|
|
4
|
+
// Supabase anon key is a public client key (like Stripe's publishable key).
|
|
5
|
+
// Security is enforced by Row Level Security policies, not key secrecy.
|
|
6
|
+
// Override via env vars for alternative Supabase instances.
|
|
7
|
+
const SUPABASE_URL =
|
|
8
|
+
process.env.SUPABASE_URL || 'https://tbnliojrqmcagojtvqpe.supabase.co';
|
|
9
|
+
const SUPABASE_ANON_KEY =
|
|
10
|
+
process.env.SUPABASE_ANON_KEY ||
|
|
11
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InRibmxpb2pycW1jYWdvanR2cXBlIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzMyNjIxNzAsImV4cCI6MjA4ODgzODE3MH0.GKlpbVFgBbcV0OwxFZuOb-LfqtOu95ZiR33KNOONPI0';
|
|
12
|
+
|
|
13
|
+
const PREFERRED_PORT = 54321;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generate PKCE code verifier + challenge
|
|
17
|
+
*/
|
|
18
|
+
function generatePKCE() {
|
|
19
|
+
const verifier = randomBytes(32)
|
|
20
|
+
.toString('base64url')
|
|
21
|
+
.slice(0, 64);
|
|
22
|
+
const challenge = createHash('sha256')
|
|
23
|
+
.update(verifier)
|
|
24
|
+
.digest('base64url');
|
|
25
|
+
return { verifier, challenge };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Build the Supabase OAuth URL for GitHub login
|
|
30
|
+
*/
|
|
31
|
+
export function buildAuthUrl(codeChallenge, callbackUrl) {
|
|
32
|
+
const params = new URLSearchParams({
|
|
33
|
+
provider: 'github',
|
|
34
|
+
redirect_to: callbackUrl,
|
|
35
|
+
flow_type: 'pkce',
|
|
36
|
+
code_challenge: codeChallenge,
|
|
37
|
+
code_challenge_method: 'S256',
|
|
38
|
+
});
|
|
39
|
+
return `${SUPABASE_URL}/auth/v1/authorize?${params}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const MAX_BODY = 8192;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create callback server. Returns { server, resultPromise, nonce }.
|
|
46
|
+
* resultPromise resolves with { type: 'code', code } or { type: 'token', access_token }.
|
|
47
|
+
* The nonce protects /receive-token from cross-origin requests.
|
|
48
|
+
*/
|
|
49
|
+
function createCallbackServer() {
|
|
50
|
+
let resolveResult, rejectResult;
|
|
51
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
52
|
+
resolveResult = resolve;
|
|
53
|
+
rejectResult = reject;
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const nonce = randomBytes(16).toString('hex');
|
|
57
|
+
const tokenPath = `/receive-token/${nonce}`;
|
|
58
|
+
|
|
59
|
+
const timeout = setTimeout(() => {
|
|
60
|
+
server.close();
|
|
61
|
+
rejectResult(new Error('Login timed out after 120 seconds'));
|
|
62
|
+
}, 120_000);
|
|
63
|
+
|
|
64
|
+
const server = createServer((req, res) => {
|
|
65
|
+
const port = server.address()?.port ?? PREFERRED_PORT;
|
|
66
|
+
const url = new URL(req.url, `http://localhost:${port}`);
|
|
67
|
+
|
|
68
|
+
// Handle token/debug info posted from browser (nonce-protected)
|
|
69
|
+
if (url.pathname === tokenPath && req.method === 'POST') {
|
|
70
|
+
let body = '';
|
|
71
|
+
req.on('data', (chunk) => {
|
|
72
|
+
body += chunk;
|
|
73
|
+
if (body.length > MAX_BODY) {
|
|
74
|
+
req.destroy();
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
server.close();
|
|
77
|
+
rejectResult(new Error('Request body too large'));
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
req.on('end', () => {
|
|
81
|
+
res.writeHead(200);
|
|
82
|
+
res.end('ok');
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
server.close();
|
|
85
|
+
|
|
86
|
+
if (body.startsWith('NO_FRAGMENT:')) {
|
|
87
|
+
rejectResult(new Error('No tokens received from browser redirect'));
|
|
88
|
+
} else {
|
|
89
|
+
const params = new URLSearchParams(body);
|
|
90
|
+
const accessToken = params.get('access_token');
|
|
91
|
+
if (accessToken) {
|
|
92
|
+
resolveResult({ type: 'token', access_token: accessToken });
|
|
93
|
+
} else {
|
|
94
|
+
rejectResult(new Error('Unexpected callback data'));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (url.pathname === '/callback') {
|
|
102
|
+
const code = url.searchParams.get('code');
|
|
103
|
+
|
|
104
|
+
if (code) {
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
106
|
+
res.end(`
|
|
107
|
+
<html>
|
|
108
|
+
<body style="background:#0a0a0a;color:#ff6600;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
109
|
+
<div style="text-align:center">
|
|
110
|
+
<h1>■ Robot Resources</h1>
|
|
111
|
+
<p style="color:#00ff41">Login successful. You can close this tab.</p>
|
|
112
|
+
</div>
|
|
113
|
+
</body>
|
|
114
|
+
</html>
|
|
115
|
+
`);
|
|
116
|
+
clearTimeout(timeout);
|
|
117
|
+
server.close();
|
|
118
|
+
resolveResult({ type: 'code', code });
|
|
119
|
+
} else {
|
|
120
|
+
// No code in query params — serve page that captures the full URL
|
|
121
|
+
// (fragment tokens, errors, etc.) and sends it back via nonce-protected endpoint
|
|
122
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
123
|
+
res.end(`
|
|
124
|
+
<html>
|
|
125
|
+
<body style="background:#0a0a0a;color:#ff6600;font-family:monospace;display:flex;align-items:center;justify-content:center;height:100vh;margin:0">
|
|
126
|
+
<div style="text-align:center">
|
|
127
|
+
<h1>■ Robot Resources</h1>
|
|
128
|
+
<p id="msg">Completing login...</p>
|
|
129
|
+
</div>
|
|
130
|
+
</body>
|
|
131
|
+
<script>
|
|
132
|
+
const fullUrl = window.location.href;
|
|
133
|
+
const hash = window.location.hash.substring(1);
|
|
134
|
+
const payload = hash || 'NO_FRAGMENT:' + fullUrl;
|
|
135
|
+
fetch('${tokenPath}', { method: 'POST', body: payload })
|
|
136
|
+
.then(() => {
|
|
137
|
+
if (hash && hash.includes('access_token')) {
|
|
138
|
+
document.getElementById('msg').style.color = '#00ff41';
|
|
139
|
+
document.getElementById('msg').textContent = 'Login successful. You can close this tab.';
|
|
140
|
+
} else {
|
|
141
|
+
document.getElementById('msg').style.color = '#ffaa00';
|
|
142
|
+
document.getElementById('msg').textContent = 'Something went wrong. Check terminal.';
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
</script>
|
|
146
|
+
</html>
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Reject all other paths
|
|
153
|
+
res.writeHead(404);
|
|
154
|
+
res.end();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
server.on('error', (err) => {
|
|
158
|
+
clearTimeout(timeout);
|
|
159
|
+
rejectResult(new Error(`Could not start callback server: ${err.message}`));
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return { server, resultPromise, nonce };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Exchange the auth code + PKCE verifier for a Supabase session.
|
|
167
|
+
*/
|
|
168
|
+
async function exchangeCodeForSession(code, codeVerifier) {
|
|
169
|
+
const res = await fetch(`${SUPABASE_URL}/auth/v1/token?grant_type=pkce`, {
|
|
170
|
+
method: 'POST',
|
|
171
|
+
headers: {
|
|
172
|
+
'Content-Type': 'application/json',
|
|
173
|
+
apikey: SUPABASE_ANON_KEY,
|
|
174
|
+
},
|
|
175
|
+
body: JSON.stringify({
|
|
176
|
+
auth_code: code,
|
|
177
|
+
code_verifier: codeVerifier,
|
|
178
|
+
}),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
const body = await res.text();
|
|
183
|
+
throw new Error(`Token exchange failed (${res.status}): ${body}`);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return res.json();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Try to listen on the preferred port, fall back to OS-assigned port.
|
|
191
|
+
*/
|
|
192
|
+
function listenWithFallback(server) {
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
server.once('error', (err) => {
|
|
195
|
+
if (err.code === 'EADDRINUSE') {
|
|
196
|
+
// Preferred port busy — let OS assign one
|
|
197
|
+
server.listen(0, '127.0.0.1', () => resolve(server.address().port));
|
|
198
|
+
} else {
|
|
199
|
+
reject(err);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
server.listen(PREFERRED_PORT, '127.0.0.1', () => resolve(server.address().port));
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Full OAuth flow with PKCE + implicit fallback.
|
|
208
|
+
* Returns { access_token, refresh_token, user }.
|
|
209
|
+
*/
|
|
210
|
+
export async function authenticate() {
|
|
211
|
+
const { verifier, challenge } = generatePKCE();
|
|
212
|
+
|
|
213
|
+
// Create server and wait for it to be listening
|
|
214
|
+
const { server, resultPromise } = createCallbackServer();
|
|
215
|
+
|
|
216
|
+
const port = await listenWithFallback(server);
|
|
217
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
218
|
+
const authUrl = buildAuthUrl(challenge, callbackUrl);
|
|
219
|
+
|
|
220
|
+
console.log(`\n Auth URL: ${authUrl}\n`);
|
|
221
|
+
|
|
222
|
+
// Open browser (use execFile to avoid shell injection)
|
|
223
|
+
const { execFile } = await import('node:child_process');
|
|
224
|
+
if (process.platform === 'win32') {
|
|
225
|
+
// 'start' is a cmd.exe builtin, not an executable — must invoke via cmd.exe
|
|
226
|
+
execFile('cmd.exe', ['/c', 'start', '""', authUrl]);
|
|
227
|
+
} else {
|
|
228
|
+
const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
229
|
+
execFile(openCmd, [authUrl]);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(' Waiting for GitHub authorization...\n');
|
|
233
|
+
|
|
234
|
+
// Wait for the callback
|
|
235
|
+
const result = await resultPromise;
|
|
236
|
+
|
|
237
|
+
if (result.type === 'code') {
|
|
238
|
+
// PKCE flow — exchange code for session
|
|
239
|
+
const session = await exchangeCodeForSession(result.code, verifier);
|
|
240
|
+
return {
|
|
241
|
+
access_token: session.access_token,
|
|
242
|
+
refresh_token: session.refresh_token,
|
|
243
|
+
user: session.user,
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
// Implicit flow fallback — we have the access_token directly
|
|
247
|
+
const res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
|
248
|
+
headers: {
|
|
249
|
+
apikey: SUPABASE_ANON_KEY,
|
|
250
|
+
Authorization: `Bearer ${result.access_token}`,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
if (!res.ok) throw new Error('Failed to fetch user info');
|
|
254
|
+
const user = await res.json();
|
|
255
|
+
return {
|
|
256
|
+
access_token: result.access_token,
|
|
257
|
+
refresh_token: null,
|
|
258
|
+
user,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
package/lib/config.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = join(homedir(), '.robot-resources');
|
|
6
|
+
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
7
|
+
|
|
8
|
+
export function getConfigPath() {
|
|
9
|
+
return CONFIG_FILE;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getConfigDir() {
|
|
13
|
+
return CONFIG_DIR;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readConfig() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
|
|
19
|
+
} catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function writeConfig(data) {
|
|
25
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
26
|
+
const existing = readConfig();
|
|
27
|
+
const merged = { ...existing, ...data };
|
|
28
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
29
|
+
return merged;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function readProviderKeys() {
|
|
33
|
+
const config = readConfig();
|
|
34
|
+
return config.provider_keys || {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeProviderKeys(keys) {
|
|
38
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
39
|
+
const existing = readConfig();
|
|
40
|
+
const existingProviderKeys = existing.provider_keys || {};
|
|
41
|
+
const merged = {
|
|
42
|
+
...existing,
|
|
43
|
+
provider_keys: { ...existingProviderKeys, ...keys },
|
|
44
|
+
};
|
|
45
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2) + '\n', { mode: 0o600 });
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function clearConfig() {
|
|
50
|
+
try {
|
|
51
|
+
writeFileSync(CONFIG_FILE, '{}\n', { mode: 0o600 });
|
|
52
|
+
} catch {
|
|
53
|
+
// config file doesn't exist, that's fine
|
|
54
|
+
}
|
|
55
|
+
}
|
package/lib/health-report.js
CHANGED
package/lib/login.mjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { authenticate } from './auth.mjs';
|
|
2
|
+
import { writeConfig, getConfigPath } from './config.mjs';
|
|
3
|
+
|
|
4
|
+
const PLATFORM_URL = process.env.RR_PLATFORM_URL || 'https://api.robotresources.ai';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate an API key via the platform API.
|
|
8
|
+
*/
|
|
9
|
+
export async function createApiKey(accessToken) {
|
|
10
|
+
const res = await fetch(`${PLATFORM_URL}/v1/keys`, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
Authorization: `Bearer ${accessToken}`,
|
|
15
|
+
},
|
|
16
|
+
body: JSON.stringify({ name: `cli-${new Date().toISOString().slice(0, 10)}` }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const body = await res.text();
|
|
21
|
+
throw new Error(`Failed to create API key (${res.status}): ${body}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { data } = await res.json();
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function login() {
|
|
29
|
+
console.log('\n ██ Robot Resources — Login\n');
|
|
30
|
+
console.log(' Opening GitHub in your browser...');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { access_token, user } = await authenticate();
|
|
34
|
+
|
|
35
|
+
console.log(` ✓ Authenticated as ${user.user_metadata?.user_name || user.email}`);
|
|
36
|
+
console.log(' Generating API key...');
|
|
37
|
+
|
|
38
|
+
const key = await createApiKey(access_token);
|
|
39
|
+
|
|
40
|
+
writeConfig({
|
|
41
|
+
api_key: key.key,
|
|
42
|
+
key_id: key.id,
|
|
43
|
+
key_name: key.name,
|
|
44
|
+
user_email: user.email,
|
|
45
|
+
user_name: user.user_metadata?.user_name || null,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(` ✓ API key saved to ${getConfigPath()}`);
|
|
49
|
+
console.log(`\n You're all set. Router and Scraper will pick up the key automatically.\n`);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.error(`\n ✗ Login failed: ${err.message}\n`);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
}
|
package/lib/wizard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { homedir, hostname, release as osRelease } from 'node:os';
|
|
4
|
-
import { readConfig, writeConfig } from '
|
|
4
|
+
import { readConfig, writeConfig } from './config.mjs';
|
|
5
5
|
import { isOpenClawInstalled } from './detect.js';
|
|
6
6
|
import { getOrCreateMachineId } from './machine-id.js';
|
|
7
7
|
import { configureToolRouting, registerScraperMcp, restartOpenClawGateway } from './tool-config.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "robot-resources",
|
|
3
|
-
"version": "1.10.
|
|
3
|
+
"version": "1.10.6",
|
|
4
4
|
"description": "Robot Resources — AI agent tools. One command to install everything.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@robot-resources/cli-core": "*",
|
|
21
20
|
"@robot-resources/router": "*",
|
|
22
21
|
"@robot-resources/scraper": "*"
|
|
23
22
|
},
|