app
Gallia Bundle
UnreviewedAdom platform bundle for Hydrogen Desktop. Built by adom-inc/gallia-hd-bundle from upstream adom-inc/gallia — strips legacy-container cleanup, drops bootstrap.sh, ships 117 skills + cleaned-down insta
#!/usr/bin/env node
/**
* Authenticate Claude Code in container environments.
*
* The standard OAuth redirect-to-localhost flow doesn't work in containers.
* This script does the PKCE flow manually:
* 1. Generates a PKCE challenge
* 2. Prints an auth URL for the user to open
* 3. Prompts the user to paste the code they receive
* 4. Tries to exchange the code for tokens server-side (multiple endpoints)
* 5. If Cloudflare blocks all endpoints, offers fallback methods:
* a) Browser console snippet (paste on claude.ai — works for everyone)
* b) Existing token from `claude setup-token` on another machine
* c) Anthropic API key from console.anthropic.com
* 6. Writes ~/.claude/.credentials.json
*/
import { createHash, randomBytes } from 'crypto';
import { request } from 'https';
import { createInterface } from 'readline';
import { mkdirSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
const REDIRECT_URI = 'https://platform.claude.com/oauth/code/callback';
const SCOPES = 'org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers';
const CRED_PATH = join(homedir(), '.claude', '.credentials.json');
// Generate PKCE verifier + challenge
const verifier = randomBytes(48).toString('base64url').slice(0, 64);
const challenge = createHash('sha256').update(verifier).digest('base64url');
const state = randomBytes(24).toString('base64url');
// Build auth URL
const params = new URLSearchParams({
code: 'true',
client_id: CLIENT_ID,
response_type: 'code',
redirect_uri: REDIRECT_URI,
scope: SCOPES,
code_challenge: challenge,
code_challenge_method: 'S256',
state: state,
});
const authUrl = `https://claude.ai/oauth/authorize?${params}`;
console.log('');
console.log(' Open this URL in your browser and sign in:');
console.log('');
console.log(` ${authUrl}`);
console.log('');
console.log(' After signing in, you\'ll see a code to copy.');
console.log('');
const rl = createInterface({ input: process.stdin, output: process.stdout });
function ask(question) {
return new Promise((resolve) => rl.question(question, resolve));
}
const rawCode = await ask(' Paste the code here: ');
// The pasted code may include #<state> appended — strip it
const code = rawCode.trim().split('#')[0];
if (!code) {
console.error(' No code entered. Aborting.');
process.exit(1);
}
console.log('');
console.log(' Exchanging code for tokens...');
// JSON body — must match what the CLI sends (axios POST with Content-Type: application/json)
const tokenBody = JSON.stringify({
grant_type: 'authorization_code',
code,
redirect_uri: REDIRECT_URI,
client_id: CLIENT_ID,
code_verifier: verifier,
state,
});
// Try multiple hostnames and paths — Cloudflare config varies by domain
const endpoints = [
{ hostname: 'platform.claude.com', path: '/v1/oauth/token' },
];
let success = false;
for (const ep of endpoints) {
const result = await tryEndpoint(ep.hostname, ep.path, tokenBody);
if (result === 'success') {
success = true;
break;
}
if (result === 'auth_error') {
process.exit(1);
}
}
if (!success) {
console.log('');
console.log(' Server-side token exchange blocked. Choose a method:');
console.log('');
console.log(' 1) Browser console — paste a snippet on claude.ai');
console.log(' 2) Existing token — from `claude setup-token` on another machine');
console.log(' 3) API key — from console.anthropic.com/settings/keys');
console.log('');
const choice = (await ask(' Enter 1, 2, or 3: ')).trim();
if (choice === '1') {
await browserConsoleFallback();
} else if (choice === '2') {
await existingTokenFallback();
} else if (choice === '3') {
await apiKeyFallback();
} else {
console.error(' Invalid choice. Run this script again to retry.');
process.exit(1);
}
}
rl.close();
// ── Fallback methods ─────────────────────────────────────────
async function browserConsoleFallback() {
console.log('');
console.log(' In your browser, stay on the platform.claude.com page where you got the code.');
console.log(' Open the browser console (F12 → Console tab) and paste this:');
console.log('');
const snippet = `fetch('/v1/oauth/token',{method:'POST',headers:{'Content-Type':'application/json','Accept':'application/json'},body:JSON.stringify({grant_type:'authorization_code',code:${JSON.stringify(code)},redirect_uri:${JSON.stringify(REDIRECT_URI)},client_id:${JSON.stringify(CLIENT_ID)},code_verifier:${JSON.stringify(verifier)},state:${JSON.stringify(state)}})}).then(r=>r.json()).then(t=>{if(t.error){console.error('Error:',t.error,t.error_description);return}const c=JSON.stringify({claudeAiOauth:{accessToken:t.access_token,refreshToken:t.refresh_token||'',expiresAt:Date.now()+(t.expires_in||3600)*1e3,scopes:(t.scope||'').split(' '),subscriptionType:null,rateLimitTier:null}});copy(c);console.log('Credentials copied to clipboard!')}).catch(e=>console.error('Failed:',e))`;
console.log(` ${snippet}`);
console.log('');
console.log(' It will copy the credentials to your clipboard.');
console.log('');
const credJson = (await ask(' Paste the credentials here: ')).trim();
if (!credJson) {
console.error(' No credentials entered.');
process.exit(1);
}
try {
const parsed = JSON.parse(credJson);
if (!parsed.claudeAiOauth?.accessToken) throw new Error('Missing accessToken');
writeCreds(parsed);
console.log(' Authenticated successfully.');
} catch (e) {
console.error(' Invalid credentials JSON:', e.message);
process.exit(1);
}
}
async function existingTokenFallback() {
console.log('');
console.log(' On a machine where Claude Code is already authenticated, run:');
console.log('');
console.log(' claude setup-token');
console.log('');
console.log(' Copy the full token (starts with sk-ant-oat01-).');
console.log(' Make sure to include the entire string if it wraps to two lines.');
console.log('');
const token = (await ask(' Paste the token here: ')).trim();
if (!token) {
console.error(' No token entered.');
process.exit(1);
}
if (!token.startsWith('sk-ant-')) {
console.error(' Token should start with sk-ant-. Got:', token.slice(0, 20));
process.exit(1);
}
writeCreds({
claudeAiOauth: {
accessToken: token,
refreshToken: '',
expiresAt: Date.now() + 90 * 24 * 3600 * 1000, // 90 days
scopes: ['user:inference', 'user:mcp_servers', 'user:profile', 'user:sessions:claude_code'],
subscriptionType: null,
rateLimitTier: null,
},
});
console.log(' Authenticated successfully.');
}
async function apiKeyFallback() {
console.log('');
console.log(' Go to https://console.anthropic.com/settings/keys');
console.log(' Create an API key (starts with sk-ant-api03-).');
console.log('');
const apiKey = (await ask(' Paste your API key here: ')).trim();
if (!apiKey) {
console.error(' No API key entered.');
process.exit(1);
}
// API keys go in the environment, not .credentials.json
const bashrc = join(homedir(), '.bashrc');
const exportLine = `export ANTHROPIC_API_KEY='${apiKey}'`;
// Add to .bashrc if not already there
const existing = existsSync(bashrc)
? (await import('fs')).readFileSync(bashrc, 'utf-8')
: '';
if (!existing.includes('ANTHROPIC_API_KEY')) {
writeFileSync(bashrc, existing + '\n' + exportLine + '\n');
}
// Also set in current process for verification
process.env.ANTHROPIC_API_KEY = apiKey;
console.log(' API key saved to ~/.bashrc');
console.log(' It will be active after reloading VS Code.');
console.log(' Authenticated successfully.');
}
// ── Helpers ──────────────────────────────────────────────────
function writeCreds(creds) {
const claudeDir = join(homedir(), '.claude');
mkdirSync(claudeDir, { recursive: true });
writeFileSync(CRED_PATH, JSON.stringify(creds), { mode: 0o600 });
}
function tryEndpoint(hostname, path, body) {
return new Promise((resolve) => {
const req = request({
hostname,
path,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(body),
'Accept': 'application/json',
},
}, (res) => {
let data = '';
res.on('data', (d) => data += d);
res.on('end', () => {
if (data.includes('challenge-platform') || data.includes('Just a moment')) {
console.log(` ${hostname}${path} — blocked by Cloudflare`);
resolve('blocked');
return;
}
try {
const r = JSON.parse(data);
if (r.error) {
const errType = typeof r.error === 'object' ? r.error.type || JSON.stringify(r.error) : r.error;
const errMsg = typeof r.error === 'object' ? r.error.message : r.error_description;
console.error(` ${hostname}${path} — ${errType}: ${errMsg || ''}`);
resolve(errType === 'invalid_grant' ? 'auth_error' : 'error');
return;
}
if (!r.access_token) {
console.log(` ${hostname}${path} — no access_token in response`);
resolve('error');
return;
}
writeCreds({
claudeAiOauth: {
accessToken: r.access_token,
refreshToken: r.refresh_token || '',
expiresAt: Date.now() + (r.expires_in || 3600) * 1000,
scopes: typeof r.scope === 'string' ? r.scope.split(' ') : (r.scope || []),
subscriptionType: null,
rateLimitTier: null,
},
});
console.log(' Authenticated successfully.');
resolve('success');
} catch (e) {
console.log(` ${hostname}${path} — unexpected response`);
resolve('error');
}
});
});
req.on('error', (e) => {
console.log(` ${hostname}${path} — ${e.message}`);
resolve('error');
});
req.write(body);
req.end();
});
}