sub-bridge 1.0.0 → 1.0.3

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 (69) hide show
  1. package/.github/workflows/npm-publish.yml +3 -2
  2. package/.release-please-manifest.json +1 -1
  3. package/CHANGELOG.md +16 -0
  4. package/dist/auth/provider.d.ts +1 -0
  5. package/dist/auth/provider.d.ts.map +1 -1
  6. package/dist/auth/provider.js +22 -0
  7. package/dist/auth/provider.js.map +1 -1
  8. package/dist/cli.js +0 -14
  9. package/dist/cli.js.map +1 -1
  10. package/dist/oauth/authorize.d.ts +73 -0
  11. package/dist/oauth/authorize.d.ts.map +1 -0
  12. package/dist/oauth/authorize.js +197 -0
  13. package/dist/oauth/authorize.js.map +1 -0
  14. package/dist/oauth/crypto.d.ts +58 -0
  15. package/dist/oauth/crypto.d.ts.map +1 -0
  16. package/dist/oauth/crypto.js +170 -0
  17. package/dist/oauth/crypto.js.map +1 -0
  18. package/dist/oauth/dcr.d.ts +44 -0
  19. package/dist/oauth/dcr.d.ts.map +1 -0
  20. package/dist/oauth/dcr.js +84 -0
  21. package/dist/oauth/dcr.js.map +1 -0
  22. package/dist/oauth/metadata.d.ts +23 -0
  23. package/dist/oauth/metadata.d.ts.map +1 -0
  24. package/dist/oauth/metadata.js +29 -0
  25. package/dist/oauth/metadata.js.map +1 -0
  26. package/dist/oauth/token.d.ts +29 -0
  27. package/dist/oauth/token.d.ts.map +1 -0
  28. package/dist/oauth/token.js +117 -0
  29. package/dist/oauth/token.js.map +1 -0
  30. package/dist/routes/chat.d.ts.map +1 -1
  31. package/dist/routes/chat.js +128 -15
  32. package/dist/routes/chat.js.map +1 -1
  33. package/dist/routes/oauth.d.ts +13 -0
  34. package/dist/routes/oauth.d.ts.map +1 -0
  35. package/dist/routes/oauth.js +174 -0
  36. package/dist/routes/oauth.js.map +1 -0
  37. package/dist/server.d.ts.map +1 -1
  38. package/dist/server.js +14 -6
  39. package/dist/server.js.map +1 -1
  40. package/dist/tunnel/providers/cloudflare.d.ts.map +1 -1
  41. package/dist/tunnel/providers/cloudflare.js +19 -5
  42. package/dist/tunnel/providers/cloudflare.js.map +1 -1
  43. package/dist/tunnel/registry.d.ts.map +1 -1
  44. package/dist/tunnel/registry.js +75 -9
  45. package/dist/tunnel/registry.js.map +1 -1
  46. package/dist/utils/cloudflared-config.d.ts +2 -0
  47. package/dist/utils/cloudflared-config.d.ts.map +1 -0
  48. package/dist/utils/cloudflared-config.js +116 -0
  49. package/dist/utils/cloudflared-config.js.map +1 -0
  50. package/dist/utils/setup-instructions.d.ts.map +1 -1
  51. package/dist/utils/setup-instructions.js +6 -9
  52. package/dist/utils/setup-instructions.js.map +1 -1
  53. package/index.html +268 -281
  54. package/package.json +6 -2
  55. package/src/cli.ts +0 -14
  56. package/src/routes/chat.ts +3 -0
  57. package/src/server.ts +10 -6
  58. package/src/tunnel/providers/cloudflare.ts +18 -5
  59. package/src/tunnel/registry.ts +79 -8
  60. package/src/utils/cloudflared-config.ts +121 -0
  61. package/src/utils/setup-instructions.ts +6 -9
  62. package/dist/auth/oauth-flow.d.ts +0 -24
  63. package/dist/auth/oauth-flow.d.ts.map +0 -1
  64. package/dist/auth/oauth-flow.js +0 -184
  65. package/dist/auth/oauth-flow.js.map +0 -1
  66. package/dist/auth/oauth-manager.d.ts +0 -13
  67. package/dist/auth/oauth-manager.d.ts.map +0 -1
  68. package/dist/auth/oauth-manager.js +0 -25
  69. package/dist/auth/oauth-manager.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sub-bridge",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "MCP bridge to use ChatGPT Pro, Claude Max, etc. in Cursor via an OpenAI-compatible proxy",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "dev": "tsx src/cli.ts --verbose",
