@vruum/skills-operator 0.1.0 → 0.1.1
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/install.js +76 -4
- package/package.json +1 -1
package/install.js
CHANGED
|
@@ -234,6 +234,49 @@ async function discoverAuthServer() {
|
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
async function registerClient(registrationEndpoint, redirectUri) {
|
|
238
|
+
// RFC 7591 Dynamic Client Registration. Supabase's auth server requires a
|
|
239
|
+
// client_id on authorize and token calls; it advertises a
|
|
240
|
+
// registration_endpoint in the OAuth metadata so public CLI apps can
|
|
241
|
+
// register themselves. We register a fresh public client per full auth flow
|
|
242
|
+
// (loopback redirect_uri varies by OS-picked port, so reuse isn't robust)
|
|
243
|
+
// and cache the returned client_id in auth.json for refresh calls.
|
|
244
|
+
const body = JSON.stringify({
|
|
245
|
+
client_name: 'Vruum Operator Skills CLI',
|
|
246
|
+
redirect_uris: [redirectUri],
|
|
247
|
+
token_endpoint_auth_method: 'none',
|
|
248
|
+
grant_types: ['authorization_code', 'refresh_token'],
|
|
249
|
+
response_types: ['code'],
|
|
250
|
+
});
|
|
251
|
+
const parsed = new URL(registrationEndpoint);
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
const req = https.request({
|
|
254
|
+
method: 'POST',
|
|
255
|
+
hostname: parsed.hostname,
|
|
256
|
+
port: parsed.port || 443,
|
|
257
|
+
path: parsed.pathname + parsed.search,
|
|
258
|
+
headers: {
|
|
259
|
+
'Content-Type': 'application/json',
|
|
260
|
+
'Content-Length': Buffer.byteLength(body),
|
|
261
|
+
},
|
|
262
|
+
}, (res) => {
|
|
263
|
+
let data = '';
|
|
264
|
+
res.on('data', (c) => { data += c; });
|
|
265
|
+
res.on('end', () => {
|
|
266
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
267
|
+
try { resolve(JSON.parse(data)); }
|
|
268
|
+
catch { reject(new Error('Registration response is not JSON')); }
|
|
269
|
+
} else {
|
|
270
|
+
reject(new Error(`Client registration returned ${res.statusCode}: ${data}`));
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
req.on('error', reject);
|
|
275
|
+
req.write(body);
|
|
276
|
+
req.end();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
237
280
|
function tryOpenBrowser(urlToOpen) {
|
|
238
281
|
const cmd = process.platform === 'darwin' ? 'open'
|
|
239
282
|
: process.platform === 'win32' ? 'start'
|
|
@@ -251,6 +294,9 @@ async function loopbackAuthFlow() {
|
|
|
251
294
|
if (!meta.authorization_endpoint || !meta.token_endpoint) {
|
|
252
295
|
throw new Error('OAuth metadata missing authorization_endpoint or token_endpoint');
|
|
253
296
|
}
|
|
297
|
+
if (!meta.registration_endpoint) {
|
|
298
|
+
throw new Error('OAuth metadata missing registration_endpoint — cannot register public client');
|
|
299
|
+
}
|
|
254
300
|
|
|
255
301
|
const state = base64UrlEncode(crypto.randomBytes(32));
|
|
256
302
|
const verifier = base64UrlEncode(crypto.randomBytes(32));
|
|
@@ -259,12 +305,27 @@ async function loopbackAuthFlow() {
|
|
|
259
305
|
// Start listener on an OS-picked port.
|
|
260
306
|
return await new Promise((resolve, reject) => {
|
|
261
307
|
const server = http.createServer();
|
|
262
|
-
server.listen(0, '127.0.0.1', () => {
|
|
308
|
+
server.listen(0, '127.0.0.1', async () => {
|
|
263
309
|
const { port } = server.address();
|
|
264
310
|
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
265
311
|
|
|
312
|
+
// Register a fresh public client for this auth flow. Supabase requires
|
|
313
|
+
// exact redirect_uri match, so we can't reuse a cached client_id across
|
|
314
|
+
// runs (the OS-picked port changes). The client_id is cached for
|
|
315
|
+
// refresh-token calls, which don't require a redirect_uri.
|
|
316
|
+
let clientId;
|
|
317
|
+
try {
|
|
318
|
+
const client = await registerClient(meta.registration_endpoint, redirectUri);
|
|
319
|
+
clientId = client.client_id;
|
|
320
|
+
} catch (e) {
|
|
321
|
+
server.close();
|
|
322
|
+
reject(new Error(`OAuth client registration failed: ${e.message}`));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
266
326
|
const authUrl = new URL(meta.authorization_endpoint);
|
|
267
327
|
authUrl.searchParams.set('response_type', 'code');
|
|
328
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
268
329
|
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
269
330
|
authUrl.searchParams.set('state', state);
|
|
270
331
|
authUrl.searchParams.set('code_challenge', challenge);
|
|
@@ -310,11 +371,13 @@ async function loopbackAuthFlow() {
|
|
|
310
371
|
code,
|
|
311
372
|
redirect_uri: redirectUri,
|
|
312
373
|
code_verifier: verifier,
|
|
374
|
+
client_id: clientId,
|
|
313
375
|
});
|
|
314
376
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
315
377
|
res.end('<html><body><h3>Signed in to Vruum.</h3><p>You can close this tab.</p></body></html>');
|
|
316
378
|
clearTimeout(timeout); server.close();
|
|
317
|
-
|
|
379
|
+
// Thread client_id through so writeAuthFile can persist it for refresh calls.
|
|
380
|
+
resolve({ ...tokens, _client_id: clientId });
|
|
318
381
|
} catch (e) {
|
|
319
382
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
320
383
|
res.end(`Token exchange failed: ${e.message}`);
|
|
@@ -370,6 +433,8 @@ function writeAuthFile(tokens) {
|
|
|
370
433
|
const payload = {
|
|
371
434
|
access_token: tokens.access_token,
|
|
372
435
|
refresh_token: tokens.refresh_token,
|
|
436
|
+
// client_id is required on Supabase refresh-token calls for public clients.
|
|
437
|
+
client_id: tokens.client_id || tokens._client_id,
|
|
373
438
|
expires_at: tokens.expires_at || (Math.floor(Date.now() / 1000) + (tokens.expires_in || 3600)),
|
|
374
439
|
token_type: tokens.token_type || 'bearer',
|
|
375
440
|
written_at: nowIso(),
|
|
@@ -390,10 +455,17 @@ async function refreshTokens(auth) {
|
|
|
390
455
|
if (!auth || !auth.refresh_token) return null;
|
|
391
456
|
const meta = await discoverAuthServer();
|
|
392
457
|
try {
|
|
393
|
-
|
|
458
|
+
const params = {
|
|
394
459
|
grant_type: 'refresh_token',
|
|
395
460
|
refresh_token: auth.refresh_token,
|
|
396
|
-
}
|
|
461
|
+
};
|
|
462
|
+
// Supabase requires client_id on public-client refresh calls. Auth files
|
|
463
|
+
// written before the DCR fix won't have it — missing client_id forces a
|
|
464
|
+
// full re-auth via loopbackAuthFlow().
|
|
465
|
+
if (auth.client_id) params.client_id = auth.client_id;
|
|
466
|
+
const tokens = await postTokenRequest(meta.token_endpoint, params);
|
|
467
|
+
// Preserve client_id across refresh writes.
|
|
468
|
+
return { ...tokens, client_id: auth.client_id };
|
|
397
469
|
} catch { return null; }
|
|
398
470
|
}
|
|
399
471
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vruum/skills-operator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OAuth-gated installer for Vruum operator skills. Pulls the operator skill bundle from api.vruum.ai and symlinks it into your AI assistant's skill directory.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|