forked from sanbuphy/learn-coding-agent
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi.ts
More file actions
182 lines (168 loc) · 5.39 KB
/
Copy pathapi.ts
File metadata and controls
182 lines (168 loc) · 5.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
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`
}