12
12
  "dev:server": "tsx watch src/server.ts",
13
13
  "dev:mcp": "tsx src/mcp.ts",
14
- "dev:tmux": "tmux attach -t sub-bridge || tmux new -s sub-bridge \"set -a; source ./.env; set +a; npm run dev:server\"",
14
+ "dev:tmux": "tmux attach -t sub-bridge 2>/dev/null || tmux new -s sub-bridge \"set -a; source ./.env; set +a; npm run dev:server || sleep 10\"",
15
15
  "start": "node dist/cli.js",
16
16
  "start:server": "tsx src/server.ts",
17
17
  "start:mcp": "tsx src/mcp.ts",
@@ -34,6 +34,10 @@
34
34
  ],
35
35
  "author": "Maol-1997",
36
36
  "license": "MIT",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/buremba/sub-bridge"
40
+ },
37
41
  "dependencies": {
38
42
  "@hono/node-server": "^1.19.7",
39
43
  "@modelcontextprotocol/sdk": "^1.0.0",
package/src/cli.ts CHANGED
@@ -69,23 +69,9 @@ async function main() {
69
69
  }
70
70
 
71
71
  function printSetupInstructions(publicUrl: string) {
72
- log()
73
- log(chalk.bold.yellow(' Setup in Cursor:'))
74
- log(chalk.dim(' ─────────────────────────────────────'))
75
- log()
76
- log(chalk.dim(' 1. Command Palette (Cmd+Shift+P / Ctrl+Shift+P)'))
77
- log(chalk.dim(' 2. Search "Cursor Settings" → Open'))
78
- log(chalk.dim(' 3. Navigate: Models → API Keys (expand)'))
79
- log(chalk.dim(' 4. Enable "OpenAI API Key" toggle'))
80
- log(chalk.dim(' 5. Set API key with routing:'))
81
- log(chalk.dim(' '), chalk.cyan('o3=opus-4.5,o3-mini=sonnet-4.5:sk-ant-xxx'))
82
- log(chalk.dim(' 6. Enable "Override OpenAI Base URL" toggle'))
83
- log(chalk.dim(' 7. Set Base URL:'), chalk.cyan.bold(`${publicUrl}/v1`))
84
72
  log()
85
73
  log(chalk.dim(' Getting API keys:'))
86
74
  log(chalk.dim(' • Open'), chalk.cyan(publicUrl), chalk.dim('in your external browser and click Login buttons'))
87
- log(chalk.dim(' • Or use Claude Code CLI:'), chalk.cyan('claude setup-token'))
88
- log(chalk.dim(' • Or use Codex CLI:'), chalk.cyan('codex login'))
89
75
  log()
90
76
  log(chalk.dim(' Use MCP tool: "get_status" to get URL anytime'))
91
77
  log()
@@ -961,6 +961,9 @@ Examples:
961
961
  export function createChatRoutes() {
962
962
  const app = new Hono()
963
963
 
964
+ // Base route for quick health checks
965
+ app.get('/', (c) => c.json({ status: 'ok' }))
966
+
964
967
  // Models endpoint
965
968
  app.get('/models', async (c) => {
966
969
  const response = await fetch('https://models.dev/api.json')
package/src/server.ts CHANGED
@@ -23,6 +23,7 @@ import { createChatRoutes } from './routes/chat'
23
23
  import { createOAuthRoutes } from './routes/oauth'
24
24
  import { addSharedOptions } from './utils/cli-args'
25
25
  import { buildStatusText } from './utils/setup-instructions'
26
+ import { getCloudflaredHostnames } from './utils/cloudflared-config'
26
27
 
27
28
  // ============================================================================
28
29
  // CONFIGURATION
@@ -86,10 +87,14 @@ export function createServerApp(config: ServerConfig, getPublicUrl: () => string
86
87
  }))
87
88
 
88
89
  // API config for frontend
89
- app.get('/api/config', (c) => c.json({
90
- publicUrl: getPublicUrl(),
91
- port: localPort.value,
92
- }))
90
+ app.get('/api/config', async (c) => {
91
+ const cloudflaredHostnames = await getCloudflaredHostnames(localPort.value)
92
+ return c.json({
93
+ publicUrl: getPublicUrl(),
94
+ port: localPort.value,
95
+ cloudflaredHostnames,
96
+ })
97
+ })
93
98
 
94
99
  // MCP tool forwarding endpoint
95
100
  app.post('/mcp/tools/:name', async (c) => {
@@ -156,14 +161,13 @@ export async function startServer(config: ServerConfig): Promise<StartedServer>
156
161
  : `http://localhost:${info.port}`
157
162
 
158
163
  log()
159
- log(chalk.bold.cyan(' Sub Bridge HTTP Server'))
164
+ log(chalk.bold.cyan(' Sub Bridge OpenAI API Gateway and MCP Server'))
160
165
  log(chalk.dim(' ─────────────────────────────────────'))
161
166
  log()
162
167
  log(' ', chalk.green(publicUrl))
163
168
  log(chalk.dim(` Port: ${info.port}`))
164
169
  log()
165
170
  log(chalk.dim(' API key format: o3=opus-4.5,o3-mini=sonnet-4.5:sk-ant-xxx'))
166
- log()
167
171
 
168
172
  resolve({
169
173
  port: info.port,
@@ -3,6 +3,9 @@
3
3
  // ============================================================================
4
4
 
5
5
  import { Tunnel } from 'cloudflared'
6
+ import fs from 'node:fs/promises'
7
+ import os from 'node:os'
8
+ import path from 'node:path'
6
9
  import type { TunnelProvider, TunnelInstance } from '../types'
7
10
 
8
11
  export class CloudflareTunnelProvider implements TunnelProvider {
@@ -27,14 +30,21 @@ export class CloudflareTunnelProvider implements TunnelProvider {
27
30
  }
28
31
 
29
32
  // Anonymous tunnel using cloudflared npm package
30
- const tunnel = Tunnel.quick(`http://localhost:${localPort}`)
33
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'sub-bridge-cloudflared-'))
34
+ const configPath = path.join(tmpDir, 'config.yml')
35
+ await fs.writeFile(configPath, 'no-autoupdate: true\n')
36
+ // Use HTTP/2 protocol for more reliable connections
37
+ const tunnel = Tunnel.quick(`http://localhost:${localPort}`, { '--config': configPath, '--protocol': 'http2' })
31
38
 
39
+ // Wait for URL event - the tunnel is usable once we have the URL
32
40
  const url = await new Promise<string>((resolve, reject) => {
33
- const timeout = setTimeout(() => reject(new Error('Tunnel timeout (30s)')), 30000)
34
- tunnel.once('url', (tunnelUrl: string) => {
41
+ const timeout = setTimeout(() => reject(new Error('Tunnel timeout (60s)')), 60000)
42
+
43
+ tunnel.once('url', (url: string) => {
35
44
  clearTimeout(timeout)
36
- resolve(tunnelUrl)
45
+ resolve(url)
37
46
  })
47
+
38
48
  tunnel.once('error', (err: Error) => {
39
49
  clearTimeout(timeout)
40
50
  reject(err)
@@ -44,7 +54,10 @@ export class CloudflareTunnelProvider implements TunnelProvider {
44
54
  return {
45
55
  providerId: this.id,
46
56
  publicUrl: url,
47
- stop: () => tunnel.stop()
57
+ stop: () => {
58
+ tunnel.stop()
59
+ void fs.rm(tmpDir, { recursive: true, force: true })
60
+ }
48
61
  }
49
62
  }
50
63
  }
@@ -3,6 +3,54 @@
3
3
  // ============================================================================
4
4
 
5
5
  import type { TunnelProvider, TunnelInstance, TunnelStatus, ProviderInfo } from './types'
6
+ import { SERVICE_IDENTIFIER } from '../utils/port'
7
+
8
+ function sleep(ms: number): Promise<void> {
9
+ return new Promise((resolve) => setTimeout(resolve, ms))
10
+ }
11
+
12
+ async function verifyTunnelHealth(publicUrl: string, expectedPort: number): Promise<void> {
13
+ const baseUrl = publicUrl.replace(/\/$/, '')
14
+ const healthUrl = `${baseUrl}/health`
15
+ const maxAttempts = 12
16
+ let lastError: Error | null = null
17
+
18
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
19
+ const controller = new AbortController()
20
+ const timeout = setTimeout(() => controller.abort(), 3000)
21
+
22
+ try {
23
+ const response = await fetch(healthUrl, { signal: controller.signal })
24
+ if (!response.ok) {
25
+ throw new Error(`Tunnel check failed (HTTP ${response.status})`)
26
+ }
27
+
28
+ const data = await response.json() as { service?: string; port?: number }
29
+ if (data.service !== SERVICE_IDENTIFIER) {
30
+ throw new Error('Tunnel check failed (unexpected service)')
31
+ }
32
+ if (typeof data.port === 'number' && data.port !== expectedPort) {
33
+ throw new Error(`Tunnel check failed (unexpected port ${data.port})`)
34
+ }
35
+ return
36
+ } catch (error) {
37
+ if (error instanceof Error && error.name === 'AbortError') {
38
+ lastError = new Error('Tunnel check timed out')
39
+ } else if (error instanceof Error) {
40
+ lastError = error
41
+ } else {
42
+ lastError = new Error(String(error))
43
+ }
44
+ if (attempt < maxAttempts) {
45
+ await sleep(400)
46
+ continue
47
+ }
48
+ throw new Error(`Tunnel check failed: ${lastError.message} (${healthUrl})`)
49
+ } finally {
50
+ clearTimeout(timeout)
51
+ }
52
+ }
53
+ }
6
54
  import {
7
55
  CloudflareTunnelProvider,
8
56
  NgrokTunnelProvider,
@@ -70,15 +118,38 @@ export class TunnelRegistry {
70
118
  throw new Error(`Tunnel provider ${provider.name} is not available`)
71
119
  }
72
120
 
73
- try {
74
- this.lastError = null
75
- this.activeTunnel = await provider.start(localPort, namedUrl)
76
- this.startedAt = new Date().toISOString()
77
- return this.getStatus()
78
- } catch (error) {
79
- this.lastError = error instanceof Error ? error.message : String(error)
80
- throw error
121
+ const isQuickCloudflare = providerId === 'cloudflare' && !namedUrl
122
+ const maxAttempts = isQuickCloudflare ? 4 : 1
123
+ let lastError: Error | null = null
124
+
125
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
126
+ try {
127
+ this.lastError = null
128
+ this.activeTunnel = await provider.start(localPort, namedUrl)
129
+ this.startedAt = new Date().toISOString()
130
+ await verifyTunnelHealth(this.activeTunnel.publicUrl, localPort)
131
+ return this.getStatus()
132
+ } catch (error) {
133
+ lastError = error instanceof Error ? error : new Error(String(error))
134
+ this.lastError = lastError.message
135
+ if (this.activeTunnel) {
136
+ this.activeTunnel.stop()
137
+ this.activeTunnel = null
138
+ this.startedAt = null
139
+ }
140
+ if (attempt < maxAttempts) {
141
+ await sleep(600)
142
+ continue
143
+ }
144
+ }
81
145
  }
146
+
147
+ const message = lastError?.message || 'Unknown error'
148
+ throw new Error(
149
+ maxAttempts > 1
150
+ ? `Failed to start tunnel after ${maxAttempts} attempts: ${message}`
151
+ : message
152
+ )
82
153
  }
83
154
 
84
155
  stop(): TunnelStatus {
@@ -0,0 +1,121 @@
1
+ import fs from 'node:fs/promises'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+
5
+ interface IngressRule {
6
+ hostname?: string
7
+ service?: string
8
+ }
9
+
10
+ function stripQuotes(value: string): string {
11
+ const trimmed = value.trim()
12
+ if (
13
+ (trimmed.startsWith('"') && trimmed.endsWith('"')) ||
14
+ (trimmed.startsWith("'") && trimmed.endsWith("'"))
15
+ ) {
16
+ return trimmed.slice(1, -1)
17
+ }
18
+ return trimmed
19
+ }
20
+
21
+ function normalizeHostname(hostname: string): string {
22
+ return hostname.replace(/^https?:\/\//, '').split('/')[0]?.trim() || ''
23
+ }
24
+
25
+ function parseIngressRules(content: string): IngressRule[] {
26
+ const entries: IngressRule[] = []
27
+ const lines = content.split(/\r?\n/)
28
+ let inIngress = false
29
+ let current: IngressRule | null = null
30
+
31
+ for (const rawLine of lines) {
32
+ const line = rawLine.replace(/\t/g, ' ')
33
+ const trimmed = line.trim()
34
+ if (!trimmed || trimmed.startsWith('#')) continue
35
+
36
+ if (!inIngress) {
37
+ if (trimmed === 'ingress:' || trimmed.startsWith('ingress:')) {
38
+ inIngress = true
39
+ }
40
+ continue
41
+ }
42
+
43
+ const indent = line.match(/^\s*/)?.[0].length ?? 0
44
+ if (indent === 0 && !trimmed.startsWith('-')) {
45
+ break
46
+ }
47
+
48
+ if (trimmed.startsWith('-')) {
49
+ if (current && (current.hostname || current.service)) entries.push(current)
50
+ current = {}
51
+ const afterDash = trimmed.slice(1).trim()
52
+ if (afterDash.startsWith('hostname:')) {
53
+ current.hostname = stripQuotes(afterDash.slice('hostname:'.length))
54
+ } else if (afterDash.startsWith('service:')) {
55
+ current.service = stripQuotes(afterDash.slice('service:'.length))
56
+ }
57
+ continue
58
+ }
59
+
60
+ if (!current) continue
61
+
62
+ if (trimmed.startsWith('hostname:')) {
63
+ current.hostname = stripQuotes(trimmed.slice('hostname:'.length))
64
+ continue
65
+ }
66
+ if (trimmed.startsWith('service:')) {
67
+ current.service = stripQuotes(trimmed.slice('service:'.length))
68
+ continue
69
+ }
70
+ }
71
+
72
+ if (current && (current.hostname || current.service)) entries.push(current)
73
+ return entries
74
+ }
75
+
76
+ function serviceMatchesPort(service: string, port: number): boolean {
77
+ if (!service || service.startsWith('http_status:')) return false
78
+
79
+ try {
80
+ const url = new URL(service)
81
+ const protocol = url.protocol
82
+ if (protocol !== 'http:' && protocol !== 'https:') return false
83
+
84
+ const resolvedPort = url.port
85
+ ? Number(url.port)
86
+ : (protocol === 'https:' ? 443 : 80)
87
+ if (resolvedPort !== port) return false
88
+
89
+ const host = url.hostname
90
+ return host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host === '::1'
91
+ } catch {
92
+ return false
93
+ }
94
+ }
95
+
96
+ export async function getCloudflaredHostnames(port: number): Promise<string[]> {
97
+ const home = os.homedir()
98
+ const configPaths = [
99
+ path.join(home, '.cloudflared', 'config.yml'),
100
+ path.join(home, '.cloudflared', 'config.yaml'),
101
+ ]
102
+
103
+ const hostnames = new Set<string>()
104
+
105
+ for (const configPath of configPaths) {
106
+ try {
107
+ const content = await fs.readFile(configPath, 'utf8')
108
+ const rules = parseIngressRules(content)
109
+ for (const rule of rules) {
110
+ if (!rule.hostname || !rule.service) continue
111
+ if (!serviceMatchesPort(rule.service, port)) continue
112
+ const normalized = normalizeHostname(rule.hostname)
113
+ if (normalized) hostnames.add(normalized)
114
+ }
115
+ } catch {
116
+ continue
117
+ }
118
+ }
119
+
120
+ return Array.from(hostnames)
121
+ }
@@ -21,15 +21,15 @@ export function buildStatusText(options: StatusTextOptions): string {
21
21
  const lines = [
22
22
  'Sub Bridge is running.',
23
23
  '',
24
- `Setup URL: ${baseUrl}`,
24
+ `Setup URL: ${baseUrl}?to=cursor`,
25
25
  "Open this URL in your browser where you're logged into ChatGPT or Claude.",
26
26
  '',
27
27
  'To configure Cursor:',
28
28
  '1. Authenticate with ChatGPT or Claude in the web UI',
29
29
  '2. Copy the generated API key',
30
- '3. In Cursor: Settings → Models → API Keys',
31
- ' - Paste the API key',
32
- ` - Set Base URL to: ${baseUrlForV1}/v1`,
30
+ '3. In Cursor Settings → Models → API Keys',
31
+ ' - Paste the API key from the web UI',
32
+ ` - Set Base URL from the web UI`,
33
33
  '',
34
34
  ...MODEL_MAPPING_LINES,
35
35
  ]
@@ -46,14 +46,11 @@ export function buildStatusText(options: StatusTextOptions): string {
46
46
  'Sub Bridge server not reachable.',
47
47
  '',
48
48
  'To set up:',
49
- `1. Open ${baseUrl} in your browser where you're logged into ChatGPT or Claude`,
49
+ `1. Open ${baseUrl}?to=cursor in your browser where you're logged into ChatGPT or Claude`,
50
50
  '2. Authenticate and copy the generated API key',
51
- `3. In Cursor: Settings → Models → API Keys, paste the key and set Base URL to ${baseUrlForV1}/v1`,
51
+ `3. In Cursor: Settings → Models → API Keys, paste the key and set Base URL from the web UI`,
52
52
  '',
53
53
  ...MODEL_MAPPING_LINES,
54
- '',
55
- 'If you need a public URL, enable a tunnel in the web UI.',
56
- '',
57
54
  `Setup screenshot: ${baseUrl}/assets/setup.png`,
58
55
  ].join('\n')
59
56
  }
@@ -1,24 +0,0 @@
1
- interface PKCE {
2
- verifier: string;
3
- challenge: string;
4
- }
5
- interface TokenResponse {
6
- access_token: string;
7
- refresh_token: string;
8
- expires_in: number;
9
- }
10
- interface ExchangeOptions {
11
- persist?: boolean;
12
- log?: boolean;
13
- }
14
- export declare function getAuthorizationUrl(pkce: PKCE): string;
15
- export declare function exchangeCodeForTokens(code: string, verifier: string, options?: ExchangeOptions): Promise<TokenResponse>;
16
- export declare function generateAuthSession(): Promise<{
17
- authUrl: string;
18
- sessionId: string;
19
- }>;
20
- export declare function handleOAuthCallback(code: string, sessionId: string): Promise<TokenResponse>;
21
- export declare function login(): Promise<boolean>;
22
- export declare function logout(): Promise<boolean>;
23
- export {};
24
- //# sourceMappingURL=oauth-flow.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"oauth-flow.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"AAQA,UAAU,IAAI;IACZ,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,UAAU,aAAa;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,GAAG,CAAC,EAAE,OAAO,CAAA;CACd;AAiBD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,CAetD;AAuCD,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,aAAa,CAAC,CAuCxB;AAGD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC;IACnD,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,CAAC,CASD;AAGD,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,CAAC,CAMxB;AAED,wBAAsB,KAAK,IAAI,OAAO,CAAC,OAAO,CAAC,CAgB9C;AAED,wBAAsB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAS/C"}
@@ -1,184 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.getAuthorizationUrl = getAuthorizationUrl;
40
- exports.exchangeCodeForTokens = exchangeCodeForTokens;
41
- exports.generateAuthSession = generateAuthSession;
42
- exports.handleOAuthCallback = handleOAuthCallback;
43
- exports.login = login;
44
- exports.logout = logout;
45
- const crypto_1 = __importDefault(require("crypto"));
46
- const authManager = __importStar(require("./oauth-manager"));
47
- const CLIENT_ID = process.env.ANTHROPIC_OAUTH_CLIENT_ID ||
48
- '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
49
- const REDIRECT_URI = 'https://console.anthropic.com/oauth/code/callback';
50
- function splitCodeState(code) {
51
- const splits = code.split('#');
52
- return { code: splits[0], state: splits[1] || undefined };
53
- }
54
- function generatePKCE() {
55
- const verifier = crypto_1.default.randomBytes(32).toString('base64url');
56
- const challenge = crypto_1.default
57
- .createHash('sha256')
58
- .update(verifier)
59
- .digest('base64url');
60
- return { verifier, challenge };
61
- }
62
- function getAuthorizationUrl(pkce) {
63
- const authUrl = new URL('https://claude.ai/oauth/authorize');
64
- authUrl.searchParams.set('code', 'true');
65
- authUrl.searchParams.set('client_id', CLIENT_ID);
66
- authUrl.searchParams.set('response_type', 'code');
67
- authUrl.searchParams.set('redirect_uri', REDIRECT_URI);
68
- authUrl.searchParams.set('scope', 'org:create_api_key user:profile user:inference');
69
- authUrl.searchParams.set('code_challenge', pkce.challenge);
70
- authUrl.searchParams.set('code_challenge_method', 'S256');
71
- authUrl.searchParams.set('state', pkce.verifier);
72
- return authUrl.toString();
73
- }
74
- async function startAuthFlow() {
75
- const pkce = generatePKCE();
76
- const authUrl = getAuthorizationUrl(pkce);
77
- console.log('\nšŸ” OAuth Authentication Required');
78
- console.log('Please visit the following URL to authenticate:');
79
- console.log(`\n${authUrl}\n`);
80
- console.log('After authentication, you will get a code.');
81
- console.log('Please paste the entire code here and press Enter:\n');
82
- // Read code from stdin
83
- const code = await readCodeFromStdin();
84
- if (!code) {
85
- throw new Error('No code provided');
86
- }
87
- // Exchange code for tokens
88
- return await exchangeCodeForTokens(code, pkce.verifier);
89
- }
90
- async function readCodeFromStdin() {
91
- return new Promise((resolve) => {
92
- let code = '';
93
- process.stdin.setEncoding('utf8');
94
- process.stdin.resume();
95
- process.stdin.on('data', (chunk) => {
96
- code += chunk;
97
- if (code.includes('\n')) {
98
- process.stdin.pause();
99
- resolve(code.trim());
100
- }
101
- });
102
- });
103
- }
104
- async function exchangeCodeForTokens(code, verifier, options = {}) {
105
- const { code: authCode, state } = splitCodeState(code);
106
- const response = await fetch('https://console.anthropic.com/v1/oauth/token', {
107
- method: 'POST',
108
- headers: {
109
- 'Content-Type': 'application/json',
110
- },
111
- body: JSON.stringify({
112
- code: authCode,
113
- state: state || verifier,
114
- grant_type: 'authorization_code',
115
- client_id: CLIENT_ID,
116
- redirect_uri: REDIRECT_URI,
117
- code_verifier: verifier,
118
- }),
119
- });
120
- if (!response.ok) {
121
- const error = await response.text();
122
- throw new Error(`Failed to exchange code: ${error}`);
123
- }
124
- const data = (await response.json());
125
- const shouldPersist = options.persist ?? true;
126
- const shouldLog = options.log ?? true;
127
- if (shouldPersist) {
128
- await authManager.set({
129
- type: 'oauth',
130
- refresh: data.refresh_token,
131
- access: data.access_token,
132
- expires: Date.now() + data.expires_in * 1000,
133
- });
134
- if (shouldLog) {
135
- console.log('āœ… OAuth tokens saved successfully!');
136
- }
137
- }
138
- return data;
139
- }
140
- // New function to generate auth URL and store PKCE verifier
141
- async function generateAuthSession() {
142
- const pkce = generatePKCE();
143
- const authUrl = getAuthorizationUrl(pkce);
144
- // Store PKCE verifier temporarily (in production, use a proper session store)
145
- // For now, we'll use the verifier as the session ID
146
- const sessionId = pkce.verifier;
147
- return { authUrl, sessionId };
148
- }
149
- // New function to handle OAuth callback
150
- async function handleOAuthCallback(code, sessionId) {
151
- // In production, retrieve the verifier from session store
152
- // For now, we use the sessionId as the verifier
153
- const verifier = sessionId;
154
- return await exchangeCodeForTokens(code, verifier);
155
- }
156
- async function login() {
157
- try {
158
- // Check if we already have valid credentials
159
- const existing = await authManager.get();
160
- if (existing && existing.access && existing.expires > Date.now()) {
161
- console.log('āœ… Valid OAuth credentials already exist');
162
- return true;
163
- }
164
- // Start OAuth flow
165
- await startAuthFlow();
166
- return true;
167
- }
168
- catch (error) {
169
- console.error('OAuth login failed:', error);
170
- return false;
171
- }
172
- }
173
- async function logout() {
174
- try {
175
- await authManager.remove();
176
- console.log('āœ… OAuth credentials removed');
177
- return true;
178
- }
179
- catch (error) {
180
- console.error('Logout failed:', error);
181
- return false;
182
- }
183
- }
184
- //# sourceMappingURL=oauth-flow.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"oauth-flow.js","sourceRoot":"","sources":["../../src/auth/oauth-flow.ts"],"names":[],"mappings":"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"}
@@ -1,13 +0,0 @@
1
- interface OAuthCredentials {
2
- type: 'oauth';
3
- refresh: string;
4
- access: string;
5
- expires: number;
6
- }
7
- declare function get(): Promise<OAuthCredentials | null>;
8
- declare function set(credentials: OAuthCredentials): Promise<boolean>;
9
- declare function remove(): Promise<boolean>;
10
- declare function getAll(): Promise<Record<string, OAuthCredentials>>;
11
- declare function getAccessToken(): Promise<string | null>;
12
- export { get, set, remove, getAll, getAccessToken };
13
- //# sourceMappingURL=oauth-manager.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"oauth-manager.d.ts","sourceRoot":"","sources":["../../src/auth/oauth-manager.ts"],"names":[],"mappings":"AAAA,UAAU,gBAAgB;IACxB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;CAChB;AAED,iBAAe,GAAG,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAErD;AAED,iBAAe,GAAG,CAAC,WAAW,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,CAElE;AAED,iBAAe,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,CAExC;AAED,iBAAe,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAEjE;AAED,iBAAe,cAAc,IAAI,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAItD;AAED,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,CAAA"}