mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
feat: wire gbrain auth subcommand with OAuth register-client
Previously auth.ts was a standalone script invoked via
`bun run src/commands/auth.ts`. CHANGELOG and README documented
`gbrain auth ...` commands that didn't actually work.
- Export `runAuth(args)` from auth.ts (keeps standalone entry intact
via `import.meta.url === file://${process.argv[1]}` check)
- Add `auth` to CLI_ONLY + dispatch in handleCliOnly
- New subcommand `gbrain auth register-client <name> [--grant-types]
[--scopes]` wraps GBrainOAuthProvider.registerClientManual
- Lazy DB check: only subcommands that need DATABASE_URL error out
Now the documented CLI flow works end to end:
gbrain auth register-client perplexity --grant-types client_credentials --scopes "read write"
gbrain serve --http --port 3131
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
5b0e61196a
commit
c4a86cec89
+6
-1
@@ -19,7 +19,7 @@ for (const op of operations) {
|
||||
}
|
||||
|
||||
// CLI-only commands that bypass the operation layer
|
||||
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'sources', 'dream', 'check-resolvable']);
|
||||
const CLI_ONLY = new Set(['init', 'upgrade', 'post-upgrade', 'check-update', 'integrations', 'publish', 'check-backlinks', 'lint', 'report', 'import', 'export', 'files', 'embed', 'serve', 'call', 'config', 'doctor', 'migrate', 'eval', 'sync', 'extract', 'features', 'autopilot', 'graph-query', 'jobs', 'agent', 'apply-migrations', 'skillpack-check', 'resolvers', 'integrity', 'repair-jsonb', 'orphans', 'sources', 'dream', 'check-resolvable', 'auth']);
|
||||
|
||||
async function main() {
|
||||
// Parse global flags (--quiet / --progress-json / --progress-interval)
|
||||
@@ -265,6 +265,11 @@ async function handleCliOnly(command: string, args: string[]) {
|
||||
await runInit(args);
|
||||
return;
|
||||
}
|
||||
if (command === 'auth') {
|
||||
const { runAuth } = await import('./commands/auth.ts');
|
||||
await runAuth(args);
|
||||
return;
|
||||
}
|
||||
if (command === 'upgrade') {
|
||||
const { runUpgrade } = await import('./commands/upgrade.ts');
|
||||
await runUpgrade(args);
|
||||
|
||||
+76
-21
@@ -12,9 +12,15 @@ import postgres from 'postgres';
|
||||
import { createHash, randomBytes } from 'crypto';
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL || process.env.GBRAIN_DATABASE_URL;
|
||||
if (!DATABASE_URL && process.argv[2] !== 'test') {
|
||||
console.error('Set DATABASE_URL or GBRAIN_DATABASE_URL environment variable.');
|
||||
process.exit(1);
|
||||
|
||||
/** Subcommands that need a DB connection. `test` (smoke test) does not. */
|
||||
const NEEDS_DB = new Set(['create', 'list', 'revoke', 'register-client']);
|
||||
|
||||
function ensureDatabaseUrl(cmd: string | undefined) {
|
||||
if (cmd && NEEDS_DB.has(cmd) && !DATABASE_URL) {
|
||||
console.error('Set DATABASE_URL or GBRAIN_DATABASE_URL environment variable.');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function hashToken(token: string): string {
|
||||
@@ -216,26 +222,75 @@ async function test(url: string, token: string) {
|
||||
console.log(`\n🧠 Your brain is live! (${elapsed}s)`);
|
||||
}
|
||||
|
||||
// CLI dispatch
|
||||
const [cmd, ...args] = process.argv.slice(2);
|
||||
switch (cmd) {
|
||||
case 'create': await create(args[0]); break;
|
||||
case 'list': await list(); break;
|
||||
case 'revoke': await revoke(args[0]); break;
|
||||
case 'test': {
|
||||
const tokenIdx = args.indexOf('--token');
|
||||
const url = args.find(a => !a.startsWith('--') && a !== args[tokenIdx + 1]);
|
||||
const token = tokenIdx >= 0 ? args[tokenIdx + 1] : '';
|
||||
await test(url || '', token || '');
|
||||
break;
|
||||
async function registerClient(name: string, args: string[]) {
|
||||
if (!name) { console.error('Usage: auth register-client <name> [--grant-types G] [--scopes S]'); process.exit(1); }
|
||||
const grantsIdx = args.indexOf('--grant-types');
|
||||
const scopesIdx = args.indexOf('--scopes');
|
||||
const grantTypes = grantsIdx >= 0 && args[grantsIdx + 1]
|
||||
? args[grantsIdx + 1].split(',').map(s => s.trim()).filter(Boolean)
|
||||
: ['client_credentials'];
|
||||
const scopes = scopesIdx >= 0 && args[scopesIdx + 1] ? args[scopesIdx + 1] : 'read';
|
||||
|
||||
const sql = postgres(DATABASE_URL!);
|
||||
try {
|
||||
const { GBrainOAuthProvider } = await import('../core/oauth-provider.ts');
|
||||
const provider = new GBrainOAuthProvider({ sql: sql as any });
|
||||
const { clientId, clientSecret } = await provider.registerClientManual(
|
||||
name, grantTypes, scopes, [],
|
||||
);
|
||||
console.log(`OAuth client registered: "${name}"\n`);
|
||||
console.log(` Client ID: ${clientId}`);
|
||||
console.log(` Client Secret: ${clientSecret}\n`);
|
||||
console.log(` Grant types: ${grantTypes.join(', ')}`);
|
||||
console.log(` Scopes: ${scopes}\n`);
|
||||
console.log('Save the client secret — it will not be shown again.');
|
||||
console.log(`Revoke with: bun run src/commands/auth.ts revoke-client "${clientId}"`);
|
||||
} catch (e: any) {
|
||||
console.error('Error:', e.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
await sql.end();
|
||||
}
|
||||
default:
|
||||
console.log(`GBrain Token Management
|
||||
}
|
||||
|
||||
function printHelp() {
|
||||
console.log(`GBrain Token Management
|
||||
|
||||
Usage:
|
||||
bun run src/commands/auth.ts create <name> Create a new access token
|
||||
bun run src/commands/auth.ts list List all tokens
|
||||
bun run src/commands/auth.ts revoke <name> Revoke a token
|
||||
bun run src/commands/auth.ts test <url> --token <token> Smoke test a remote MCP server
|
||||
gbrain auth create <name> Create a legacy bearer token
|
||||
gbrain auth list List all tokens
|
||||
gbrain auth revoke <name> Revoke a legacy token
|
||||
gbrain auth register-client <name> [options] Register an OAuth 2.1 client
|
||||
--grant-types <client_credentials,authorization_code> (default: client_credentials)
|
||||
--scopes "<read write admin>" (default: read)
|
||||
gbrain auth test <url> --token <token> Smoke test a remote MCP server
|
||||
`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked by src/cli.ts for `gbrain auth <subcommand>` and by direct
|
||||
* `bun run src/commands/auth.ts <subcommand>` (legacy path).
|
||||
*/
|
||||
export async function runAuth(args: string[]) {
|
||||
const [cmd, ...rest] = args;
|
||||
ensureDatabaseUrl(cmd);
|
||||
switch (cmd) {
|
||||
case 'create': await create(rest[0]); break;
|
||||
case 'list': await list(); break;
|
||||
case 'revoke': await revoke(rest[0]); break;
|
||||
case 'register-client': await registerClient(rest[0], rest.slice(1)); break;
|
||||
case 'test': {
|
||||
const tokenIdx = rest.indexOf('--token');
|
||||
const url = rest.find(a => !a.startsWith('--') && a !== rest[tokenIdx + 1]);
|
||||
const token = tokenIdx >= 0 ? rest[tokenIdx + 1] : '';
|
||||
await test(url || '', token || '');
|
||||
break;
|
||||
}
|
||||
default: printHelp();
|
||||
}
|
||||
}
|
||||
|
||||
// Direct invocation (`bun run src/commands/auth.ts ...`). Skip when imported.
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
await runAuth(process.argv.slice(2));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user