@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from "react";
44import Link from "next/link" ;
55import { useAuth } from "@/lib/auth-context" ;
66import { authHeaders } from "@/lib/auth-client" ;
7+ import { decryptClientSecret , encryptClientSecret , isE2ESecret } from "@/lib/client-secret-crypto" ;
78import { parseWalletPaste , formatWalletCopyText } from "@/lib/wallet-import" ;
89
910interface ReferralWallet {
@@ -37,6 +38,10 @@ export default function AccountContent() {
3738 const [ showAllWallets , setShowAllWallets ] = useState ( false ) ;
3839 const [ aiGatewayKeyDraft , setAiGatewayKeyDraft ] = useState ( "" ) ;
3940 const [ aiGatewayKeySet , setAiGatewayKeySet ] = useState ( false ) ;
41+ const [ aiGatewayKeyE2E , setAiGatewayKeyE2E ] = useState ( false ) ;
42+ const [ revealedAiGatewayKey , setRevealedAiGatewayKey ] = useState ( "" ) ;
43+ const [ revealingAiGatewayKey , setRevealingAiGatewayKey ] = useState ( false ) ;
44+ const [ copiedAiGatewayKey , setCopiedAiGatewayKey ] = useState ( false ) ;
4045 const [ settingsSaving , setSettingsSaving ] = useState ( false ) ;
4146 const [ settingsStatus , setSettingsStatus ] = useState < string | null > ( null ) ;
4247 const [ settingsError , setSettingsError ] = useState < string | null > ( null ) ;
@@ -72,6 +77,7 @@ export default function AccountContent() {
7277 if ( ! res . ok ) return ;
7378 const data = await res . json ( ) ;
7479 setAiGatewayKeySet ( ! ! data . secrets ?. AI_GATEWAY_API_KEY ?. isSet ) ;
80+ setAiGatewayKeyE2E ( ! ! data . secrets ?. AI_GATEWAY_API_KEY ?. e2eEncrypted ) ;
7581 } catch {
7682 // ignore
7783 }
@@ -199,15 +205,19 @@ export default function AccountContent() {
199205 setSettingsStatus ( null ) ;
200206 setSettingsError ( null ) ;
201207 try {
208+ if ( ! profile ?. id ) throw new Error ( "Account profile is still loading" ) ;
209+ const secretValue = value === null ? null : await encryptClientSecret ( profile . id , value . trim ( ) ) ;
202210 const res = await fetch ( "/api/settings" , {
203211 method : "PUT" ,
204212 headers : authHeaders ( { "Content-Type" : "application/json" } ) ,
205- body : JSON . stringify ( { secrets : { AI_GATEWAY_API_KEY : value } } ) ,
213+ body : JSON . stringify ( { secrets : { AI_GATEWAY_API_KEY : secretValue } } ) ,
206214 } ) ;
207215 const data = await res . json ( ) ;
208216 if ( ! res . ok ) throw new Error ( data . error || "Failed to save settings" ) ;
209217 setAiGatewayKeySet ( ! ! data . secrets ?. AI_GATEWAY_API_KEY ?. isSet ) ;
218+ setAiGatewayKeyE2E ( ! ! data . secrets ?. AI_GATEWAY_API_KEY ?. e2eEncrypted ) ;
210219 setAiGatewayKeyDraft ( "" ) ;
220+ setRevealedAiGatewayKey ( "" ) ;
211221 setSettingsStatus ( value === null ? "Cleared" : "Saved" ) ;
212222 } catch ( error ) {
213223 setSettingsError ( ( error as Error ) . message ) ;
@@ -216,6 +226,53 @@ export default function AccountContent() {
216226 }
217227 } ;
218228
229+ const revealAiGatewayKey = async ( ) => {
230+ setSettingsStatus ( null ) ;
231+ setSettingsError ( null ) ;
232+ setRevealingAiGatewayKey ( true ) ;
233+ try {
234+ if ( ! profile ?. id ) throw new Error ( "Account profile is still loading" ) ;
235+ const res = await fetch ( "/api/settings?includeSecretValues=1" , {
236+ headers : authHeaders ( ) ,
237+ cache : "no-store" ,
238+ } ) ;
239+ const data = await res . json ( ) ;
240+ if ( ! res . ok ) throw new Error ( data . error || "Failed to load settings" ) ;
241+
242+ const stored = data . secretValues ?. AI_GATEWAY_API_KEY ;
243+ if ( typeof stored !== "string" || ! stored ) throw new Error ( "No saved Vercel key found" ) ;
244+
245+ const value = await decryptClientSecret ( profile . id , stored ) ;
246+ setRevealedAiGatewayKey ( value ) ;
247+
248+ if ( ! isE2ESecret ( stored ) ) {
249+ const encrypted = await encryptClientSecret ( profile . id , value ) ;
250+ const upgradeRes = await fetch ( "/api/settings" , {
251+ method : "PUT" ,
252+ headers : authHeaders ( { "Content-Type" : "application/json" } ) ,
253+ body : JSON . stringify ( { secrets : { AI_GATEWAY_API_KEY : encrypted } } ) ,
254+ } ) ;
255+ if ( upgradeRes . ok ) {
256+ const upgraded = await upgradeRes . json ( ) ;
257+ setAiGatewayKeySet ( ! ! upgraded . secrets ?. AI_GATEWAY_API_KEY ?. isSet ) ;
258+ setAiGatewayKeyE2E ( ! ! upgraded . secrets ?. AI_GATEWAY_API_KEY ?. e2eEncrypted ) ;
259+ setSettingsStatus ( "Revealed and upgraded to E2E encryption" ) ;
260+ }
261+ }
262+ } catch ( error ) {
263+ setSettingsError ( ( error as Error ) . message ) ;
264+ } finally {
265+ setRevealingAiGatewayKey ( false ) ;
266+ }
267+ } ;
268+
269+ const copyAiGatewayKey = async ( ) => {
270+ if ( ! revealedAiGatewayKey ) return ;
271+ await navigator . clipboard ?. writeText ( revealedAiGatewayKey ) ;
272+ setCopiedAiGatewayKey ( true ) ;
273+ setTimeout ( ( ) => setCopiedAiGatewayKey ( false ) , 2000 ) ;
274+ } ;
275+
219276 return (
220277 < div className = "min-h-screen bg-tc-darker" >
221278 { /* Nav */ }
@@ -342,18 +399,49 @@ export default function AccountContent() {
342399 { settingsSaving ? "Saving..." : "Save" }
343400 </ button >
344401 { aiGatewayKeySet && (
402+ < >
403+ < button
404+ onClick = { revealAiGatewayKey }
405+ disabled = { settingsSaving || revealingAiGatewayKey }
406+ className = "border border-tc-border text-tc-text-dim px-4 py-2 rounded-lg text-sm hover:border-tc-green/40 hover:text-tc-green disabled:opacity-50"
407+ >
408+ { revealingAiGatewayKey ? "Decrypting..." : "Reveal" }
409+ </ button >
410+ < button
411+ onClick = { ( ) => saveAiGatewayKey ( null ) }
412+ disabled = { settingsSaving }
413+ className = "border border-tc-border text-tc-text-dim px-4 py-2 rounded-lg text-sm hover:border-red-400/40 hover:text-red-400 disabled:opacity-50"
414+ >
415+ Clear
416+ </ button >
417+ </ >
418+ ) }
419+ </ div >
420+ { revealedAiGatewayKey && (
421+ < div className = "mt-2 flex flex-col gap-2 rounded-lg border border-tc-border bg-black/40 p-2 sm:flex-row" >
422+ < input
423+ readOnly
424+ type = "text"
425+ value = { revealedAiGatewayKey }
426+ className = "flex-1 rounded border border-tc-border bg-tc-darker px-2 py-1.5 font-mono text-xs text-tc-text focus:outline-none"
427+ />
345428 < button
346- onClick = { ( ) => saveAiGatewayKey ( null ) }
347- disabled = { settingsSaving }
348- className = "border border-tc-border text-tc-text-dim px-4 py-2 rounded-lg text-sm hover:border-red-400 /40 hover:text-red-400 disabled:opacity-50 "
429+ type = "button"
430+ onClick = { copyAiGatewayKey }
431+ className = "rounded border border-tc-border px-3 py-1.5 text-xs text-tc-text-dim hover:border-tc-green /40 hover:text-tc-green "
349432 >
350- Clear
433+ { copiedAiGatewayKey ? "Copied" : "Copy" }
351434 </ button >
352- ) }
353- </ div >
435+ </ div >
436+ ) }
354437 < p className = "text-xs text-tc-text-dim mt-2" >
355438 Used by AI-powered modules such as DeepSec. The key is stored in your account settings, not in the public plugin store.
356439 </ p >
440+ { aiGatewayKeySet && (
441+ < p className = "text-xs text-tc-text-dim mt-1" >
442+ { aiGatewayKeyE2E ? "E2E encrypted in this browser." : "Legacy secret; reveal once to upgrade it to E2E encryption." }
443+ </ p >
444+ ) }
357445 { settingsStatus && < p className = "text-xs text-tc-green mt-2" > { settingsStatus } </ p > }
358446 { settingsError && < p className = "text-xs text-red-400 mt-2" > { settingsError } </ p > }
359447 </ div >
0 commit comments