@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.
Files changed (2) hide show
  1. package/install.js +76 -4
  2. 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
- resolve(tokens);
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
- return await postTokenRequest(meta.token_endpoint, {
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.0",
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": {