sub-bridge 1.0.0

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 (163) hide show
  1. package/.cursor/commands/mcp-only.md +1 -0
  2. package/.github/workflows/npm-publish.yml +33 -0
  3. package/.github/workflows/pages.yml +40 -0
  4. package/.github/workflows/release-please.yml +21 -0
  5. package/.release-please-manifest.json +3 -0
  6. package/CHANGELOG.md +8 -0
  7. package/DEVELOPMENT.md +31 -0
  8. package/LICENSE +21 -0
  9. package/README.md +87 -0
  10. package/api/index.ts +12 -0
  11. package/bun.lock +102 -0
  12. package/dist/auth/oauth-flow.d.ts +24 -0
  13. package/dist/auth/oauth-flow.d.ts.map +1 -0
  14. package/dist/auth/oauth-flow.js +184 -0
  15. package/dist/auth/oauth-flow.js.map +1 -0
  16. package/dist/auth/oauth-manager.d.ts +13 -0
  17. package/dist/auth/oauth-manager.d.ts.map +1 -0
  18. package/dist/auth/oauth-manager.js +25 -0
  19. package/dist/auth/oauth-manager.js.map +1 -0
  20. package/dist/auth/provider.d.ts +42 -0
  21. package/dist/auth/provider.d.ts.map +1 -0
  22. package/dist/auth/provider.js +270 -0
  23. package/dist/auth/provider.js.map +1 -0
  24. package/dist/cli.d.ts +3 -0
  25. package/dist/cli.d.ts.map +1 -0
  26. package/dist/cli.js +91 -0
  27. package/dist/cli.js.map +1 -0
  28. package/dist/mcp/proxy.d.ts +16 -0
  29. package/dist/mcp/proxy.d.ts.map +1 -0
  30. package/dist/mcp/proxy.js +85 -0
  31. package/dist/mcp/proxy.js.map +1 -0
  32. package/dist/mcp.d.ts +3 -0
  33. package/dist/mcp.d.ts.map +1 -0
  34. package/dist/mcp.js +50 -0
  35. package/dist/mcp.js.map +1 -0
  36. package/dist/routes/auth.d.ts +6 -0
  37. package/dist/routes/auth.d.ts.map +1 -0
  38. package/dist/routes/auth.js +149 -0
  39. package/dist/routes/auth.js.map +1 -0
  40. package/dist/routes/chat.d.ts +6 -0
  41. package/dist/routes/chat.d.ts.map +1 -0
  42. package/dist/routes/chat.js +808 -0
  43. package/dist/routes/chat.js.map +1 -0
  44. package/dist/routes/tunnels.d.ts +7 -0
  45. package/dist/routes/tunnels.d.ts.map +1 -0
  46. package/dist/routes/tunnels.js +44 -0
  47. package/dist/routes/tunnels.js.map +1 -0
  48. package/dist/server.d.ts +25 -0
  49. package/dist/server.d.ts.map +1 -0
  50. package/dist/server.js +157 -0
  51. package/dist/server.js.map +1 -0
  52. package/dist/tunnel/providers/cloudflare.d.ts +9 -0
  53. package/dist/tunnel/providers/cloudflare.d.ts.map +1 -0
  54. package/dist/tunnel/providers/cloudflare.js +47 -0
  55. package/dist/tunnel/providers/cloudflare.js.map +1 -0
  56. package/dist/tunnel/providers/index.d.ts +4 -0
  57. package/dist/tunnel/providers/index.d.ts.map +1 -0
  58. package/dist/tunnel/providers/index.js +13 -0
  59. package/dist/tunnel/providers/index.js.map +1 -0
  60. package/dist/tunnel/providers/ngrok.d.ts +10 -0
  61. package/dist/tunnel/providers/ngrok.d.ts.map +1 -0
  62. package/dist/tunnel/providers/ngrok.js +52 -0
  63. package/dist/tunnel/providers/ngrok.js.map +1 -0
  64. package/dist/tunnel/providers/tailscale.d.ts +10 -0
  65. package/dist/tunnel/providers/tailscale.d.ts.map +1 -0
  66. package/dist/tunnel/providers/tailscale.js +48 -0
  67. package/dist/tunnel/providers/tailscale.js.map +1 -0
  68. package/dist/tunnel/registry.d.ts +14 -0
  69. package/dist/tunnel/registry.d.ts.map +1 -0
  70. package/dist/tunnel/registry.js +86 -0
  71. package/dist/tunnel/registry.js.map +1 -0
  72. package/dist/tunnel/types.d.ts +26 -0
  73. package/dist/tunnel/types.d.ts.map +1 -0
  74. package/dist/tunnel/types.js +6 -0
  75. package/dist/tunnel/types.js.map +1 -0
  76. package/dist/tunnel/utils.d.ts +18 -0
  77. package/dist/tunnel/utils.d.ts.map +1 -0
  78. package/dist/tunnel/utils.js +57 -0
  79. package/dist/tunnel/utils.js.map +1 -0
  80. package/dist/types.d.ts +52 -0
  81. package/dist/types.d.ts.map +1 -0
  82. package/dist/types.js +4 -0
  83. package/dist/types.js.map +1 -0
  84. package/dist/utils/anthropic-to-openai-converter.d.ts +103 -0
  85. package/dist/utils/anthropic-to-openai-converter.d.ts.map +1 -0
  86. package/dist/utils/anthropic-to-openai-converter.js +376 -0
  87. package/dist/utils/anthropic-to-openai-converter.js.map +1 -0
  88. package/dist/utils/chat-to-responses.d.ts +59 -0
  89. package/dist/utils/chat-to-responses.d.ts.map +1 -0
  90. package/dist/utils/chat-to-responses.js +395 -0
  91. package/dist/utils/chat-to-responses.js.map +1 -0
  92. package/dist/utils/chatgpt-instructions.d.ts +3 -0
  93. package/dist/utils/chatgpt-instructions.d.ts.map +1 -0
  94. package/dist/utils/chatgpt-instructions.js +12 -0
  95. package/dist/utils/chatgpt-instructions.js.map +1 -0
  96. package/dist/utils/cli-args.d.ts +3 -0
  97. package/dist/utils/cli-args.d.ts.map +1 -0
  98. package/dist/utils/cli-args.js +10 -0
  99. package/dist/utils/cli-args.js.map +1 -0
  100. package/dist/utils/cors-bypass.d.ts +4 -0
  101. package/dist/utils/cors-bypass.d.ts.map +1 -0
  102. package/dist/utils/cors-bypass.js +30 -0
  103. package/dist/utils/cors-bypass.js.map +1 -0
  104. package/dist/utils/cursor-byok-bypass.d.ts +37 -0
  105. package/dist/utils/cursor-byok-bypass.d.ts.map +1 -0
  106. package/dist/utils/cursor-byok-bypass.js +53 -0
  107. package/dist/utils/cursor-byok-bypass.js.map +1 -0
  108. package/dist/utils/logger.d.ts +19 -0
  109. package/dist/utils/logger.d.ts.map +1 -0
  110. package/dist/utils/logger.js +192 -0
  111. package/dist/utils/logger.js.map +1 -0
  112. package/dist/utils/port.d.ts +27 -0
  113. package/dist/utils/port.d.ts.map +1 -0
  114. package/dist/utils/port.js +78 -0
  115. package/dist/utils/port.js.map +1 -0
  116. package/dist/utils/setup-instructions.d.ts +10 -0
  117. package/dist/utils/setup-instructions.d.ts.map +1 -0
  118. package/dist/utils/setup-instructions.js +49 -0
  119. package/dist/utils/setup-instructions.js.map +1 -0
  120. package/env.example +25 -0
  121. package/index.html +992 -0
  122. package/package.json +57 -0
  123. package/public/.nojekyll +0 -0
  124. package/public/assets/chat.png +0 -0
  125. package/public/assets/demo.gif +0 -0
  126. package/public/assets/demo.mp4 +0 -0
  127. package/public/assets/setup.png +0 -0
  128. package/public/assets/ui.png +0 -0
  129. package/public/index.html +292 -0
  130. package/release-please-config.json +10 -0
  131. package/src/auth/provider.ts +412 -0
  132. package/src/cli.ts +97 -0
  133. package/src/mcp/proxy.ts +64 -0
  134. package/src/mcp.ts +56 -0
  135. package/src/oauth/authorize.ts +270 -0
  136. package/src/oauth/crypto.ts +198 -0
  137. package/src/oauth/dcr.ts +129 -0
  138. package/src/oauth/metadata.ts +40 -0
  139. package/src/oauth/token.ts +173 -0
  140. package/src/routes/auth.ts +149 -0
  141. package/src/routes/chat.ts +983 -0
  142. package/src/routes/oauth.ts +220 -0
  143. package/src/routes/tunnels.ts +45 -0
  144. package/src/server.ts +204 -0
  145. package/src/tunnel/providers/cloudflare.ts +50 -0
  146. package/src/tunnel/providers/index.ts +7 -0
  147. package/src/tunnel/providers/ngrok.ts +56 -0
  148. package/src/tunnel/providers/tailscale.ts +50 -0
  149. package/src/tunnel/registry.ts +96 -0
  150. package/src/tunnel/types.ts +32 -0
  151. package/src/tunnel/utils.ts +59 -0
  152. package/src/types.ts +55 -0
  153. package/src/utils/anthropic-to-openai-converter.ts +578 -0
  154. package/src/utils/chat-to-responses.ts +512 -0
  155. package/src/utils/chatgpt-instructions.ts +7 -0
  156. package/src/utils/cli-args.ts +8 -0
  157. package/src/utils/cors-bypass.ts +39 -0
  158. package/src/utils/cursor-byok-bypass.ts +56 -0
  159. package/src/utils/logger.ts +174 -0
  160. package/src/utils/port.ts +99 -0
  161. package/src/utils/setup-instructions.ts +59 -0
  162. package/tsconfig.json +22 -0
  163. package/vercel.json +20 -0
