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.
- package/.github/workflows/npm-publish.yml +3 -2
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +16 -0
- package/dist/auth/provider.d.ts +1 -0
- package/dist/auth/provider.d.ts.map +1 -1
- package/dist/auth/provider.js +22 -0
- package/dist/auth/provider.js.map +1 -1
- package/dist/cli.js +0 -14
- package/dist/cli.js.map +1 -1
- package/dist/oauth/authorize.d.ts +73 -0
- package/dist/oauth/authorize.d.ts.map +1 -0
- package/dist/oauth/authorize.js +197 -0
- package/dist/oauth/authorize.js.map +1 -0
- package/dist/oauth/crypto.d.ts +58 -0
- package/dist/oauth/crypto.d.ts.map +1 -0
- package/dist/oauth/crypto.js +170 -0
- package/dist/oauth/crypto.js.map +1 -0
- package/dist/oauth/dcr.d.ts +44 -0
- package/dist/oauth/dcr.d.ts.map +1 -0
- package/dist/oauth/dcr.js +84 -0
- package/dist/oauth/dcr.js.map +1 -0
- package/dist/oauth/metadata.d.ts +23 -0
- package/dist/oauth/metadata.d.ts.map +1 -0
- package/dist/oauth/metadata.js +29 -0
- package/dist/oauth/metadata.js.map +1 -0
- package/dist/oauth/token.d.ts +29 -0
- package/dist/oauth/token.d.ts.map +1 -0
- package/dist/oauth/token.js +117 -0
- package/dist/oauth/token.js.map +1 -0
- package/dist/routes/chat.d.ts.map +1 -1
- package/dist/routes/chat.js +128 -15
- package/dist/routes/chat.js.map +1 -1
- package/dist/routes/oauth.d.ts +13 -0
- package/dist/routes/oauth.d.ts.map +1 -0
- package/dist/routes/oauth.js +174 -0
- package/dist/routes/oauth.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +14 -6
- package/dist/server.js.map +1 -1
- package/dist/tunnel/providers/cloudflare.d.ts.map +1 -1
- package/dist/tunnel/providers/cloudflare.js +19 -5
- package/dist/tunnel/providers/cloudflare.js.map +1 -1
- package/dist/tunnel/registry.d.ts.map +1 -1
- package/dist/tunnel/registry.js +75 -9
- package/dist/tunnel/registry.js.map +1 -1
- package/dist/utils/cloudflared-config.d.ts +2 -0
- package/dist/utils/cloudflared-config.d.ts.map +1 -0
- package/dist/utils/cloudflared-config.js +116 -0
- package/dist/utils/cloudflared-config.js.map +1 -0
- package/dist/utils/setup-instructions.d.ts.map +1 -1
- package/dist/utils/setup-instructions.js +6 -9
- package/dist/utils/setup-instructions.js.map +1 -1
- package/index.html +268 -281
- package/package.json +6 -2
- package/src/cli.ts +0 -14
- package/src/routes/chat.ts +3 -0
- package/src/server.ts +10 -6
- package/src/tunnel/providers/cloudflare.ts +18 -5
- package/src/tunnel/registry.ts +79 -8
- package/src/utils/cloudflared-config.ts +121 -0
- package/src/utils/setup-instructions.ts +6 -9
- package/dist/auth/oauth-flow.d.ts +0 -24
- package/dist/auth/oauth-flow.d.ts.map +0 -1
- package/dist/auth/oauth-flow.js +0 -184
- package/dist/auth/oauth-flow.js.map +0 -1
- package/dist/auth/oauth-manager.d.ts +0 -13
- package/dist/auth/oauth-manager.d.ts.map +0 -1
- package/dist/auth/oauth-manager.js +0 -25
- package/dist/auth/oauth-manager.js.map +0 -1
package/index.html
CHANGED
|
@@ -67,68 +67,68 @@
|
|
|
67
67
|
<div class="flex flex-col gap-3">
|
|
68
68
|
|
|
69
69
|
<!-- Claude Row -->
|
|
70
|
-
<div class="flex
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<svg class="h-4 w-4 text-white" viewBox="0 0 24 24" aria-hidden="true"><use href="#logo-claude"></use></svg>
|
|
79
|
-
<span>Login with Claude</span>
|
|
80
|
-
</a>
|
|
81
|
-
</template>
|
|
82
|
-
<template x-if="claude.showInput">
|
|
83
|
-
<button
|
|
84
|
-
@click="submitClaudeCode()"
|
|
85
|
-
:disabled="claude.loading || !claude.code.includes('#')"
|
|
86
|
-
class="group inline-flex items-center justify-center gap-2 rounded-lg bg-[#d97757] px-3 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-[#c4674b] disabled:opacity-50 shrink-0"
|
|
87
|
-
>
|
|
88
|
-
<svg class="h-4 w-4 text-white" viewBox="0 0 24 24" aria-hidden="true"><use href="#logo-claude"></use></svg>
|
|
89
|
-
<span x-text="claude.loading ? 'Connecting...' : 'Paste Code'"></span>
|
|
90
|
-
</button>
|
|
91
|
-
</template>
|
|
92
|
-
<!-- Input (shown after first click) -->
|
|
93
|
-
<template x-if="claude.showInput">
|
|
94
|
-
<input
|
|
95
|
-
type="text"
|
|
96
|
-
x-model="claude.code"
|
|
97
|
-
@input="claudeCodeInput()"
|
|
98
|
-
placeholder="CODE#STATE"
|
|
99
|
-
class="flex-1 rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-600 focus:outline-none font-mono"
|
|
100
|
-
:disabled="claude.loading"
|
|
101
|
-
x-init="$nextTick(() => $el.focus())"
|
|
102
|
-
>
|
|
103
|
-
</template>
|
|
104
|
-
<!-- Spacer -->
|
|
105
|
-
<div class="flex-1" x-show="!claude.showInput"></div>
|
|
106
|
-
<!-- Status Indicator -->
|
|
107
|
-
<div class="flex items-center gap-2 shrink-0">
|
|
108
|
-
<span class="h-2 w-2 rounded-full" :class="claude.connected ? 'bg-emerald-400' : 'bg-neutral-600'"></span>
|
|
109
|
-
<span class="text-xs" :class="claude.connected ? 'text-emerald-400' : 'text-neutral-500'" x-text="claude.connected ? 'Connected' : 'Not connected'"></span>
|
|
110
|
-
<!-- Account Switcher (show when multiple accounts) -->
|
|
111
|
-
<template x-if="Object.keys(claude.accounts).length > 1">
|
|
112
|
-
<select
|
|
113
|
-
x-model="claude.activeAccount"
|
|
114
|
-
@change="localStorage.setItem('claude_active_account', claude.activeAccount)"
|
|
115
|
-
class="ml-2 rounded border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs text-neutral-200 focus:border-neutral-600 focus:outline-none"
|
|
70
|
+
<div class="flex flex-col gap-1">
|
|
71
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
72
|
+
<!-- Button -->
|
|
73
|
+
<template x-if="!claude.showInput">
|
|
74
|
+
<a
|
|
75
|
+
@click.prevent="claude.showInput = true; window.open('/auth/claude', '_blank')"
|
|
76
|
+
href="/auth/claude"
|
|
77
|
+
class="group inline-flex items-center justify-center gap-2 rounded-lg bg-[#d97757] px-3 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-[#c4674b] cursor-pointer"
|
|
116
78
|
>
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</select>
|
|
79
|
+
<svg class="h-4 w-4 text-white" viewBox="0 0 24 24" aria-hidden="true"><use href="#logo-claude"></use></svg>
|
|
80
|
+
<span>Login with Claude</span>
|
|
81
|
+
</a>
|
|
121
82
|
</template>
|
|
122
|
-
<template x-if="claude.
|
|
83
|
+
<template x-if="claude.showInput">
|
|
123
84
|
<button
|
|
124
|
-
|
|
125
|
-
class="
|
|
126
|
-
title="Disconnect and clear token"
|
|
85
|
+
disabled
|
|
86
|
+
class="group inline-flex items-center justify-center gap-2 rounded-lg bg-[#d97757] px-3 py-2 text-sm font-semibold text-white shadow-sm transition disabled:opacity-50 shrink-0"
|
|
127
87
|
>
|
|
128
|
-
<svg class="h-4 w-4
|
|
88
|
+
<svg class="h-4 w-4 text-white" viewBox="0 0 24 24" aria-hidden="true"><use href="#logo-claude"></use></svg>
|
|
89
|
+
<span>Login with Claude</span>
|
|
129
90
|
</button>
|
|
130
91
|
</template>
|
|
92
|
+
<!-- Input (shown after first click) -->
|
|
93
|
+
<template x-if="claude.showInput">
|
|
94
|
+
<div class="flex items-center gap-2 flex-1">
|
|
95
|
+
<span class="text-xs text-neutral-400 shrink-0">Enter code</span>
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
x-model="claude.code"
|
|
99
|
+
@input="claudeCodeInput()"
|
|
100
|
+
placeholder="CODE#STATE"
|
|
101
|
+
class="flex-1 rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-600 focus:outline-none font-mono"
|
|
102
|
+
:disabled="claude.loading"
|
|
103
|
+
x-init="$nextTick(() => $el.focus())"
|
|
104
|
+
>
|
|
105
|
+
<template x-if="claude.loading">
|
|
106
|
+
<span class="text-xs text-neutral-400 shrink-0">Checking...</span>
|
|
107
|
+
</template>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
</div>
|
|
111
|
+
<!-- Status Indicator -->
|
|
112
|
+
<div class="flex items-center gap-2 text-xs">
|
|
113
|
+
<span class="h-2 w-2 rounded-full" :class="claude.connected ? 'bg-emerald-400' : 'bg-neutral-600'"></span>
|
|
114
|
+
<span class="text-xs" :class="claude.connected ? 'text-emerald-400' : 'text-neutral-500'" x-text="claude.connected ? 'Connected' : 'Not connected'"></span>
|
|
131
115
|
</div>
|
|
116
|
+
<template x-if="Object.keys(claude.accounts).length">
|
|
117
|
+
<div class="mt-1 flex flex-col gap-1">
|
|
118
|
+
<template x-for="email in Object.keys(claude.accounts)" :key="'claude-account-' + email">
|
|
119
|
+
<div class="flex items-center gap-2 text-xs">
|
|
120
|
+
<span class="text-neutral-300 font-mono" x-text="claude.accounts[email].displayName || email"></span>
|
|
121
|
+
<button
|
|
122
|
+
@click="disconnectClaude(email)"
|
|
123
|
+
class="text-xs text-neutral-400 hover:text-red-400 transition"
|
|
124
|
+
title="Disconnect account"
|
|
125
|
+
>
|
|
126
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
</div>
|
|
131
|
+
</template>
|
|
132
132
|
</div>
|
|
133
133
|
<!-- Claude Error -->
|
|
134
134
|
<template x-if="claude.error">
|
|
@@ -136,56 +136,58 @@
|
|
|
136
136
|
</template>
|
|
137
137
|
|
|
138
138
|
<!-- ChatGPT Row -->
|
|
139
|
-
<div class="flex
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
139
|
+
<div class="flex flex-col gap-1">
|
|
140
|
+
<div class="flex items-center gap-3 flex-wrap">
|
|
141
|
+
<!-- Button -->
|
|
142
|
+
<button
|
|
143
|
+
@click="if(!chatgpt.showInput) { startChatGPT(); window.open('https://auth.openai.com/codex/device', '_blank'); } chatgpt.showInput = true"
|
|
144
|
+
:disabled="chatgpt.polling || chatgpt.showInput"
|
|
144
145
|
class="group inline-flex items-center justify-center gap-2 rounded-lg bg-[#10a37f] px-3 py-2 text-sm font-semibold text-white shadow-sm transition hover:bg-[#0d8a6a] disabled:opacity-50 shrink-0"
|
|
145
146
|
>
|
|
146
147
|
<svg class="h-4 w-4 text-white" viewBox="0 0 24 24" aria-hidden="true"><use href="#logo-openai"></use></svg>
|
|
147
|
-
<span
|
|
148
|
+
<span>Login with ChatGPT</span>
|
|
148
149
|
</button>
|
|
149
150
|
<!-- Login link + Input (shown after first click) -->
|
|
150
151
|
<template x-if="chatgpt.showInput">
|
|
151
152
|
<div class="flex items-center gap-2">
|
|
153
|
+
<span class="text-xs text-neutral-400 shrink-0">Enter device id</span>
|
|
152
154
|
<input
|
|
153
155
|
type="text"
|
|
154
156
|
:value="chatgpt.deviceId"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
readonly
|
|
158
|
+
@focus="$el.select()"
|
|
159
|
+
class="w-32 rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 font-mono text-center"
|
|
160
|
+
>
|
|
161
|
+
<template x-if="chatgpt.polling">
|
|
162
|
+
<span class="text-xs text-neutral-400 shrink-0">Checking...</span>
|
|
163
|
+
</template>
|
|
164
|
+
</div>
|
|
165
|
+
</template>
|
|
166
|
+
</div>
|
|
163
167
|
<!-- Status Indicator -->
|
|
164
|
-
<div class="flex items-center gap-2
|
|
168
|
+
<div class="flex items-center gap-2 text-xs">
|
|
165
169
|
<span class="h-2 w-2 rounded-full" :class="chatgpt.connected ? 'bg-emerald-400' : 'bg-neutral-600'"></span>
|
|
166
170
|
<span class="text-xs" :class="chatgpt.connected ? 'text-emerald-400' : 'text-neutral-500'" x-text="chatgpt.connected ? 'Connected' : 'Not connected'"></span>
|
|
167
|
-
<!-- Account Switcher (show when multiple accounts) -->
|
|
168
|
-
<template x-if="Object.keys(chatgpt.accounts).length > 1">
|
|
169
|
-
<select
|
|
170
|
-
x-model="chatgpt.activeAccount"
|
|
171
|
-
@change="localStorage.setItem('chatgpt_active_account', chatgpt.activeAccount)"
|
|
172
|
-
class="ml-2 rounded border border-neutral-700 bg-neutral-900 px-2 py-1 text-xs text-neutral-200 focus:border-neutral-600 focus:outline-none"
|
|
173
|
-
>
|
|
174
|
-
<template x-for="accountId in Object.keys(chatgpt.accounts)" :key="accountId">
|
|
175
|
-
<option :value="accountId" x-text="chatgpt.accounts[accountId].displayName || accountId"></option>
|
|
176
|
-
</template>
|
|
177
|
-
</select>
|
|
178
|
-
</template>
|
|
179
|
-
<template x-if="chatgpt.connected">
|
|
180
|
-
<button
|
|
181
|
-
@click="disconnectChatGPT()"
|
|
182
|
-
class="ml-1 text-xs text-neutral-400 hover:text-red-400 transition"
|
|
183
|
-
title="Disconnect and clear token"
|
|
184
|
-
>
|
|
185
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
186
|
-
</button>
|
|
187
|
-
</template>
|
|
188
171
|
</div>
|
|
172
|
+
<template x-if="Object.keys(chatgpt.accounts).length">
|
|
173
|
+
<div class="mt-1 flex flex-col gap-1">
|
|
174
|
+
<template x-for="accountId in Object.keys(chatgpt.accounts)" :key="'chatgpt-account-' + accountId">
|
|
175
|
+
<div class="flex items-center gap-2 text-xs">
|
|
176
|
+
<span
|
|
177
|
+
class="text-neutral-300 font-mono"
|
|
178
|
+
x-text="chatgpt.accounts[accountId].displayName && chatgpt.accounts[accountId].displayName !== accountId ? chatgpt.accounts[accountId].displayName + ' (' + accountId + ')' : accountId"
|
|
179
|
+
></span>
|
|
180
|
+
<button
|
|
181
|
+
@click="disconnectChatGPT(accountId)"
|
|
182
|
+
class="text-xs text-neutral-400 hover:text-red-400 transition"
|
|
183
|
+
title="Disconnect account"
|
|
184
|
+
>
|
|
185
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
</template>
|
|
189
|
+
</div>
|
|
190
|
+
</template>
|
|
189
191
|
</div>
|
|
190
192
|
<!-- ChatGPT Error -->
|
|
191
193
|
<template x-if="chatgpt.error">
|
|
@@ -216,30 +218,95 @@
|
|
|
216
218
|
<div class="space-y-4" x-init="fetchTunnelProviders()">
|
|
217
219
|
<section class="space-y-2">
|
|
218
220
|
<h2 class="text-xs font-semibold uppercase tracking-wider text-neutral-400">OpenAI API Gateway</h2>
|
|
219
|
-
<div class="rounded-xl border border-neutral-800 bg-neutral-900/70 shadow-sm
|
|
220
|
-
<div class="flex
|
|
221
|
+
<div class="rounded-xl border border-neutral-800 bg-neutral-900/70 shadow-sm">
|
|
222
|
+
<div class="flex flex-col gap-4 p-4">
|
|
223
|
+
<div class="flex items-center justify-between">
|
|
224
|
+
<div class="flex items-center gap-2">
|
|
225
|
+
<span
|
|
226
|
+
class="h-2 w-2 rounded-full"
|
|
227
|
+
:class="tunnel.loading ? 'bg-neutral-500 animate-pulse' : (tunnel.status.active || hasExternalUrl ? 'bg-emerald-400' : 'bg-neutral-600')"
|
|
228
|
+
></span>
|
|
229
|
+
<span
|
|
230
|
+
class="text-xs"
|
|
231
|
+
:class="tunnel.loading ? 'text-neutral-400' : (tunnel.status.active || hasExternalUrl ? 'text-emerald-400' : 'text-neutral-500')"
|
|
232
|
+
x-text="tunnel.loading ? 'Loading' : (tunnel.status.active ? 'Connected' : (hasExternalUrl ? 'External URL' : 'Inactive'))"
|
|
233
|
+
></span>
|
|
234
|
+
</div>
|
|
235
|
+
<template x-if="tunnel.status.active">
|
|
236
|
+
<button
|
|
237
|
+
@click="stopTunnel()"
|
|
238
|
+
class="rounded-lg border border-red-500/60 px-3 py-1.5 text-xs font-semibold text-red-200 hover:bg-red-500/20"
|
|
239
|
+
>Stop Tunnel</button>
|
|
240
|
+
</template>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="space-y-1">
|
|
244
|
+
<label
|
|
245
|
+
class="text-[10px] font-semibold uppercase tracking-wider text-neutral-500"
|
|
246
|
+
x-text="tunnelLabel"
|
|
247
|
+
></label>
|
|
248
|
+
<input
|
|
249
|
+
type="text"
|
|
250
|
+
:value="hasTunnel ? effectivePublicUrl + '/v1' : tunnel.customDomain"
|
|
251
|
+
:readonly="hasTunnel"
|
|
252
|
+
@input="if (!hasTunnel) tunnel.customDomain = $event.target.value"
|
|
253
|
+
@focus="$el.select()"
|
|
254
|
+
:placeholder="hasTunnel ? '' : (tunnel.loading ? 'Loading providers...' : 'e.g. yourname.example.com')"
|
|
255
|
+
class="w-full rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 placeholder:text-neutral-600 focus:border-neutral-600 focus:outline-none font-mono"
|
|
256
|
+
>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<template x-if="!hasTunnel">
|
|
260
|
+
<div class="space-y-3">
|
|
261
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
262
|
+
<div class="flex flex-wrap gap-2" x-show="!tunnel.loading && tunnel.providers.some(p => p.available)">
|
|
263
|
+
<template x-for="p in tunnel.providers.filter(p => p.available)" :key="p.id">
|
|
264
|
+
<button
|
|
265
|
+
@click="tunnel.selectedProvider = p.id"
|
|
266
|
+
:class="tunnel.selectedProvider === p.id ? 'bg-neutral-700 text-white border-neutral-600' : 'bg-neutral-950 text-neutral-400 border-neutral-800'"
|
|
267
|
+
class="inline-flex items-center justify-center rounded-lg border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition"
|
|
268
|
+
x-text="p.name"
|
|
269
|
+
></button>
|
|
270
|
+
</template>
|
|
271
|
+
</div>
|
|
272
|
+
<div class="flex flex-wrap gap-2" x-show="tunnel.loading">
|
|
273
|
+
<span class="h-7 w-20 rounded-lg border border-neutral-800 bg-neutral-900/60 animate-pulse"></span>
|
|
274
|
+
<span class="h-7 w-16 rounded-lg border border-neutral-800 bg-neutral-900/60 animate-pulse"></span>
|
|
275
|
+
<span class="h-7 w-12 rounded-lg border border-neutral-800 bg-neutral-900/60 animate-pulse"></span>
|
|
276
|
+
</div>
|
|
277
|
+
<template x-if="!tunnel.status.active && !hasExternalUrl">
|
|
278
|
+
<button
|
|
279
|
+
@click="startTunnel()"
|
|
280
|
+
:disabled="tunnel.starting || !tunnel.selectedProvider || !tunnel.providers.some(p => p.available)"
|
|
281
|
+
class="rounded-lg bg-neutral-100 px-3 py-2 text-xs font-semibold text-neutral-950 hover:bg-white disabled:opacity-50"
|
|
282
|
+
x-text="tunnel.starting ? 'Creating...' : 'Create Tunnel'"
|
|
283
|
+
x-show="!tunnel.loading"
|
|
284
|
+
></button>
|
|
285
|
+
</template>
|
|
286
|
+
</div>
|
|
287
|
+
<p x-show="!tunnel.providers.some(p => p.available) && !tunnel.loading" class="text-xs text-neutral-400">
|
|
288
|
+
No tunnel providers found (install cloudflared, ngrok, or tailscale).
|
|
289
|
+
</p>
|
|
290
|
+
</div>
|
|
291
|
+
</template>
|
|
292
|
+
|
|
293
|
+
<p x-show="tunnel.error" class="text-xs text-red-400" x-text="tunnel.error"></p>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
<div class="space-y-1 px-4 pb-4">
|
|
297
|
+
<label class="text-[10px] font-semibold uppercase tracking-wider text-neutral-500">Generated API Key</label>
|
|
221
298
|
<input
|
|
222
299
|
type="text"
|
|
223
|
-
:value="
|
|
300
|
+
:value="apiKey"
|
|
224
301
|
readonly
|
|
225
302
|
@focus="$el.select()"
|
|
303
|
+
placeholder="Connect to a provider first..."
|
|
226
304
|
class="w-full rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 focus:border-neutral-600 focus:outline-none font-mono"
|
|
227
305
|
>
|
|
228
|
-
<label class="text-xs text-neutral-500 shrink-0 w-28 text-right">OpenAI Base URL</label>
|
|
229
306
|
</div>
|
|
230
|
-
<div class="flex items-center gap-3 px-4 pb-0">
|
|
231
|
-
<input
|
|
232
|
-
type="text"
|
|
233
|
-
:value="apiKey"
|
|
234
|
-
readonly
|
|
235
|
-
@focus="$el.select()"
|
|
236
|
-
placeholder="Connect to a provider first..."
|
|
237
|
-
class="w-full rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 focus:border-neutral-600 focus:outline-none font-mono"
|
|
238
|
-
> <label class="text-xs text-neutral-500 shrink-0 w-28 text-right">Generated API Key</label>
|
|
239
|
-
</div>
|
|
240
307
|
|
|
241
|
-
<
|
|
242
|
-
<
|
|
308
|
+
<template x-if="claude.connected || chatgpt.connected">
|
|
309
|
+
<div class="flex items-center gap-3 p-4 border-t border-neutral-800">
|
|
243
310
|
<div class="flex items-start gap-3">
|
|
244
311
|
<div class="flex flex-col gap-2 w-full">
|
|
245
312
|
<template x-for="(route, i) in routes" :key="i">
|
|
@@ -258,7 +325,7 @@
|
|
|
258
325
|
<template x-if="Object.keys(claude.accounts).length > 1">
|
|
259
326
|
<select
|
|
260
327
|
x-model="route.account"
|
|
261
|
-
x-init="route.account = route.account ||
|
|
328
|
+
x-init="route.account = route.account || firstClaudeAccountId"
|
|
262
329
|
class="rounded-lg border border-neutral-800 bg-neutral-950 px-2 py-2 text-sm text-neutral-100 focus:border-neutral-600 focus:outline-none"
|
|
263
330
|
>
|
|
264
331
|
<template x-for="email in Object.keys(claude.accounts)" :key="'route-account-' + email">
|
|
@@ -283,7 +350,7 @@
|
|
|
283
350
|
</div>
|
|
284
351
|
</template>
|
|
285
352
|
<button
|
|
286
|
-
@click="routes.push({ cursor: '', claude: '', account:
|
|
353
|
+
@click="routes.push({ cursor: '', claude: '', account: firstClaudeAccountId || '' })"
|
|
287
354
|
class="inline-flex items-center gap-1 text-sm text-neutral-400 hover:text-neutral-200 transition mt-1"
|
|
288
355
|
>
|
|
289
356
|
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
|
@@ -292,8 +359,8 @@
|
|
|
292
359
|
</div>
|
|
293
360
|
<label class="text-xs text-neutral-500 shrink-0 w-28 text-right self-start">Routes</label>
|
|
294
361
|
</div>
|
|
362
|
+
</div>
|
|
295
363
|
</template>
|
|
296
|
-
</div>
|
|
297
364
|
</div>
|
|
298
365
|
</section>
|
|
299
366
|
<!-- Integrate Section -->
|
|
@@ -341,102 +408,18 @@
|
|
|
341
408
|
<li class="flex flex-col gap-1">
|
|
342
409
|
<div class="flex items-start gap-2">
|
|
343
410
|
<span class="mt-0.5 flex h-5 w-5 items-center justify-center rounded-full bg-neutral-800 text-[11px] shrink-0">5</span>
|
|
344
|
-
<div class="flex-1 space-y-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
</div>
|
|
357
|
-
</template>
|
|
358
|
-
<!-- Show instructions when no tunnel -->
|
|
359
|
-
<template x-if="!hasTunnel">
|
|
360
|
-
<div>
|
|
361
|
-
<span>Set Base URL: Expose port <code class="rounded bg-neutral-800/80 px-1.5 py-0.5 text-xs" x-text="port"></code> publicly or</span>
|
|
362
|
-
<button
|
|
363
|
-
type="button"
|
|
364
|
-
@click="tunnel.showPanel = !tunnel.showPanel; if(tunnel.showPanel && !tunnel.providers.length) fetchTunnelProviders()"
|
|
365
|
-
class="text-sky-400 hover:text-sky-300 hover:underline"
|
|
366
|
-
>create a tunnel</button>
|
|
367
|
-
</div>
|
|
368
|
-
</template>
|
|
369
|
-
<!-- Manage tunnel link when dynamic tunnel is active (not for pre-configured) -->
|
|
370
|
-
<template x-if="tunnel.status.active">
|
|
371
|
-
<button
|
|
372
|
-
type="button"
|
|
373
|
-
@click="tunnel.showPanel = !tunnel.showPanel"
|
|
374
|
-
class="text-sky-400 hover:text-sky-300 hover:underline text-sm"
|
|
375
|
-
x-text="tunnel.showPanel ? 'hide tunnel options' : 'manage tunnel'"
|
|
376
|
-
></button>
|
|
377
|
-
</template>
|
|
378
|
-
<!-- Tunnel Panel (collapsible in cursor view) -->
|
|
379
|
-
<template x-if="tunnel.showPanel">
|
|
380
|
-
<div class="rounded-lg border border-neutral-800 bg-neutral-900/50 p-2 space-y-3">
|
|
381
|
-
<!-- Provider Tabs -->
|
|
382
|
-
<div class="flex flex-wrap gap-2" x-show="tunnel.providers.length > 0">
|
|
383
|
-
<template x-for="p in tunnel.providers.filter(p => p.available)" :key="p.id">
|
|
384
|
-
<button
|
|
385
|
-
@click="tunnel.selectedProvider = p.id"
|
|
386
|
-
:class="tunnel.selectedProvider === p.id ? 'bg-neutral-700 text-white border-neutral-600' : 'bg-neutral-950 text-neutral-400 border-neutral-800'"
|
|
387
|
-
class="inline-flex items-center justify-center rounded-lg border px-3 py-1.5 text-xs font-semibold uppercase tracking-wide transition"
|
|
388
|
-
x-text="p.name"
|
|
389
|
-
></button>
|
|
390
|
-
</template>
|
|
391
|
-
</div>
|
|
392
|
-
<!-- No providers -->
|
|
393
|
-
<p x-show="tunnel.providers.length === 0 && !tunnel.loading" class="text-sm text-neutral-400">
|
|
394
|
-
No tunnel providers found (install cloudflared, ngrok, or tailscale).
|
|
395
|
-
</p>
|
|
396
|
-
<!-- Loading -->
|
|
397
|
-
<p x-show="tunnel.loading" class="text-sm text-neutral-400">Loading providers...</p>
|
|
398
|
-
<!-- Active Dynamic Tunnel (with stop button) -->
|
|
399
|
-
<template x-if="tunnel.status.active">
|
|
400
|
-
<div class="space-y-2">
|
|
401
|
-
<div class="flex items-center gap-2 text-sm">
|
|
402
|
-
<span class="inline-flex h-2 w-2 rounded-full bg-emerald-400"></span>
|
|
403
|
-
<span class="font-semibold text-emerald-400">Active</span>
|
|
404
|
-
</div>
|
|
405
|
-
<input
|
|
406
|
-
type="text"
|
|
407
|
-
:value="tunnel.status.publicUrl + '/v1'"
|
|
408
|
-
readonly
|
|
409
|
-
@focus="$el.select()"
|
|
410
|
-
class="w-full rounded-lg border border-emerald-500/30 bg-neutral-950 px-3 py-2 text-xs text-neutral-100 font-mono"
|
|
411
|
-
>
|
|
412
|
-
<button
|
|
413
|
-
@click="stopTunnel()"
|
|
414
|
-
class="w-full rounded-lg bg-red-500/90 px-3 py-2 text-sm font-semibold text-white hover:bg-red-600"
|
|
415
|
-
>Stop Tunnel</button>
|
|
416
|
-
</div>
|
|
417
|
-
</template>
|
|
418
|
-
<!-- Start Form (when no dynamic tunnel and not pre-configured) -->
|
|
419
|
-
<template x-if="!tunnel.status.active && !hasExternalUrl && tunnel.providers.some(p => p.available)">
|
|
420
|
-
<div class="space-y-2">
|
|
421
|
-
<input
|
|
422
|
-
x-show="tunnel.providers.find(p => p.id === tunnel.selectedProvider)?.supportsNamedTunnels"
|
|
423
|
-
type="text"
|
|
424
|
-
x-model="tunnel.customDomain"
|
|
425
|
-
placeholder="Custom domain (optional)"
|
|
426
|
-
class="w-full rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-sm text-neutral-100 placeholder:text-neutral-500 focus:border-neutral-600 focus:outline-none"
|
|
427
|
-
>
|
|
428
|
-
<button
|
|
429
|
-
@click="startTunnel()"
|
|
430
|
-
:disabled="tunnel.starting"
|
|
431
|
-
class="w-full rounded-lg bg-neutral-100 px-3 py-2 text-sm font-semibold text-neutral-950 hover:bg-white disabled:opacity-50"
|
|
432
|
-
x-text="tunnel.starting ? 'Starting...' : 'Start Tunnel'"
|
|
433
|
-
></button>
|
|
434
|
-
</div>
|
|
435
|
-
</template>
|
|
436
|
-
<!-- Error -->
|
|
437
|
-
<p x-show="tunnel.error" class="text-sm text-red-400" x-text="tunnel.error"></p>
|
|
438
|
-
</div>
|
|
439
|
-
</template>
|
|
411
|
+
<div class="flex-1 space-y-1">
|
|
412
|
+
<div class="relative w-full">
|
|
413
|
+
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-500 text-xs pr-1">Set Base URL:</span>
|
|
414
|
+
<input
|
|
415
|
+
type="text"
|
|
416
|
+
:value="hasTunnel ? effectivePublicUrl + '/v1' : ''"
|
|
417
|
+
readonly
|
|
418
|
+
@focus="$el.select()"
|
|
419
|
+
:placeholder="hasTunnel ? '' : 'Create a tunnel first...'"
|
|
420
|
+
class="rounded-lg border border-neutral-800 bg-neutral-950 px-3 py-2 text-xs text-neutral-100 focus:border-neutral-600 focus:outline-none font-mono pl-[100px] w-full"
|
|
421
|
+
>
|
|
422
|
+
</div>
|
|
440
423
|
</div>
|
|
441
424
|
</div>
|
|
442
425
|
</li>
|
|
@@ -464,7 +447,6 @@
|
|
|
464
447
|
// Auth state
|
|
465
448
|
claude: {
|
|
466
449
|
accounts: {}, // Map of email -> account data
|
|
467
|
-
activeAccount: null, // Currently active email
|
|
468
450
|
connected: false,
|
|
469
451
|
code: '',
|
|
470
452
|
loading: false,
|
|
@@ -473,7 +455,6 @@
|
|
|
473
455
|
},
|
|
474
456
|
chatgpt: {
|
|
475
457
|
accounts: {}, // Map of accountId -> account data
|
|
476
|
-
activeAccount: null, // Currently active accountId
|
|
477
458
|
connected: false,
|
|
478
459
|
deviceId: '',
|
|
479
460
|
sessionId: null,
|
|
@@ -497,7 +478,8 @@
|
|
|
497
478
|
loading: false,
|
|
498
479
|
starting: false,
|
|
499
480
|
error: null,
|
|
500
|
-
customDomain: ''
|
|
481
|
+
customDomain: '',
|
|
482
|
+
detectedDomains: []
|
|
501
483
|
},
|
|
502
484
|
|
|
503
485
|
// Server config
|
|
@@ -532,6 +514,32 @@
|
|
|
532
514
|
return this.tunnel.status.active || this.hasExternalUrl;
|
|
533
515
|
},
|
|
534
516
|
|
|
517
|
+
// Selected tunnel provider name (falls back to generic label)
|
|
518
|
+
get selectedTunnelName() {
|
|
519
|
+
const selected = this.tunnel.providers.find(p => p.id === this.tunnel.selectedProvider);
|
|
520
|
+
return selected ? selected.name : 'Tunnel';
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
get firstClaudeAccountId() {
|
|
524
|
+
return Object.keys(this.claude.accounts)[0] || '';
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
get firstChatGptAccountId() {
|
|
528
|
+
return Object.keys(this.chatgpt.accounts)[0] || '';
|
|
529
|
+
},
|
|
530
|
+
|
|
531
|
+
// Label text for the tunnel/domain input
|
|
532
|
+
get tunnelLabel() {
|
|
533
|
+
if (this.hasTunnel) {
|
|
534
|
+
return 'OpenAI Base URL';
|
|
535
|
+
}
|
|
536
|
+
if (!this.tunnel.selectedProvider) {
|
|
537
|
+
return 'Select a tunnel provider';
|
|
538
|
+
}
|
|
539
|
+
const port = this.port || 'PORT';
|
|
540
|
+
return `Your ${this.selectedTunnelName} tunnel listens on localhost:${port}`;
|
|
541
|
+
},
|
|
542
|
+
|
|
535
543
|
// Debounce timer
|
|
536
544
|
_debounce: null,
|
|
537
545
|
|
|
@@ -539,10 +547,13 @@
|
|
|
539
547
|
get apiKey() {
|
|
540
548
|
const keys = [];
|
|
541
549
|
const claudeMappings = new Map();
|
|
550
|
+
const fallbackClaudeAccountId = this.firstClaudeAccountId;
|
|
542
551
|
|
|
543
552
|
for (const route of this.routes) {
|
|
544
553
|
if (!route.cursor || !route.claude) continue;
|
|
545
|
-
const accountId = route.account
|
|
554
|
+
const accountId = route.account && this.claude.accounts[route.account]
|
|
555
|
+
? route.account
|
|
556
|
+
: fallbackClaudeAccountId;
|
|
546
557
|
if (!accountId) continue;
|
|
547
558
|
const claudeAccount = this.claude.accounts[accountId];
|
|
548
559
|
if (!claudeAccount || !claudeAccount.token) continue;
|
|
@@ -558,14 +569,16 @@
|
|
|
558
569
|
keys.push(`${group.mappings.join(',')}:${group.token}`);
|
|
559
570
|
}
|
|
560
571
|
|
|
561
|
-
const
|
|
572
|
+
const chatgptAccountId = this.firstChatGptAccountId;
|
|
573
|
+
const chatgptAccount = chatgptAccountId ? this.chatgpt.accounts[chatgptAccountId] : null;
|
|
562
574
|
if (chatgptAccount && chatgptAccount.token) {
|
|
563
575
|
const chatgptKey = chatgptAccount.accountId
|
|
564
576
|
? `${chatgptAccount.token}#${chatgptAccount.accountId}`
|
|
565
577
|
: chatgptAccount.token;
|
|
566
578
|
keys.push(chatgptKey);
|
|
567
579
|
} else if (!keys.length) {
|
|
568
|
-
const
|
|
580
|
+
const claudeAccountId = this.firstClaudeAccountId;
|
|
581
|
+
const claudeAccount = claudeAccountId ? this.claude.accounts[claudeAccountId] : null;
|
|
569
582
|
if (claudeAccount && claudeAccount.token) {
|
|
570
583
|
keys.push(claudeAccount.token);
|
|
571
584
|
}
|
|
@@ -598,6 +611,12 @@
|
|
|
598
611
|
localStorage.removeItem('chatgpt_account_id');
|
|
599
612
|
console.log('[Migration] Cleared old tokens - please re-login');
|
|
600
613
|
}
|
|
614
|
+
if (localStorage.getItem('claude_active_account')) {
|
|
615
|
+
localStorage.removeItem('claude_active_account');
|
|
616
|
+
}
|
|
617
|
+
if (localStorage.getItem('chatgpt_active_account')) {
|
|
618
|
+
localStorage.removeItem('chatgpt_active_account');
|
|
619
|
+
}
|
|
601
620
|
|
|
602
621
|
// Load Claude accounts from localStorage
|
|
603
622
|
const savedClaudeAccounts = localStorage.getItem('claude_accounts');
|
|
@@ -608,11 +627,7 @@
|
|
|
608
627
|
console.error('Failed to parse Claude accounts:', e);
|
|
609
628
|
}
|
|
610
629
|
}
|
|
611
|
-
|
|
612
|
-
if (savedClaudeActive && this.claude.accounts[savedClaudeActive]) {
|
|
613
|
-
this.claude.activeAccount = savedClaudeActive;
|
|
614
|
-
this.claude.connected = true;
|
|
615
|
-
}
|
|
630
|
+
this.claude.connected = Object.keys(this.claude.accounts).length > 0;
|
|
616
631
|
|
|
617
632
|
// Load ChatGPT accounts from localStorage
|
|
618
633
|
const savedChatGPTAccounts = localStorage.getItem('chatgpt_accounts');
|
|
@@ -623,17 +638,19 @@
|
|
|
623
638
|
console.error('Failed to parse ChatGPT accounts:', e);
|
|
624
639
|
}
|
|
625
640
|
}
|
|
626
|
-
|
|
627
|
-
if (savedChatGPTActive && this.chatgpt.accounts[savedChatGPTActive]) {
|
|
628
|
-
this.chatgpt.activeAccount = savedChatGPTActive;
|
|
629
|
-
this.chatgpt.connected = true;
|
|
630
|
-
}
|
|
641
|
+
this.chatgpt.connected = Object.keys(this.chatgpt.accounts).length > 0;
|
|
631
642
|
|
|
632
643
|
try {
|
|
633
644
|
const res = await fetch('/api/config');
|
|
634
645
|
const config = await res.json();
|
|
635
646
|
this.publicUrl = config.publicUrl;
|
|
636
647
|
this.port = config.port;
|
|
648
|
+
if (Array.isArray(config.cloudflaredHostnames)) {
|
|
649
|
+
this.tunnel.detectedDomains = config.cloudflaredHostnames;
|
|
650
|
+
}
|
|
651
|
+
if (!this.hasTunnel && !this.tunnel.customDomain && this.tunnel.detectedDomains.length > 0) {
|
|
652
|
+
this.tunnel.customDomain = this.tunnel.detectedDomains[0];
|
|
653
|
+
}
|
|
637
654
|
} catch (e) {
|
|
638
655
|
console.error('Failed to fetch config:', e);
|
|
639
656
|
}
|
|
@@ -681,7 +698,6 @@
|
|
|
681
698
|
email: email,
|
|
682
699
|
displayName: email
|
|
683
700
|
};
|
|
684
|
-
this.claude.activeAccount = email;
|
|
685
701
|
this.claude.connected = true;
|
|
686
702
|
this.claude.code = '';
|
|
687
703
|
this.claude.showInput = false;
|
|
@@ -692,7 +708,6 @@
|
|
|
692
708
|
|
|
693
709
|
// Save to localStorage
|
|
694
710
|
localStorage.setItem('claude_accounts', JSON.stringify(this.claude.accounts));
|
|
695
|
-
localStorage.setItem('claude_active_account', email);
|
|
696
711
|
|
|
697
712
|
// If in OAuth flow, complete it
|
|
698
713
|
if (this.oauth.active) {
|
|
@@ -712,35 +727,21 @@
|
|
|
712
727
|
const hasRoutes = this.routes.some(route => route.cursor && route.claude);
|
|
713
728
|
if (hasRoutes) return;
|
|
714
729
|
this.routes = [
|
|
715
|
-
{ cursor: 'o3', claude: 'opus-4.5', account: this.
|
|
716
|
-
{ cursor: 'o3-mini', claude: 'sonnet-4.5', account: this.
|
|
730
|
+
{ cursor: 'o3', claude: 'opus-4.5', account: this.firstClaudeAccountId || '' },
|
|
731
|
+
{ cursor: 'o3-mini', claude: 'sonnet-4.5', account: this.firstClaudeAccountId || '' }
|
|
717
732
|
];
|
|
718
733
|
},
|
|
719
734
|
|
|
720
|
-
// Disconnect Claude
|
|
721
|
-
disconnectClaude() {
|
|
722
|
-
if (this.claude.
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const remainingAccounts = Object.keys(this.claude.accounts);
|
|
729
|
-
if (remainingAccounts.length > 0) {
|
|
730
|
-
this.claude.activeAccount = remainingAccounts[0];
|
|
731
|
-
this.claude.connected = true;
|
|
732
|
-
} else {
|
|
733
|
-
this.claude.connected = false;
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
// Update localStorage
|
|
737
|
-
localStorage.setItem('claude_accounts', JSON.stringify(this.claude.accounts));
|
|
738
|
-
if (this.claude.activeAccount) {
|
|
739
|
-
localStorage.setItem('claude_active_account', this.claude.activeAccount);
|
|
740
|
-
} else {
|
|
741
|
-
localStorage.removeItem('claude_active_account');
|
|
742
|
-
}
|
|
735
|
+
// Disconnect Claude account
|
|
736
|
+
disconnectClaude(accountId) {
|
|
737
|
+
if (!accountId || !this.claude.accounts[accountId]) return;
|
|
738
|
+
delete this.claude.accounts[accountId];
|
|
739
|
+
this.claude.connected = Object.keys(this.claude.accounts).length > 0;
|
|
740
|
+
const fallbackAccountId = this.firstClaudeAccountId;
|
|
741
|
+
for (const route of this.routes) {
|
|
742
|
+
if (route.account === accountId) route.account = fallbackAccountId || '';
|
|
743
743
|
}
|
|
744
|
+
localStorage.setItem('claude_accounts', JSON.stringify(this.claude.accounts));
|
|
744
745
|
|
|
745
746
|
this.claude.code = '';
|
|
746
747
|
this.claude.showInput = false;
|
|
@@ -793,14 +794,12 @@
|
|
|
793
794
|
email: email,
|
|
794
795
|
displayName: displayName
|
|
795
796
|
};
|
|
796
|
-
this.chatgpt.activeAccount = accountId;
|
|
797
797
|
this.chatgpt.connected = true;
|
|
798
798
|
this.chatgpt.polling = false;
|
|
799
799
|
this.chatgpt.showInput = false;
|
|
800
800
|
|
|
801
801
|
// Save to localStorage
|
|
802
802
|
localStorage.setItem('chatgpt_accounts', JSON.stringify(this.chatgpt.accounts));
|
|
803
|
-
localStorage.setItem('chatgpt_active_account', accountId);
|
|
804
803
|
|
|
805
804
|
// If in OAuth flow, complete it
|
|
806
805
|
if (this.oauth.active) {
|
|
@@ -818,30 +817,12 @@
|
|
|
818
817
|
}
|
|
819
818
|
},
|
|
820
819
|
|
|
821
|
-
// Disconnect ChatGPT
|
|
822
|
-
disconnectChatGPT() {
|
|
823
|
-
if (this.chatgpt.
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
// If other accounts exist, switch to first one
|
|
829
|
-
const remainingAccounts = Object.keys(this.chatgpt.accounts);
|
|
830
|
-
if (remainingAccounts.length > 0) {
|
|
831
|
-
this.chatgpt.activeAccount = remainingAccounts[0];
|
|
832
|
-
this.chatgpt.connected = true;
|
|
833
|
-
} else {
|
|
834
|
-
this.chatgpt.connected = false;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
// Update localStorage
|
|
838
|
-
localStorage.setItem('chatgpt_accounts', JSON.stringify(this.chatgpt.accounts));
|
|
839
|
-
if (this.chatgpt.activeAccount) {
|
|
840
|
-
localStorage.setItem('chatgpt_active_account', this.chatgpt.activeAccount);
|
|
841
|
-
} else {
|
|
842
|
-
localStorage.removeItem('chatgpt_active_account');
|
|
843
|
-
}
|
|
844
|
-
}
|
|
820
|
+
// Disconnect ChatGPT account
|
|
821
|
+
disconnectChatGPT(accountId) {
|
|
822
|
+
if (!accountId || !this.chatgpt.accounts[accountId]) return;
|
|
823
|
+
delete this.chatgpt.accounts[accountId];
|
|
824
|
+
this.chatgpt.connected = Object.keys(this.chatgpt.accounts).length > 0;
|
|
825
|
+
localStorage.setItem('chatgpt_accounts', JSON.stringify(this.chatgpt.accounts));
|
|
845
826
|
|
|
846
827
|
this.chatgpt.deviceId = '';
|
|
847
828
|
this.chatgpt.sessionId = null;
|
|
@@ -877,11 +858,15 @@
|
|
|
877
858
|
if (!this.tunnel.selectedProvider) return;
|
|
878
859
|
this.tunnel.starting = true;
|
|
879
860
|
this.tunnel.error = null;
|
|
861
|
+
const rawDomain = (this.tunnel.customDomain || '').trim();
|
|
862
|
+
const namedUrl = rawDomain
|
|
863
|
+
? rawDomain.replace(/^https?:\/\//, '').split('/')[0]
|
|
864
|
+
: undefined;
|
|
880
865
|
try {
|
|
881
866
|
const res = await fetch(`/api/tunnels/${this.tunnel.selectedProvider}/start`, {
|
|
882
867
|
method: 'POST',
|
|
883
868
|
headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
|
|
884
|
-
body: JSON.stringify({ namedUrl
|
|
869
|
+
body: JSON.stringify({ namedUrl })
|
|
885
870
|
});
|
|
886
871
|
const data = await res.json();
|
|
887
872
|
if (data.success) {
|
|
@@ -924,9 +909,11 @@
|
|
|
924
909
|
try {
|
|
925
910
|
// Build providers object with current credentials
|
|
926
911
|
const providers = {};
|
|
912
|
+
const claudeAccountId = this.firstClaudeAccountId;
|
|
913
|
+
const chatgptAccountId = this.firstChatGptAccountId;
|
|
927
914
|
|
|
928
|
-
if (this.claude.connected &&
|
|
929
|
-
const account = this.claude.accounts[
|
|
915
|
+
if (this.claude.connected && claudeAccountId) {
|
|
916
|
+
const account = this.claude.accounts[claudeAccountId];
|
|
930
917
|
if (account) {
|
|
931
918
|
providers.claude = {
|
|
932
919
|
access_token: account.token,
|
|
@@ -935,8 +922,8 @@
|
|
|
935
922
|
}
|
|
936
923
|
}
|
|
937
924
|
|
|
938
|
-
if (this.chatgpt.connected &&
|
|
939
|
-
const account = this.chatgpt.accounts[
|
|
925
|
+
if (this.chatgpt.connected && chatgptAccountId) {
|
|
926
|
+
const account = this.chatgpt.accounts[chatgptAccountId];
|
|
940
927
|
if (account) {
|
|
941
928
|
providers.chatgpt = {
|
|
942
929
|
access_token: account.token,
|
|
@@ -951,7 +938,7 @@
|
|
|
951
938
|
}
|
|
952
939
|
|
|
953
940
|
// Get user ID (email or account ID)
|
|
954
|
-
const userId =
|
|
941
|
+
const userId = claudeAccountId || chatgptAccountId || 'anonymous';
|
|
955
942
|
|
|
956
943
|
// Request authorization code
|
|
957
944
|
const res = await fetch('/oauth/code', {
|