mirror of
https://github.com/codeaashu/claude-code.git
synced 2026-04-08 22:28:48 +03:00
claude-code
This commit is contained in:
184
src/commands/remote-setup/api.ts
Normal file
184
src/commands/remote-setup/api.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import axios from 'axios'
|
||||
import { getOauthConfig } from '../../constants/oauth.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { getOAuthHeaders, prepareApiRequest } from '../../utils/teleport/api.js'
|
||||
import { fetchEnvironments } from '../../utils/teleport/environments.js'
|
||||
|
||||
const CCR_BYOC_BETA_HEADER = 'ccr-byoc-2025-07-29'
|
||||
|
||||
/**
|
||||
* Wraps a raw GitHub token so that its string representation is redacted.
|
||||
* `String(token)`, template literals, `JSON.stringify(token)`, and any
|
||||
* attached error messages will show `[REDACTED:gh-token]` instead of the
|
||||
* token value. Call `.reveal()` only at the single point where the raw
|
||||
* value is placed into an HTTP body.
|
||||
*/
|
||||
export class RedactedGithubToken {
|
||||
readonly #value: string
|
||||
constructor(raw: string) {
|
||||
this.#value = raw
|
||||
}
|
||||
reveal(): string {
|
||||
return this.#value
|
||||
}
|
||||
toString(): string {
|
||||
return '[REDACTED:gh-token]'
|
||||
}
|
||||
toJSON(): string {
|
||||
return '[REDACTED:gh-token]'
|
||||
}
|
||||
[Symbol.for('nodejs.util.inspect.custom')](): string {
|
||||
return '[REDACTED:gh-token]'
|
||||
}
|
||||
}
|
||||
|
||||
export type ImportTokenResult = {
|
||||
github_username: string
|
||||
}
|
||||
|
||||
export type ImportTokenError =
|
||||
| { kind: 'not_signed_in' }
|
||||
| { kind: 'invalid_token' }
|
||||
| { kind: 'server'; status: number }
|
||||
| { kind: 'network' }
|
||||
|
||||
/**
|
||||
* POSTs a GitHub token to the CCR backend, which validates it against
|
||||
* GitHub's /user endpoint and stores it Fernet-encrypted in sync_user_tokens.
|
||||
* The stored token satisfies the same read paths as an OAuth token, so
|
||||
* clone/push in claude.ai/code works immediately after this succeeds.
|
||||
*/
|
||||
export async function importGithubToken(
|
||||
token: RedactedGithubToken,
|
||||
): Promise<
|
||||
| { ok: true; result: ImportTokenResult }
|
||||
| { ok: false; error: ImportTokenError }
|
||||
> {
|
||||
let accessToken: string, orgUUID: string
|
||||
try {
|
||||
;({ accessToken, orgUUID } = await prepareApiRequest())
|
||||
} catch {
|
||||
return { ok: false, error: { kind: 'not_signed_in' } }
|
||||
}
|
||||
|
||||
const url = `${getOauthConfig().BASE_API_URL}/v1/code/github/import-token`
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'anthropic-beta': CCR_BYOC_BETA_HEADER,
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post<ImportTokenResult>(
|
||||
url,
|
||||
{ token: token.reveal() },
|
||||
{ headers, timeout: 15000, validateStatus: () => true },
|
||||
)
|
||||
if (response.status === 200) {
|
||||
return { ok: true, result: response.data }
|
||||
}
|
||||
if (response.status === 400) {
|
||||
return { ok: false, error: { kind: 'invalid_token' } }
|
||||
}
|
||||
if (response.status === 401) {
|
||||
return { ok: false, error: { kind: 'not_signed_in' } }
|
||||
}
|
||||
logForDebugging(`import-token returned ${response.status}`, {
|
||||
level: 'error',
|
||||
})
|
||||
return { ok: false, error: { kind: 'server', status: response.status } }
|
||||
} catch (err) {
|
||||
if (axios.isAxiosError(err)) {
|
||||
// err.config.data would contain the POST body with the raw token.
|
||||
// Do not include it in any log. The error code alone is enough.
|
||||
logForDebugging(`import-token network error: ${err.code ?? 'unknown'}`, {
|
||||
level: 'error',
|
||||
})
|
||||
}
|
||||
return { ok: false, error: { kind: 'network' } }
|
||||
}
|
||||
}
|
||||
|
||||
async function hasExistingEnvironment(): Promise<boolean> {
|
||||
try {
|
||||
const envs = await fetchEnvironments()
|
||||
return envs.length > 0
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort default environment creation. Mirrors the web onboarding's
|
||||
* DEFAULT_CLOUD_ENVIRONMENT_REQUEST so a first-time user lands on the
|
||||
* composer instead of env-setup. Checks for existing environments first
|
||||
* so re-running /web-setup doesn't pile up duplicates. Failures are
|
||||
* non-fatal — the token import already succeeded, and the web state
|
||||
* machine falls back to env-setup on next load.
|
||||
*/
|
||||
export async function createDefaultEnvironment(): Promise<boolean> {
|
||||
let accessToken: string, orgUUID: string
|
||||
try {
|
||||
;({ accessToken, orgUUID } = await prepareApiRequest())
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
if (await hasExistingEnvironment()) {
|
||||
return true
|
||||
}
|
||||
|
||||
// The /private/organizations/{org}/ path rejects CLI OAuth tokens (wrong
|
||||
// auth dep). The public path uses build_flexible_auth — same path
|
||||
// fetchEnvironments() uses. Org is passed via x-organization-uuid header.
|
||||
const url = `${getOauthConfig().BASE_API_URL}/v1/environment_providers/cloud/create`
|
||||
const headers = {
|
||||
...getOAuthHeaders(accessToken),
|
||||
'x-organization-uuid': orgUUID,
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
url,
|
||||
{
|
||||
name: 'Default',
|
||||
kind: 'anthropic_cloud',
|
||||
description: 'Default - trusted network access',
|
||||
config: {
|
||||
environment_type: 'anthropic',
|
||||
cwd: '/home/user',
|
||||
init_script: null,
|
||||
environment: {},
|
||||
languages: [
|
||||
{ name: 'python', version: '3.11' },
|
||||
{ name: 'node', version: '20' },
|
||||
],
|
||||
network_config: {
|
||||
allowed_hosts: [],
|
||||
allow_default_hosts: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{ headers, timeout: 15000, validateStatus: () => true },
|
||||
)
|
||||
return response.status >= 200 && response.status < 300
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns true when the user has valid Claude OAuth credentials. */
|
||||
export async function isSignedIn(): Promise<boolean> {
|
||||
try {
|
||||
await prepareApiRequest()
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getCodeWebUrl(): string {
|
||||
return `${getOauthConfig().CLAUDE_AI_ORIGIN}/code`
|
||||
}
|
||||
|
||||
|
||||
22
src/commands/remote-setup/index.ts
Normal file
22
src/commands/remote-setup/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Command } from '../../commands.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { isPolicyAllowed } from '../../services/policyLimits/index.js'
|
||||
|
||||
const web = {
|
||||
type: 'local-jsx',
|
||||
name: 'web-setup',
|
||||
description:
|
||||
'Setup Claude Code on the web (requires connecting your GitHub account)',
|
||||
availability: ['claude-ai'],
|
||||
isEnabled: () =>
|
||||
getFeatureValue_CACHED_MAY_BE_STALE('tengu_cobalt_lantern', false) &&
|
||||
isPolicyAllowed('allow_remote_sessions'),
|
||||
get isHidden() {
|
||||
return !isPolicyAllowed('allow_remote_sessions')
|
||||
},
|
||||
load: () => import('./remote-setup.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default web
|
||||
|
||||
|
||||
188
src/commands/remote-setup/remote-setup.tsx
Normal file
188
src/commands/remote-setup/remote-setup.tsx
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user