@@ -0,0 +1,173 @@
1
+ /**
2
+ * OAuth Token Endpoint
3
+ *
4
+ * Handles token exchange and issues encrypted access tokens
5
+ * containing upstream provider credentials.
6
+ */
7
+ import { createAccessToken, type SubBridgeTokenPayload } from './crypto'
8
+ import { validateClient } from './dcr'
9
+ import { exchangeAuthorizationCode, type AuthorizationCode } from './authorize'
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface TokenRequest {
16
+ grant_type: string
17
+ code?: string
18
+ redirect_uri?: string
19
+ client_id?: string
20
+ client_secret?: string
21
+ code_verifier?: string
22
+ refresh_token?: string
23
+ }
24
+
25
+ export interface TokenResponse {
26
+ access_token: string
27
+ token_type: 'Bearer'
28
+ expires_in: number
29
+ refresh_token?: string
30
+ scope?: string
31
+ }
32
+
33
+ export interface TokenError {
34
+ error: string
35
+ error_description: string
36
+ }
37
+
38
+ // ============================================================================
39
+ // Token Generation
40
+ // ============================================================================
41
+
42
+ const ACCESS_TOKEN_TTL_SECONDS = 3600 * 24 * 7 // 7 days
43
+
44
+ /**
45
+ * Process a token request and return an access token or error.
46
+ */
47
+ export function processTokenRequest(
48
+ request: TokenRequest
49
+ ): TokenResponse | TokenError {
50
+ // Validate grant_type
51
+ if (request.grant_type === 'authorization_code') {
52
+ return handleAuthorizationCodeGrant(request)
53
+ }
54
+
55
+ if (request.grant_type === 'refresh_token') {
56
+ return handleRefreshTokenGrant(request)
57
+ }
58
+
59
+ return {
60
+ error: 'unsupported_grant_type',
61
+ error_description: 'Only authorization_code and refresh_token grants are supported',
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Handle authorization_code grant type.
67
+ */
68
+ function handleAuthorizationCodeGrant(
69
+ request: TokenRequest
70
+ ): TokenResponse | TokenError {
71
+ // Validate required parameters
72
+ if (!request.code) {
73
+ return {
74
+ error: 'invalid_request',
75
+ error_description: 'Missing authorization code',
76
+ }
77
+ }
78
+
79
+ if (!request.client_id) {
80
+ return {
81
+ error: 'invalid_request',
82
+ error_description: 'Missing client_id',
83
+ }
84
+ }
85
+
86
+ if (!request.redirect_uri) {
87
+ return {
88
+ error: 'invalid_request',
89
+ error_description: 'Missing redirect_uri',
90
+ }
91
+ }
92
+
93
+ // Validate client
94
+ const client = validateClient(request.client_id, request.client_secret)
95
+ if (!client) {
96
+ return {
97
+ error: 'invalid_client',
98
+ error_description: 'Client authentication failed',
99
+ }
100
+ }
101
+
102
+ // Exchange authorization code
103
+ const codeResult = exchangeAuthorizationCode(
104
+ request.code,
105
+ request.client_id,
106
+ request.redirect_uri,
107
+ request.code_verifier
108
+ )
109
+
110
+ if ('error' in codeResult) {
111
+ return {
112
+ error: codeResult.error,
113
+ error_description: codeResult.errorDescription,
114
+ }
115
+ }
116
+
117
+ // Create access token
118
+ return createTokenResponse(codeResult, request.client_id)
119
+ }
120
+
121
+ /**
122
+ * Handle refresh_token grant type.
123
+ * For now, we don't support refresh tokens on the Sub Bridge side.
124
+ * Users need to re-authenticate when tokens expire.
125
+ */
126
+ function handleRefreshTokenGrant(
127
+ _request: TokenRequest
128
+ ): TokenResponse | TokenError {
129
+ // TODO: Implement refresh token support
130
+ // This would require storing refresh tokens securely
131
+ return {
132
+ error: 'unsupported_grant_type',
133
+ error_description: 'Refresh tokens are not yet supported. Please re-authenticate.',
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Create a token response from an authorization code.
139
+ */
140
+ function createTokenResponse(
141
+ authCode: AuthorizationCode,
142
+ clientId: string
143
+ ): TokenResponse {
144
+ const now = Math.floor(Date.now() / 1000)
145
+ const expiresAt = now + ACCESS_TOKEN_TTL_SECONDS
146
+
147
+ const payload: SubBridgeTokenPayload = {
148
+ iss: 'sub-bridge',
149
+ sub: authCode.userId,
150
+ aud: clientId,
151
+ exp: expiresAt,
152
+ iat: now,
153
+ providers: authCode.providers,
154
+ }
155
+
156
+ const accessToken = createAccessToken(payload)
157
+
158
+ return {
159
+ access_token: accessToken,
160
+ token_type: 'Bearer',
161
+ expires_in: ACCESS_TOKEN_TTL_SECONDS,
162
+ scope: 'openid profile',
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Check if a response is an error.
168
+ */
169
+ export function isTokenError(
170
+ response: TokenResponse | TokenError
171
+ ): response is TokenError {
172
+ return 'error' in response
173
+ }
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Authentication routes for Claude and OpenAI OAuth
3
+ */
4
+ import { Hono } from 'hono'
5
+ import { claudeProvider, openaiProvider, type AuthSession } from '../auth/provider'
6
+
7
+ // Auth session storage
8
+ const AUTH_SESSION_TTL_MS = 15 * 60 * 1000
9
+ const authSessions = new Map<string, AuthSession & { createdAt: number }>()
10
+
11
+ function storeAuthSession(session: AuthSession) {
12
+ authSessions.set(session.sessionId, { ...session, createdAt: Date.now() })
13
+ const now = Date.now()
14
+ for (const [id, s] of authSessions.entries()) {
15
+ if (now - s.createdAt > AUTH_SESSION_TTL_MS) {
16
+ authSessions.delete(id)
17
+ }
18
+ }
19
+ }
20
+
21
+ function getAuthSession(sessionId: string): (AuthSession & { createdAt: number }) | null {
22
+ const session = authSessions.get(sessionId)
23
+ if (!session) return null
24
+ if (Date.now() - session.createdAt > AUTH_SESSION_TTL_MS) {
25
+ authSessions.delete(sessionId)
26
+ return null
27
+ }
28
+ return session
29
+ }
30
+
31
+ export function createAuthRoutes() {
32
+ const app = new Hono()
33
+
34
+ // OpenAI: Redirect to auth page
35
+ app.get('/openai', async (c) => {
36
+ try {
37
+ return c.redirect(openaiProvider.getAuthUrl())
38
+ } catch (error) {
39
+ const msg = error instanceof Error ? error.message : 'Unknown error'
40
+ return c.text(`Error: ${msg}`, 500)
41
+ }
42
+ })
43
+
44
+ // OpenAI: Debug device code endpoint
45
+ app.get('/openai/device-code', async (c) => {
46
+ try {
47
+ const session = await openaiProvider.startAuth()
48
+ storeAuthSession(session)
49
+ return c.json({
50
+ sessionId: session.sessionId,
51
+ userCode: session.userCode,
52
+ authUrl: openaiProvider.getAuthUrl(),
53
+ expiresAt: session.expiresAt,
54
+ })
55
+ } catch (error) {
56
+ const msg = error instanceof Error ? error.message : 'Unknown error'
57
+ return c.json({ error: msg }, 500)
58
+ }
59
+ })
60
+
61
+ // Claude: Redirect to auth page
62
+ app.get('/claude', async (c) => {
63
+ try {
64
+ const session = await claudeProvider.startAuth()
65
+ storeAuthSession(session)
66
+ return c.redirect(session.authUrl)
67
+ } catch (error) {
68
+ const msg = error instanceof Error ? error.message : 'Unknown error'
69
+ return c.text(`Error: ${msg}`, 500)
70
+ }
71
+ })
72
+
73
+ // Claude: Start auth
74
+ app.post('/claude/start', async (c) => {
75
+ try {
76
+ const session = await claudeProvider.startAuth()
77
+ storeAuthSession(session)
78
+ return c.redirect(session.authUrl)
79
+ } catch (error) {
80
+ const msg = error instanceof Error ? error.message : 'Unknown error'
81
+ return c.text(`Error: ${msg}`, 500)
82
+ }
83
+ })
84
+
85
+ // Claude: Complete auth (returns JSON)
86
+ app.post('/claude/complete', async (c) => {
87
+ try {
88
+ const body = await c.req.json().catch(() => ({})) as { code?: string; sessionId?: string }
89
+ const codeInput = body.code?.trim() || ''
90
+ const sessionId = body.sessionId || codeInput.split('#')[1]
91
+ if (!codeInput) {
92
+ return c.json({ success: false, error: 'Missing code' })
93
+ }
94
+ if (!sessionId) {
95
+ return c.json({ success: false, error: 'Missing state. Paste CODE#STATE.' })
96
+ }
97
+ const result = await claudeProvider.completeAuth(codeInput, sessionId)
98
+ return c.json({ success: true, accessToken: result.accessToken })
99
+ } catch (error) {
100
+ const msg = error instanceof Error ? error.message : 'Unknown error'
101
+ return c.json({ success: false, error: msg })
102
+ }
103
+ })
104
+
105
+ // OpenAI: Start auth (returns JSON)
106
+ app.post('/openai/start', async (c) => {
107
+ try {
108
+ const session = await openaiProvider.startAuth()
109
+ storeAuthSession(session)
110
+ return c.json({
111
+ sessionId: session.sessionId,
112
+ userCode: session.userCode,
113
+ authUrl: openaiProvider.getAuthUrl(),
114
+ })
115
+ } catch (error) {
116
+ const msg = error instanceof Error ? error.message : 'Unknown error'
117
+ return c.json({ error: msg }, 500)
118
+ }
119
+ })
120
+
121
+ // OpenAI: Poll for auth (returns JSON)
122
+ app.post('/openai/poll', async (c) => {
123
+ try {
124
+ const body = await c.req.json().catch(() => ({})) as { sessionId?: string }
125
+ const sessionId = body.sessionId
126
+ if (!sessionId) {
127
+ return c.json({ status: 'error', error: 'Missing session' })
128
+ }
129
+ const session = getAuthSession(sessionId)
130
+ if (!session) {
131
+ return c.json({ status: 'error', error: 'Session expired' })
132
+ }
133
+ const result = await openaiProvider.completeAuth('', sessionId)
134
+ return c.json({
135
+ status: 'success',
136
+ accessToken: result.accessToken,
137
+ accountId: result.accountId,
138
+ })
139
+ } catch (error) {
140
+ const msg = error instanceof Error ? error.message : 'Unknown error'
141
+ if (msg === 'PENDING') {
142
+ return c.json({ status: 'pending' })
143
+ }
144
+ return c.json({ status: 'error', error: msg })
145
+ }
146
+ })
147
+
148
+ return app
149
+ }