@xopcai/xopc 0.0.52 → 0.0.54

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 (43) hide show
  1. package/dist/extensions/telegram/xopc.extension.json +1 -1
  2. package/dist/gateway/static/root/assets/{agents-CLNe-mJX.js → agents-Cccit5xQ.js} +2 -2
  3. package/dist/gateway/static/root/assets/{agents-CLNe-mJX.js.map → agents-Cccit5xQ.js.map} +1 -1
  4. package/dist/gateway/static/root/assets/{apps-page-c5XF02ek.js → apps-page-JMpsr3yK.js} +2 -2
  5. package/dist/gateway/static/root/assets/{apps-page-c5XF02ek.js.map → apps-page-JMpsr3yK.js.map} +1 -1
  6. package/dist/gateway/static/root/assets/{channels-settings-BTgdsBM4.js → channels-settings-jJX9xOyE.js} +2 -2
  7. package/dist/gateway/static/root/assets/{channels-settings-BTgdsBM4.js.map → channels-settings-jJX9xOyE.js.map} +1 -1
  8. package/dist/gateway/static/root/assets/{cron-dreaming-jobs-CiQJHx3s.js → cron-dreaming-jobs-DousYqF2.js} +2 -2
  9. package/dist/gateway/static/root/assets/{cron-dreaming-jobs-CiQJHx3s.js.map → cron-dreaming-jobs-DousYqF2.js.map} +1 -1
  10. package/dist/gateway/static/root/assets/{cron-page-DJOj7JWb.js → cron-page-CdYb690n.js} +2 -2
  11. package/dist/gateway/static/root/assets/{cron-page-DJOj7JWb.js.map → cron-page-CdYb690n.js.map} +1 -1
  12. package/dist/gateway/static/root/assets/{dist-BY3E71wk.js → dist-D8QibYZR.js} +2 -2
  13. package/dist/gateway/static/root/assets/{dist-BY3E71wk.js.map → dist-D8QibYZR.js.map} +1 -1
  14. package/dist/gateway/static/root/assets/{extension-debug-page-DzkYBiW-.js → extension-debug-page-DJ-puhUh.js} +2 -2
  15. package/dist/gateway/static/root/assets/{extension-debug-page-DzkYBiW-.js.map → extension-debug-page-DJ-puhUh.js.map} +1 -1
  16. package/dist/gateway/static/root/assets/{extension-page-yF0LIy5D.js → extension-page-DgDGH7uc.js} +2 -2
  17. package/dist/gateway/static/root/assets/{extension-page-yF0LIy5D.js.map → extension-page-DgDGH7uc.js.map} +1 -1
  18. package/dist/gateway/static/root/assets/{extension-settings-page-CS_Y4y3w.js → extension-settings-page-BnQ2f4Fq.js} +2 -2
  19. package/dist/gateway/static/root/assets/{extension-settings-page-CS_Y4y3w.js.map → extension-settings-page-BnQ2f4Fq.js.map} +1 -1
  20. package/dist/gateway/static/root/assets/{heartbeat-config-api-DD2LdM_Q.js → heartbeat-config-api-B9hxMalj.js} +2 -2
  21. package/dist/gateway/static/root/assets/{heartbeat-config-api-DD2LdM_Q.js.map → heartbeat-config-api-B9hxMalj.js.map} +1 -1
  22. package/dist/gateway/static/root/assets/{index-V5ZnG6RE.js → index-8IFT6i7x.js} +4 -4
  23. package/dist/gateway/static/root/assets/{index-V5ZnG6RE.js.map → index-8IFT6i7x.js.map} +1 -1
  24. package/dist/gateway/static/root/assets/{logs-page-D79WclZL.js → logs-page-DNHn8mTk.js} +2 -2
  25. package/dist/gateway/static/root/assets/{logs-page-D79WclZL.js.map → logs-page-DNHn8mTk.js.map} +1 -1
  26. package/dist/gateway/static/root/assets/{sessions-page-R_QJy3OK.js → sessions-page-D3kE1tob.js} +2 -2
  27. package/dist/gateway/static/root/assets/{sessions-page-R_QJy3OK.js.map → sessions-page-D3kE1tob.js.map} +1 -1
  28. package/dist/gateway/static/root/assets/{settings-page-CGaj_9Qb.js → settings-page-B4tuVOtd.js} +2 -2
  29. package/dist/gateway/static/root/assets/{settings-page-CGaj_9Qb.js.map → settings-page-B4tuVOtd.js.map} +1 -1
  30. package/dist/gateway/static/root/assets/{skills-page-UzbTgnVY.js → skills-page-Dgd20nUt.js} +2 -2
  31. package/dist/gateway/static/root/assets/{skills-page-UzbTgnVY.js.map → skills-page-Dgd20nUt.js.map} +1 -1
  32. package/dist/gateway/static/root/assets/{use-image-provider-credentials-DAx__u3a.js → use-image-provider-credentials-CqMkyIiU.js} +2 -2
  33. package/dist/gateway/static/root/assets/{use-image-provider-credentials-DAx__u3a.js.map → use-image-provider-credentials-CqMkyIiU.js.map} +1 -1
  34. package/dist/gateway/static/root/index.html +1 -1
  35. package/dist/package.js +1 -1
  36. package/dist/src/browser/providers/extension.js +37 -3
  37. package/dist/src/browser/providers/extension.js.map +1 -1
  38. package/dist/src/gateway/security/csp.d.ts +1 -0
  39. package/dist/src/gateway/security/csp.js +2 -0
  40. package/dist/src/gateway/security/csp.js.map +1 -1
  41. package/dist/src/gateway/service.js +13 -4
  42. package/dist/src/gateway/service.js.map +1 -1
  43. package/package.json +1 -1
@@ -1,2 +1,2 @@
1
- import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{i as t,t as n}from"./vendor-react-DbimaAId.js";import{o as r}from"./vendor-swr-B5fPo7KK.js";import{Ar as i,Ei as a,In as o,Qr as s,St as c,Xr as l,Y as u,Zr as d,_n as f,b as p,dr as m,hn as h,jn as g,kn as _,p as v,ri as y,ui as b,un as x,xt as S,y as C}from"./index-V5ZnG6RE.js";var w=`/api/image/providers`;async function T(){return(await h(x(`/api/image/providers`)))?.payload?.providers??[]}var E=e(t(),1);function D(){return{apiKey:``,region:``,baseUrl:``,imageBaseUrl:``}}function O(e){return e?.apiKey?`••••••••••••`:``}function k(e,t){let n=(()=>{if(!e||typeof e!=`object`||!(`providersConfig`in e))return;let t=e.providersConfig;if(!(!t||typeof t!=`object`||Array.isArray(t)))return t})(),r={};for(let e of t){let t=n?.[e];r[e]={apiKey:O(t),region:t?.region??``,baseUrl:t?.baseUrl??``,imageBaseUrl:t?.imageBaseUrl??``}}return r}function A(e,t,n){let r=e[n].trim();if(r!==t[n].trim())return r||null}function j(e,t){let n=e.trim(),r=t.trim();if(n!==r&&!(v(n)&&v(r)))return n||(r?null:void 0)}function M(e,t,n){let r={};for(let i of e){let e=t[i]??D(),a=n[i]??D();if(JSON.stringify(e)===JSON.stringify(a))continue;let o={},s=j(e.apiKey,a.apiKey);s!==void 0&&(o.apiKey=s);let c=A(e,a,`region`);c!==void 0&&(o.region=c);let l=A(e,a,`baseUrl`);l!==void 0&&(o.baseUrl=l);let u=A(e,a,`imageBaseUrl`);u!==void 0&&(o.imageBaseUrl=u),Object.keys(o).length>0&&(r[i]=o)}return r}async function N(e){let t=await h(x(`/api/image/providers/${encodeURIComponent(e)}/reveal-api-key`),{method:`POST`,headers:{"Content-Type":`application/json`},body:`{}`});if(!t.ok||!t.payload)throw Error(t.error?.message??`Reveal failed`);return t.payload}async function P(e){Object.keys(e).length!==0&&(await h(x(`/api/config`),{method:`PATCH`,body:JSON.stringify({providersConfig:e})}),await S())}var F=n();function I({providerId:e,value:t,onChange:n,labels:r,apiKeyLinks:a,apiKeyLinkLabels:c}){let[f,m]=(0,E.useState)(!1),[h,g]=(0,E.useState)(void 0),[x,S]=(0,E.useState)(!1),[C,w]=(0,E.useState)(null),[T,D]=(0,E.useState)(!1),O=v(t);(0,E.useEffect)(()=>{O||(g(void 0),w(null))},[O,t]);let k=O&&f&&typeof h==`string`?h:t,A=!O||O&&f&&typeof h==`string`?`text`:`password`,j=!O&&t.trim().length>0&&!v(t)||!!f&&typeof h==`string`&&h.length>0,M=(0,E.useCallback)(async()=>{let e=!O&&t.trim()&&!v(t)?t.trim():typeof h==`string`&&h.length>0?h:``;if(e)try{await navigator.clipboard.writeText(e),D(!0),window.setTimeout(()=>D(!1),2e3)}catch{}},[O,h,t]),P=(0,E.useCallback)(async()=>{if(w(null),!O){m(e=>!e);return}if(h!==void 0){m(e=>!e);return}S(!0);try{g((await N(e)).apiKey??null),m(!0)}catch(e){w(e instanceof Error?e.message:r.loadFailed),g(null)}finally{S(!1)}},[O,e,h,r.loadFailed]);return(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-key-${e}`,children:r.apiKeyLabel}),a.length>0?(0,F.jsx)(`div`,{className:`flex flex-col gap-1`,children:a.map(e=>(0,F.jsxs)(`a`,{href:e.href,target:`_blank`,rel:`noopener noreferrer`,className:`inline-flex w-fit items-center gap-1 text-xs font-medium text-accent-fg hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`,children:[p(e.kind,c),(0,F.jsx)(s,{className:`size-3`,"aria-hidden":!0})]},`${e.kind}-${e.href}`))}):null,O?(0,F.jsx)(`p`,{className:`text-[11px] text-fg-subtle`,children:r.maskedHelp}):null,(0,F.jsxs)(`div`,{className:`relative min-w-0`,children:[(0,F.jsx)(`input`,{id:`img-cred-key-${e}`,type:A,autoComplete:`off`,spellCheck:!1,className:o(`w-full rounded-lg border border-edge bg-surface-panel py-2 pl-3 pr-24 font-mono text-sm text-fg`,`placeholder:text-fg-subtle`,_),value:k,placeholder:O?`••••••••`:r.optionalPlaceholder,onChange:e=>{let t=e.target.value;O&&typeof h==`string`&&f&&t!==h&&(g(void 0),m(!1)),n(t)}}),(0,F.jsxs)(`div`,{className:`absolute right-1 top-1/2 flex -translate-y-1/2 gap-0.5`,children:[j?(0,F.jsx)(`button`,{type:`button`,className:o(`rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg`,u.transition,u.press,u.focusRingPanel),title:T?r.copied:r.copy,"aria-label":T?r.copied:r.copy,onClick:()=>void M(),children:T?(0,F.jsx)(b,{className:`size-4`}):(0,F.jsx)(y,{className:`size-4`})}):null,(0,F.jsx)(`button`,{type:`button`,className:o(`rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg disabled:opacity-40`,u.transition,u.press,u.focusRingPanel),title:f?r.hide:r.show,"aria-label":f?r.hide:r.show,disabled:x,onClick:()=>void P(),children:x?(0,F.jsx)(i,{className:`size-4 animate-spin`,"aria-hidden":!0}):f?(0,F.jsx)(d,{className:`size-4`,"aria-hidden":!0}):(0,F.jsx)(l,{className:`size-4`,"aria-hidden":!0})})]})]}),O&&f&&h===null&&!C?(0,F.jsx)(`p`,{className:`text-xs text-amber-700 dark:text-amber-400/90`,children:r.notInConfigFile}):null,C?(0,F.jsx)(`p`,{className:`text-xs text-red-600 dark:text-red-400`,children:C}):null]})}function L(){return o(`w-full rounded-lg border border-edge bg-surface-panel px-3 py-2 text-sm text-fg`,`placeholder:text-fg-subtle`,_)}function R(){return o(L(),`appearance-none bg-[length:1rem] bg-[right_0.5rem_center] bg-no-repeat pr-9`)}var z=`__custom__`;function B(e,t){if(!e.region.trim()&&!e.imageBaseUrl.trim())return``;let n=e.region.trim().toLowerCase();return t.some(e=>e.value===n)?n:z}function V(e,t){let n=e.baseUrl.trim().replace(/\/+$/,``);if(!n)return``;let r=t.map(e=>e.value.replace(/\/+$/,``)).indexOf(n);return r>=0?t[r].value:z}function H(e,t,n){return t===`beijing`?e.dashscopeRegion_beijing:t===`singapore`?e.dashscopeRegion_singapore:t===`us`?e.dashscopeRegion_us:n}function U(e,t){return t===`minimax`?e.minimaxClusterLabel:t===`fal`?e.falQueueBaseLabel:e.baseUrlLabel}function W(e,t){return t===`minimax`?e.minimaxClusterHint:t===`fal`?e.falQueueBaseHint:null}function G({summaries:e,credDraft:t,credDirty:n,credSaving:r,credError:c,credSavedFlash:l,credNoopFlash:u,updateCredRow:d,onDiscardCredentials:f,onSaveCredentials:p,extensionIds:h,showExtensionLinks:_,showImageModelsLink:v,language:y,apiKeyLinkLabels:b,messages:x}){if(e.length===0)return null;let S=e.some(e=>(e.ui?.regions?.length??0)>0),w=e.some(e=>(e.ui?.baseUrlPresets?.length??0)>0);return(0,F.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[(0,F.jsxs)(`div`,{className:`flex flex-col gap-1 text-xs leading-relaxed text-fg-muted`,children:[(0,F.jsx)(`p`,{children:x.credentialsIntro}),S?(0,F.jsx)(`p`,{className:`text-fg-subtle`,children:x.regionHint}):null,w?(0,F.jsx)(`p`,{className:`text-fg-subtle`,children:x.endpointPresetsHint}):null,v?(0,F.jsx)(`p`,{children:(0,F.jsx)(a,{to:`/settings/image-models`,className:`font-medium text-accent hover:underline`,title:x.imageModelsLinkTitle,children:x.openImageModelsPage})}):null]}),c?(0,F.jsx)(`div`,{className:`rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-700 dark:text-red-300`,children:c}):null,(0,F.jsxs)(`div`,{className:`flex flex-wrap items-center justify-end gap-2`,children:[l?(0,F.jsx)(`span`,{className:`text-sm text-fg-muted`,children:x.credentialsSaved}):null,u?(0,F.jsx)(`span`,{className:`text-sm text-fg-muted`,children:x.credentialsNothingToSave}):null,(0,F.jsx)(g,{type:`button`,variant:`secondary`,onClick:f,disabled:!n||r,children:x.discardCredentials}),(0,F.jsx)(g,{type:`button`,variant:`primary`,onClick:p,disabled:!n||r,children:r?(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(i,{className:`size-3.5 animate-spin`}),(0,F.jsx)(`span`,{className:`ml-1.5`,children:x.savingCredentials})]}):(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(m,{className:`size-3.5`}),(0,F.jsx)(`span`,{className:`ml-1.5`,children:x.saveCredentials})]})})]}),(0,F.jsx)(`div`,{className:`flex flex-col gap-4`,children:e.map(e=>{let n=t[e.id]??D(),r=e.ui,i=_&&h.has(e.id)?`/settings/ext/${encodeURIComponent(e.id)}`:null;return(0,F.jsxs)(`div`,{className:`rounded-lg border border-edge bg-surface-panel px-4 py-3 shadow-sm dark:shadow-none`,children:[(0,F.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-wrap items-center gap-2`,children:[(0,F.jsx)(`span`,{className:`text-sm font-semibold text-fg`,children:e.label??e.id}),(0,F.jsxs)(`span`,{className:`text-xs text-fg-subtle`,children:[`(`,e.id,`)`]}),i?(0,F.jsxs)(a,{to:i,className:`inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline`,title:x.extensionSettingsLinkTitle,children:[(0,F.jsx)(s,{className:`size-3`}),x.openExtensionSettings]}):null]}),e.configured?(0,F.jsx)(`span`,{className:`rounded-full bg-accent-soft px-2 py-0.5 text-xs font-medium text-accent-fg`,children:x.configured}):(0,F.jsx)(`span`,{className:`rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300`,children:x.missingKey})]}),e.defaultModel?(0,F.jsxs)(`p`,{className:`mt-1 text-xs text-fg-subtle`,children:[(0,F.jsxs)(`span`,{className:`text-fg-muted`,children:[x.defaultModel,`:`]}),` `,e.id,`/`,e.defaultModel]}):null,e.models.length>0?(0,F.jsxs)(`p`,{className:`mt-0.5 text-xs text-fg-subtle`,children:[(0,F.jsxs)(`span`,{className:`text-fg-muted`,children:[x.modelsLabel,`:`]}),` `,e.models.map(t=>`${e.id}/${t}`).join(`, `)]}):null,(0,F.jsxs)(`div`,{className:`mt-4 grid gap-3 sm:grid-cols-2`,children:[(0,F.jsx)(I,{providerId:e.id,value:n.apiKey,onChange:t=>d(e.id,{apiKey:t}),apiKeyLinks:C(e.id,y),apiKeyLinkLabels:b,labels:{apiKeyLabel:x.apiKeyLabel,optionalPlaceholder:x.optionalPlaceholder,maskedHelp:x.apiKeyMaskedHelp,copy:x.apiKeyCopy,copied:x.apiKeyCopied,show:x.apiKeyShow,hide:x.apiKeyHide,notInConfigFile:x.apiKeyNotInConfigFile,loadFailed:x.apiKeyRevealFailed}}),r?.regions?.length?(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-region-preset-${e.id}`,children:x.regionLabel}),(0,F.jsxs)(`select`,{id:`img-cred-region-preset-${e.id}`,className:R(),value:B(n,r.regions),onChange:t=>{let n=t.target.value;if(n===``){d(e.id,{region:``,imageBaseUrl:``});return}if(n===z){d(e.id,{region:``,imageBaseUrl:``});return}let i=r.regions.find(e=>e.value===n);i&&d(e.id,{region:i.value,imageBaseUrl:i.imageBaseUrl})},children:[(0,F.jsx)(`option`,{value:``,children:x.regionPresetDefault}),r.regions.map(e=>(0,F.jsx)(`option`,{value:e.value,children:H(x,e.value,e.label)},e.value)),(0,F.jsx)(`option`,{value:z,children:x.regionPresetCustom})]}),B(n,r.regions)===z?(0,F.jsxs)(`div`,{className:`mt-2 grid gap-2 sm:grid-cols-2`,children:[(0,F.jsx)(`input`,{type:`text`,className:L(),value:n.region,placeholder:`region`,onChange:t=>d(e.id,{region:t.target.value})}),(0,F.jsx)(`input`,{type:`url`,className:L(),value:n.imageBaseUrl,placeholder:x.imageBaseUrlLabel,onChange:t=>d(e.id,{imageBaseUrl:t.target.value})})]}):null]}):null,r?.baseUrlPresets?.length?(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-base-preset-${e.id}`,children:U(x,r.baseUrlPresetKind)}),W(x,r.baseUrlPresetKind)?(0,F.jsx)(`p`,{className:`text-[11px] text-fg-subtle`,children:W(x,r.baseUrlPresetKind)}):null,(0,F.jsxs)(`select`,{id:`img-cred-base-preset-${e.id}`,className:R(),value:V(n,r.baseUrlPresets),onChange:t=>{let n=t.target.value;if(n===``){d(e.id,{baseUrl:``});return}if(n===z){d(e.id,{baseUrl:``});return}d(e.id,{baseUrl:n.replace(/\/+$/,``)})},children:[(0,F.jsx)(`option`,{value:``,children:x.baseUrlPresetDefault}),r.baseUrlPresets.map(e=>(0,F.jsx)(`option`,{value:e.value,children:e.label},e.value)),(0,F.jsx)(`option`,{value:z,children:x.baseUrlPresetCustom})]}),V(n,r.baseUrlPresets)===z?(0,F.jsx)(`input`,{type:`url`,className:o(L(),`mt-2`),value:n.baseUrl,placeholder:`https://…`,onChange:t=>d(e.id,{baseUrl:t.target.value})}):null]}):null,r?.regions?.length&&B(n,r.regions)!==z?(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-imgbase-ro-${e.id}`,children:x.imageBaseUrlLabel}),(0,F.jsx)(`input`,{id:`img-cred-imgbase-ro-${e.id}`,type:`url`,readOnly:!0,className:o(L(),`cursor-not-allowed opacity-90`),value:n.imageBaseUrl,title:x.imageBaseUrlPresetHint}),(0,F.jsx)(`p`,{className:`text-[11px] text-fg-subtle`,children:x.imageBaseUrlPresetHint})]}):null]})]},e.id)})})]})}function K(e){let t=c(f(e=>!!e.token)),n=t.data,i=(0,E.useMemo)(()=>e.map(e=>e.id),[e]),[a,o]=(0,E.useState)({}),[s,l]=(0,E.useState)({}),[u,d]=(0,E.useState)(!1),[p,m]=(0,E.useState)(null),[h,g]=(0,E.useState)(!1),[_,v]=(0,E.useState)(!1),y=(0,E.useMemo)(()=>k(n?.payload?.config,i),[n?.payload?.config,i]),b=(0,E.useMemo)(()=>JSON.stringify(a)!==JSON.stringify(s),[a,s]);return(0,E.useEffect)(()=>{b||(o(structuredClone(y)),l(structuredClone(y)))},[y,b]),{gwSwr:t,credDraft:a,credBaseline:s,credDirty:b,credSaving:u,credError:p,credSavedFlash:h,credNoopFlash:_,updateCredRow:(0,E.useCallback)((e,t)=>{o(n=>{let r=n[e]??D();return{...n,[e]:{...r,...t}}})},[]),onDiscardCredentials:(0,E.useCallback)(()=>{o(structuredClone(s)),m(null),g(!1),v(!1)},[s]),saveCredentials:(0,E.useCallback)(async e=>{let n=M(i,a,s);if(Object.keys(n).length===0){v(!0),window.setTimeout(()=>v(!1),2200);return}d(!0),m(null),g(!1);try{await P(n);let e=await t.mutate?.();r(x(w));let a=k(e?.payload?.config,i);o(structuredClone(a)),l(structuredClone(a)),g(!0),window.setTimeout(()=>g(!1),2e3)}catch(t){m(t instanceof Error?t.message:e)}finally{d(!1)}},[i,a,s,t])}}export{w as i,G as n,T as r,K as t};
2
- //# sourceMappingURL=use-image-provider-credentials-DAx__u3a.js.map
1
+ import{i as e}from"./rolldown-runtime-DWdDZTNf.js";import{i as t,t as n}from"./vendor-react-DbimaAId.js";import{o as r}from"./vendor-swr-B5fPo7KK.js";import{Ar as i,Ei as a,In as o,Qr as s,St as c,Xr as l,Y as u,Zr as d,_n as f,b as p,dr as m,hn as h,jn as g,kn as _,p as v,ri as y,ui as b,un as x,xt as S,y as C}from"./index-8IFT6i7x.js";var w=`/api/image/providers`;async function T(){return(await h(x(`/api/image/providers`)))?.payload?.providers??[]}var E=e(t(),1);function D(){return{apiKey:``,region:``,baseUrl:``,imageBaseUrl:``}}function O(e){return e?.apiKey?`••••••••••••`:``}function k(e,t){let n=(()=>{if(!e||typeof e!=`object`||!(`providersConfig`in e))return;let t=e.providersConfig;if(!(!t||typeof t!=`object`||Array.isArray(t)))return t})(),r={};for(let e of t){let t=n?.[e];r[e]={apiKey:O(t),region:t?.region??``,baseUrl:t?.baseUrl??``,imageBaseUrl:t?.imageBaseUrl??``}}return r}function A(e,t,n){let r=e[n].trim();if(r!==t[n].trim())return r||null}function j(e,t){let n=e.trim(),r=t.trim();if(n!==r&&!(v(n)&&v(r)))return n||(r?null:void 0)}function M(e,t,n){let r={};for(let i of e){let e=t[i]??D(),a=n[i]??D();if(JSON.stringify(e)===JSON.stringify(a))continue;let o={},s=j(e.apiKey,a.apiKey);s!==void 0&&(o.apiKey=s);let c=A(e,a,`region`);c!==void 0&&(o.region=c);let l=A(e,a,`baseUrl`);l!==void 0&&(o.baseUrl=l);let u=A(e,a,`imageBaseUrl`);u!==void 0&&(o.imageBaseUrl=u),Object.keys(o).length>0&&(r[i]=o)}return r}async function N(e){let t=await h(x(`/api/image/providers/${encodeURIComponent(e)}/reveal-api-key`),{method:`POST`,headers:{"Content-Type":`application/json`},body:`{}`});if(!t.ok||!t.payload)throw Error(t.error?.message??`Reveal failed`);return t.payload}async function P(e){Object.keys(e).length!==0&&(await h(x(`/api/config`),{method:`PATCH`,body:JSON.stringify({providersConfig:e})}),await S())}var F=n();function I({providerId:e,value:t,onChange:n,labels:r,apiKeyLinks:a,apiKeyLinkLabels:c}){let[f,m]=(0,E.useState)(!1),[h,g]=(0,E.useState)(void 0),[x,S]=(0,E.useState)(!1),[C,w]=(0,E.useState)(null),[T,D]=(0,E.useState)(!1),O=v(t);(0,E.useEffect)(()=>{O||(g(void 0),w(null))},[O,t]);let k=O&&f&&typeof h==`string`?h:t,A=!O||O&&f&&typeof h==`string`?`text`:`password`,j=!O&&t.trim().length>0&&!v(t)||!!f&&typeof h==`string`&&h.length>0,M=(0,E.useCallback)(async()=>{let e=!O&&t.trim()&&!v(t)?t.trim():typeof h==`string`&&h.length>0?h:``;if(e)try{await navigator.clipboard.writeText(e),D(!0),window.setTimeout(()=>D(!1),2e3)}catch{}},[O,h,t]),P=(0,E.useCallback)(async()=>{if(w(null),!O){m(e=>!e);return}if(h!==void 0){m(e=>!e);return}S(!0);try{g((await N(e)).apiKey??null),m(!0)}catch(e){w(e instanceof Error?e.message:r.loadFailed),g(null)}finally{S(!1)}},[O,e,h,r.loadFailed]);return(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-key-${e}`,children:r.apiKeyLabel}),a.length>0?(0,F.jsx)(`div`,{className:`flex flex-col gap-1`,children:a.map(e=>(0,F.jsxs)(`a`,{href:e.href,target:`_blank`,rel:`noopener noreferrer`,className:`inline-flex w-fit items-center gap-1 text-xs font-medium text-accent-fg hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent`,children:[p(e.kind,c),(0,F.jsx)(s,{className:`size-3`,"aria-hidden":!0})]},`${e.kind}-${e.href}`))}):null,O?(0,F.jsx)(`p`,{className:`text-[11px] text-fg-subtle`,children:r.maskedHelp}):null,(0,F.jsxs)(`div`,{className:`relative min-w-0`,children:[(0,F.jsx)(`input`,{id:`img-cred-key-${e}`,type:A,autoComplete:`off`,spellCheck:!1,className:o(`w-full rounded-lg border border-edge bg-surface-panel py-2 pl-3 pr-24 font-mono text-sm text-fg`,`placeholder:text-fg-subtle`,_),value:k,placeholder:O?`••••••••`:r.optionalPlaceholder,onChange:e=>{let t=e.target.value;O&&typeof h==`string`&&f&&t!==h&&(g(void 0),m(!1)),n(t)}}),(0,F.jsxs)(`div`,{className:`absolute right-1 top-1/2 flex -translate-y-1/2 gap-0.5`,children:[j?(0,F.jsx)(`button`,{type:`button`,className:o(`rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg`,u.transition,u.press,u.focusRingPanel),title:T?r.copied:r.copy,"aria-label":T?r.copied:r.copy,onClick:()=>void M(),children:T?(0,F.jsx)(b,{className:`size-4`}):(0,F.jsx)(y,{className:`size-4`})}):null,(0,F.jsx)(`button`,{type:`button`,className:o(`rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg disabled:opacity-40`,u.transition,u.press,u.focusRingPanel),title:f?r.hide:r.show,"aria-label":f?r.hide:r.show,disabled:x,onClick:()=>void P(),children:x?(0,F.jsx)(i,{className:`size-4 animate-spin`,"aria-hidden":!0}):f?(0,F.jsx)(d,{className:`size-4`,"aria-hidden":!0}):(0,F.jsx)(l,{className:`size-4`,"aria-hidden":!0})})]})]}),O&&f&&h===null&&!C?(0,F.jsx)(`p`,{className:`text-xs text-amber-700 dark:text-amber-400/90`,children:r.notInConfigFile}):null,C?(0,F.jsx)(`p`,{className:`text-xs text-red-600 dark:text-red-400`,children:C}):null]})}function L(){return o(`w-full rounded-lg border border-edge bg-surface-panel px-3 py-2 text-sm text-fg`,`placeholder:text-fg-subtle`,_)}function R(){return o(L(),`appearance-none bg-[length:1rem] bg-[right_0.5rem_center] bg-no-repeat pr-9`)}var z=`__custom__`;function B(e,t){if(!e.region.trim()&&!e.imageBaseUrl.trim())return``;let n=e.region.trim().toLowerCase();return t.some(e=>e.value===n)?n:z}function V(e,t){let n=e.baseUrl.trim().replace(/\/+$/,``);if(!n)return``;let r=t.map(e=>e.value.replace(/\/+$/,``)).indexOf(n);return r>=0?t[r].value:z}function H(e,t,n){return t===`beijing`?e.dashscopeRegion_beijing:t===`singapore`?e.dashscopeRegion_singapore:t===`us`?e.dashscopeRegion_us:n}function U(e,t){return t===`minimax`?e.minimaxClusterLabel:t===`fal`?e.falQueueBaseLabel:e.baseUrlLabel}function W(e,t){return t===`minimax`?e.minimaxClusterHint:t===`fal`?e.falQueueBaseHint:null}function G({summaries:e,credDraft:t,credDirty:n,credSaving:r,credError:c,credSavedFlash:l,credNoopFlash:u,updateCredRow:d,onDiscardCredentials:f,onSaveCredentials:p,extensionIds:h,showExtensionLinks:_,showImageModelsLink:v,language:y,apiKeyLinkLabels:b,messages:x}){if(e.length===0)return null;let S=e.some(e=>(e.ui?.regions?.length??0)>0),w=e.some(e=>(e.ui?.baseUrlPresets?.length??0)>0);return(0,F.jsxs)(`div`,{className:`flex flex-col gap-4`,children:[(0,F.jsxs)(`div`,{className:`flex flex-col gap-1 text-xs leading-relaxed text-fg-muted`,children:[(0,F.jsx)(`p`,{children:x.credentialsIntro}),S?(0,F.jsx)(`p`,{className:`text-fg-subtle`,children:x.regionHint}):null,w?(0,F.jsx)(`p`,{className:`text-fg-subtle`,children:x.endpointPresetsHint}):null,v?(0,F.jsx)(`p`,{children:(0,F.jsx)(a,{to:`/settings/image-models`,className:`font-medium text-accent hover:underline`,title:x.imageModelsLinkTitle,children:x.openImageModelsPage})}):null]}),c?(0,F.jsx)(`div`,{className:`rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-700 dark:text-red-300`,children:c}):null,(0,F.jsxs)(`div`,{className:`flex flex-wrap items-center justify-end gap-2`,children:[l?(0,F.jsx)(`span`,{className:`text-sm text-fg-muted`,children:x.credentialsSaved}):null,u?(0,F.jsx)(`span`,{className:`text-sm text-fg-muted`,children:x.credentialsNothingToSave}):null,(0,F.jsx)(g,{type:`button`,variant:`secondary`,onClick:f,disabled:!n||r,children:x.discardCredentials}),(0,F.jsx)(g,{type:`button`,variant:`primary`,onClick:p,disabled:!n||r,children:r?(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(i,{className:`size-3.5 animate-spin`}),(0,F.jsx)(`span`,{className:`ml-1.5`,children:x.savingCredentials})]}):(0,F.jsxs)(F.Fragment,{children:[(0,F.jsx)(m,{className:`size-3.5`}),(0,F.jsx)(`span`,{className:`ml-1.5`,children:x.saveCredentials})]})})]}),(0,F.jsx)(`div`,{className:`flex flex-col gap-4`,children:e.map(e=>{let n=t[e.id]??D(),r=e.ui,i=_&&h.has(e.id)?`/settings/ext/${encodeURIComponent(e.id)}`:null;return(0,F.jsxs)(`div`,{className:`rounded-lg border border-edge bg-surface-panel px-4 py-3 shadow-sm dark:shadow-none`,children:[(0,F.jsxs)(`div`,{className:`flex flex-wrap items-center justify-between gap-3`,children:[(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-wrap items-center gap-2`,children:[(0,F.jsx)(`span`,{className:`text-sm font-semibold text-fg`,children:e.label??e.id}),(0,F.jsxs)(`span`,{className:`text-xs text-fg-subtle`,children:[`(`,e.id,`)`]}),i?(0,F.jsxs)(a,{to:i,className:`inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline`,title:x.extensionSettingsLinkTitle,children:[(0,F.jsx)(s,{className:`size-3`}),x.openExtensionSettings]}):null]}),e.configured?(0,F.jsx)(`span`,{className:`rounded-full bg-accent-soft px-2 py-0.5 text-xs font-medium text-accent-fg`,children:x.configured}):(0,F.jsx)(`span`,{className:`rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300`,children:x.missingKey})]}),e.defaultModel?(0,F.jsxs)(`p`,{className:`mt-1 text-xs text-fg-subtle`,children:[(0,F.jsxs)(`span`,{className:`text-fg-muted`,children:[x.defaultModel,`:`]}),` `,e.id,`/`,e.defaultModel]}):null,e.models.length>0?(0,F.jsxs)(`p`,{className:`mt-0.5 text-xs text-fg-subtle`,children:[(0,F.jsxs)(`span`,{className:`text-fg-muted`,children:[x.modelsLabel,`:`]}),` `,e.models.map(t=>`${e.id}/${t}`).join(`, `)]}):null,(0,F.jsxs)(`div`,{className:`mt-4 grid gap-3 sm:grid-cols-2`,children:[(0,F.jsx)(I,{providerId:e.id,value:n.apiKey,onChange:t=>d(e.id,{apiKey:t}),apiKeyLinks:C(e.id,y),apiKeyLinkLabels:b,labels:{apiKeyLabel:x.apiKeyLabel,optionalPlaceholder:x.optionalPlaceholder,maskedHelp:x.apiKeyMaskedHelp,copy:x.apiKeyCopy,copied:x.apiKeyCopied,show:x.apiKeyShow,hide:x.apiKeyHide,notInConfigFile:x.apiKeyNotInConfigFile,loadFailed:x.apiKeyRevealFailed}}),r?.regions?.length?(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-region-preset-${e.id}`,children:x.regionLabel}),(0,F.jsxs)(`select`,{id:`img-cred-region-preset-${e.id}`,className:R(),value:B(n,r.regions),onChange:t=>{let n=t.target.value;if(n===``){d(e.id,{region:``,imageBaseUrl:``});return}if(n===z){d(e.id,{region:``,imageBaseUrl:``});return}let i=r.regions.find(e=>e.value===n);i&&d(e.id,{region:i.value,imageBaseUrl:i.imageBaseUrl})},children:[(0,F.jsx)(`option`,{value:``,children:x.regionPresetDefault}),r.regions.map(e=>(0,F.jsx)(`option`,{value:e.value,children:H(x,e.value,e.label)},e.value)),(0,F.jsx)(`option`,{value:z,children:x.regionPresetCustom})]}),B(n,r.regions)===z?(0,F.jsxs)(`div`,{className:`mt-2 grid gap-2 sm:grid-cols-2`,children:[(0,F.jsx)(`input`,{type:`text`,className:L(),value:n.region,placeholder:`region`,onChange:t=>d(e.id,{region:t.target.value})}),(0,F.jsx)(`input`,{type:`url`,className:L(),value:n.imageBaseUrl,placeholder:x.imageBaseUrlLabel,onChange:t=>d(e.id,{imageBaseUrl:t.target.value})})]}):null]}):null,r?.baseUrlPresets?.length?(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-base-preset-${e.id}`,children:U(x,r.baseUrlPresetKind)}),W(x,r.baseUrlPresetKind)?(0,F.jsx)(`p`,{className:`text-[11px] text-fg-subtle`,children:W(x,r.baseUrlPresetKind)}):null,(0,F.jsxs)(`select`,{id:`img-cred-base-preset-${e.id}`,className:R(),value:V(n,r.baseUrlPresets),onChange:t=>{let n=t.target.value;if(n===``){d(e.id,{baseUrl:``});return}if(n===z){d(e.id,{baseUrl:``});return}d(e.id,{baseUrl:n.replace(/\/+$/,``)})},children:[(0,F.jsx)(`option`,{value:``,children:x.baseUrlPresetDefault}),r.baseUrlPresets.map(e=>(0,F.jsx)(`option`,{value:e.value,children:e.label},e.value)),(0,F.jsx)(`option`,{value:z,children:x.baseUrlPresetCustom})]}),V(n,r.baseUrlPresets)===z?(0,F.jsx)(`input`,{type:`url`,className:o(L(),`mt-2`),value:n.baseUrl,placeholder:`https://…`,onChange:t=>d(e.id,{baseUrl:t.target.value})}):null]}):null,r?.regions?.length&&B(n,r.regions)!==z?(0,F.jsxs)(`div`,{className:`flex min-w-0 flex-col gap-1 sm:col-span-2`,children:[(0,F.jsx)(`label`,{className:`text-xs font-medium text-fg-muted`,htmlFor:`img-cred-imgbase-ro-${e.id}`,children:x.imageBaseUrlLabel}),(0,F.jsx)(`input`,{id:`img-cred-imgbase-ro-${e.id}`,type:`url`,readOnly:!0,className:o(L(),`cursor-not-allowed opacity-90`),value:n.imageBaseUrl,title:x.imageBaseUrlPresetHint}),(0,F.jsx)(`p`,{className:`text-[11px] text-fg-subtle`,children:x.imageBaseUrlPresetHint})]}):null]})]},e.id)})})]})}function K(e){let t=c(f(e=>!!e.token)),n=t.data,i=(0,E.useMemo)(()=>e.map(e=>e.id),[e]),[a,o]=(0,E.useState)({}),[s,l]=(0,E.useState)({}),[u,d]=(0,E.useState)(!1),[p,m]=(0,E.useState)(null),[h,g]=(0,E.useState)(!1),[_,v]=(0,E.useState)(!1),y=(0,E.useMemo)(()=>k(n?.payload?.config,i),[n?.payload?.config,i]),b=(0,E.useMemo)(()=>JSON.stringify(a)!==JSON.stringify(s),[a,s]);return(0,E.useEffect)(()=>{b||(o(structuredClone(y)),l(structuredClone(y)))},[y,b]),{gwSwr:t,credDraft:a,credBaseline:s,credDirty:b,credSaving:u,credError:p,credSavedFlash:h,credNoopFlash:_,updateCredRow:(0,E.useCallback)((e,t)=>{o(n=>{let r=n[e]??D();return{...n,[e]:{...r,...t}}})},[]),onDiscardCredentials:(0,E.useCallback)(()=>{o(structuredClone(s)),m(null),g(!1),v(!1)},[s]),saveCredentials:(0,E.useCallback)(async e=>{let n=M(i,a,s);if(Object.keys(n).length===0){v(!0),window.setTimeout(()=>v(!1),2200);return}d(!0),m(null),g(!1);try{await P(n);let e=await t.mutate?.();r(x(w));let a=k(e?.payload?.config,i);o(structuredClone(a)),l(structuredClone(a)),g(!0),window.setTimeout(()=>g(!1),2e3)}catch(t){m(t instanceof Error?t.message:e)}finally{d(!1)}},[i,a,s,t])}}export{w as i,G as n,T as r,K as t};
2
+ //# sourceMappingURL=use-image-provider-credentials-CqMkyIiU.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-image-provider-credentials-DAx__u3a.js","names":[],"sources":["../../../../../web/src/features/settings/image-providers-swr-key.ts","../../../../../web/src/features/settings/fetch-image-providers.ts","../../../../../web/src/features/settings/image-providers-config-api.ts","../../../../../web/src/features/settings/image-provider-api-key-field.tsx","../../../../../web/src/features/settings/image-provider-credentials-panel.tsx","../../../../../web/src/features/settings/use-image-provider-credentials.ts"],"sourcesContent":["/** Shared SWR key for GET `/api/image/providers` (image settings + extension image pages). */\nexport const IMAGE_PROVIDERS_SWR_KEY = '/api/image/providers';\n","import { fetchJson } from '@/lib/fetch';\nimport { apiUrl } from '@/lib/url';\n\nimport { IMAGE_PROVIDERS_SWR_KEY } from '@/features/settings/image-providers-swr-key';\nimport type { ImageGenProviderCredentialSummary } from '@/features/settings/use-image-provider-credentials';\n\nexport async function fetchImageProvidersList(): Promise<ImageGenProviderCredentialSummary[]> {\n const res = await fetchJson<{\n ok?: boolean;\n payload?: { providers?: ImageGenProviderCredentialSummary[] };\n }>(apiUrl(IMAGE_PROVIDERS_SWR_KEY));\n return res?.payload?.providers ?? [];\n}\n","import { isMaskedKey } from '@/features/settings/providers-api';\nimport { revalidateGatewayConfig } from '@/features/gateway/gateway-config-swr';\nimport { fetchJson } from '@/lib/fetch';\nimport { apiUrl } from '@/lib/url';\n\n/** One row of image-provider credential fields (matches PATCH `providersConfig` subset). */\nexport type ImageProviderCredRow = {\n apiKey: string;\n region: string;\n baseUrl: string;\n imageBaseUrl: string;\n};\n\nexport function emptyImageProviderCredRow(): ImageProviderCredRow {\n return { apiKey: '', region: '', baseUrl: '', imageBaseUrl: '' };\n}\n\nexport type SafeProviderAuthEntry = {\n apiKey: string;\n region?: string;\n baseUrl?: string;\n imageBaseUrl?: string;\n};\n\nfunction maskedApiKeyDisplay(safe?: SafeProviderAuthEntry): string {\n if (!safe?.apiKey) return '';\n return '••••••••••••';\n}\n\n/** Read `payload.config.providersConfig` from GET /api/config (masked). */\nexport function imageProviderCredRowsFromConfigRoot(\n config: unknown,\n imageProviderIds: string[],\n): Record<string, ImageProviderCredRow> {\n const pc = (() => {\n if (!config || typeof config !== 'object' || !('providersConfig' in config)) return undefined;\n const v = (config as { providersConfig?: unknown }).providersConfig;\n if (!v || typeof v !== 'object' || Array.isArray(v)) return undefined;\n return v as Record<string, SafeProviderAuthEntry>;\n })();\n\n const out: Record<string, ImageProviderCredRow> = {};\n for (const id of imageProviderIds) {\n const safe = pc?.[id];\n out[id] = {\n apiKey: maskedApiKeyDisplay(safe),\n region: safe?.region ?? '',\n baseUrl: safe?.baseUrl ?? '',\n imageBaseUrl: safe?.imageBaseUrl ?? '',\n };\n }\n return out;\n}\n\nfunction optionalStringField(\n draft: ImageProviderCredRow,\n baseline: ImageProviderCredRow,\n key: keyof Pick<ImageProviderCredRow, 'region' | 'baseUrl' | 'imageBaseUrl'>,\n): string | null | undefined {\n const d = draft[key].trim();\n const b = baseline[key].trim();\n if (d === b) return undefined;\n if (!d) return null;\n return d;\n}\n\nfunction apiKeyPatchValue(draftKey: string, baselineKey: string): string | null | undefined {\n const d = draftKey.trim();\n const b = baselineKey.trim();\n if (d === b) return undefined;\n if (isMaskedKey(d) && isMaskedKey(b)) return undefined;\n if (!d) {\n if (!b) return undefined;\n return null;\n }\n return d;\n}\n\n/**\n * Build `providersConfig` PATCH entries only for image providers whose row changed.\n * Omits `apiKey` when unchanged (still masked); sends `null` to clear stored key.\n */\nexport function buildImageProvidersConfigPatch(\n imageProviderIds: string[],\n draft: Record<string, ImageProviderCredRow>,\n baseline: Record<string, ImageProviderCredRow>,\n): Record<string, Record<string, unknown>> {\n const patch: Record<string, Record<string, unknown>> = {};\n for (const id of imageProviderIds) {\n const d = draft[id] ?? emptyImageProviderCredRow();\n const b = baseline[id] ?? emptyImageProviderCredRow();\n if (JSON.stringify(d) === JSON.stringify(b)) continue;\n\n const entry: Record<string, unknown> = {};\n const keyDelta = apiKeyPatchValue(d.apiKey, b.apiKey);\n if (keyDelta !== undefined) {\n entry.apiKey = keyDelta;\n }\n const region = optionalStringField(d, b, 'region');\n if (region !== undefined) entry.region = region;\n const baseUrl = optionalStringField(d, b, 'baseUrl');\n if (baseUrl !== undefined) entry.baseUrl = baseUrl;\n const imageBaseUrl = optionalStringField(d, b, 'imageBaseUrl');\n if (imageBaseUrl !== undefined) entry.imageBaseUrl = imageBaseUrl;\n\n if (Object.keys(entry).length > 0) {\n patch[id] = entry;\n }\n }\n return patch;\n}\n\nexport type RevealImageProviderApiKeyPayload = {\n id: string;\n apiKey: string | null;\n source: 'config' | 'none';\n};\n\n/** POST /api/image/providers/:id/reveal-api-key — plaintext only when stored in config file. */\nexport async function revealImageProviderConfigApiKey(providerId: string): Promise<RevealImageProviderApiKeyPayload> {\n const data = await fetchJson<{\n ok?: boolean;\n payload?: RevealImageProviderApiKeyPayload;\n error?: { message?: string };\n }>(apiUrl(`/api/image/providers/${encodeURIComponent(providerId)}/reveal-api-key`), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!data.ok || !data.payload) {\n throw new Error(data.error?.message ?? 'Reveal failed');\n }\n return data.payload;\n}\n\nexport async function patchImageProvidersConfig(\n patch: Record<string, Record<string, unknown>>,\n): Promise<void> {\n if (Object.keys(patch).length === 0) return;\n await fetchJson(apiUrl('/api/config'), {\n method: 'PATCH',\n body: JSON.stringify({ providersConfig: patch }),\n });\n await revalidateGatewayConfig();\n}\n","import { CheckCircle2, Copy, ExternalLink, Eye, EyeOff, Loader2 } from 'lucide-react';\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { revealImageProviderConfigApiKey } from '@/features/settings/image-providers-config-api';\nimport type { ApiKeyLinkKind } from '@/features/settings/provider-enrichment';\nimport { providerApiKeyLinkLabel } from '@/features/settings/provider-enrichment';\nimport { isMaskedKey } from '@/features/settings/providers-api';\nimport type { ProvidersSettingsMessages } from '@/i18n/messages';\nimport { settingsInputFocusClass } from '@/lib/form-field-width';\nimport { interaction } from '@/lib/interaction';\nimport { cn } from '@/lib/cn';\n\nexport type ImageProviderApiKeyFieldLabels = {\n apiKeyLabel: string;\n optionalPlaceholder: string;\n maskedHelp: string;\n copy: string;\n copied: string;\n show: string;\n hide: string;\n notInConfigFile: string;\n loadFailed: string;\n};\n\nexport function ImageProviderApiKeyField({\n providerId,\n value,\n onChange,\n labels,\n apiKeyLinks,\n apiKeyLinkLabels,\n}: {\n providerId: string;\n value: string;\n onChange: (next: string) => void;\n labels: ImageProviderApiKeyFieldLabels;\n apiKeyLinks: { href: string; kind: ApiKeyLinkKind }[];\n apiKeyLinkLabels: Pick<ProvidersSettingsMessages, 'getApiKey' | 'getApiKeyIntl' | 'getApiKeyCn'>;\n}) {\n const [showKey, setShowKey] = useState(false);\n /** `undefined` = not fetched; `null` = fetched, not in config file; string = plaintext from config */\n const [revealed, setRevealed] = useState<string | null | undefined>(undefined);\n const [revealLoading, setRevealLoading] = useState(false);\n const [revealErr, setRevealErr] = useState<string | null>(null);\n const [copied, setCopied] = useState(false);\n\n const masked = isMaskedKey(value);\n\n useEffect(() => {\n if (!masked) {\n setRevealed(undefined);\n setRevealErr(null);\n }\n }, [masked, value]);\n\n const inputValue = (() => {\n if (!masked) return value;\n if (showKey && typeof revealed === 'string') return revealed;\n return value;\n })();\n\n const inputType =\n !masked || (masked && showKey && typeof revealed === 'string') ? ('text' as const) : ('password' as const);\n\n const copyEnabled =\n (!masked && value.trim().length > 0 && !isMaskedKey(value)) ||\n (Boolean(showKey) && typeof revealed === 'string' && revealed.length > 0);\n\n const copyKey = useCallback(async () => {\n const text =\n !masked && value.trim() && !isMaskedKey(value)\n ? value.trim()\n : typeof revealed === 'string' && revealed.length > 0\n ? revealed\n : '';\n if (!text) return;\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n window.setTimeout(() => setCopied(false), 2000);\n } catch {\n /* ignore */\n }\n }, [masked, revealed, value]);\n\n const toggleEye = useCallback(async () => {\n setRevealErr(null);\n if (!masked) {\n setShowKey((s) => !s);\n return;\n }\n if (revealed !== undefined) {\n setShowKey((s) => !s);\n return;\n }\n setRevealLoading(true);\n try {\n const payload = await revealImageProviderConfigApiKey(providerId);\n setRevealed(payload.apiKey ?? null);\n setShowKey(true);\n } catch (e) {\n setRevealErr(e instanceof Error ? e.message : labels.loadFailed);\n setRevealed(null);\n } finally {\n setRevealLoading(false);\n }\n }, [masked, providerId, revealed, labels.loadFailed]);\n\n return (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-key-${providerId}`}>\n {labels.apiKeyLabel}\n </label>\n {apiKeyLinks.length > 0 ? (\n <div className=\"flex flex-col gap-1\">\n {apiKeyLinks.map((link) => (\n <a\n key={`${link.kind}-${link.href}`}\n href={link.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex w-fit items-center gap-1 text-xs font-medium text-accent-fg hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n >\n {providerApiKeyLinkLabel(link.kind, apiKeyLinkLabels)}\n <ExternalLink className=\"size-3\" aria-hidden />\n </a>\n ))}\n </div>\n ) : null}\n {masked ? <p className=\"text-[11px] text-fg-subtle\">{labels.maskedHelp}</p> : null}\n <div className=\"relative min-w-0\">\n <input\n id={`img-cred-key-${providerId}`}\n type={inputType}\n autoComplete=\"off\"\n spellCheck={false}\n className={cn(\n 'w-full rounded-lg border border-edge bg-surface-panel py-2 pl-3 pr-24 font-mono text-sm text-fg',\n 'placeholder:text-fg-subtle',\n settingsInputFocusClass,\n )}\n value={inputValue}\n placeholder={masked ? '••••••••' : labels.optionalPlaceholder}\n onChange={(e) => {\n const next = e.target.value;\n if (masked && typeof revealed === 'string' && showKey && next !== revealed) {\n setRevealed(undefined);\n setShowKey(false);\n }\n onChange(next);\n }}\n />\n <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 gap-0.5\">\n {copyEnabled ? (\n <button\n type=\"button\"\n className={cn(\n 'rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg',\n interaction.transition,\n interaction.press,\n interaction.focusRingPanel,\n )}\n title={copied ? labels.copied : labels.copy}\n aria-label={copied ? labels.copied : labels.copy}\n onClick={() => void copyKey()}\n >\n {copied ? <CheckCircle2 className=\"size-4\" /> : <Copy className=\"size-4\" />}\n </button>\n ) : null}\n <button\n type=\"button\"\n className={cn(\n 'rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg disabled:opacity-40',\n interaction.transition,\n interaction.press,\n interaction.focusRingPanel,\n )}\n title={showKey ? labels.hide : labels.show}\n aria-label={showKey ? labels.hide : labels.show}\n disabled={revealLoading}\n onClick={() => void toggleEye()}\n >\n {revealLoading ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden />\n ) : showKey ? (\n <EyeOff className=\"size-4\" aria-hidden />\n ) : (\n <Eye className=\"size-4\" aria-hidden />\n )}\n </button>\n </div>\n </div>\n {masked && showKey && revealed === null && !revealErr ? (\n <p className=\"text-xs text-amber-700 dark:text-amber-400/90\">{labels.notInConfigFile}</p>\n ) : null}\n {revealErr ? <p className=\"text-xs text-red-600 dark:text-red-400\">{revealErr}</p> : null}\n </div>\n );\n}\n","import { ExternalLink, Loader2, Save } from 'lucide-react';\nimport { Link } from 'react-router-dom';\n\nimport { Button } from '@/components/ui/button';\nimport { ImageProviderApiKeyField } from '@/features/settings/image-provider-api-key-field';\nimport { emptyImageProviderCredRow, type ImageProviderCredRow } from '@/features/settings/image-providers-config-api';\nimport { getOrderedApiKeyLinks } from '@/features/settings/provider-enrichment';\nimport type {\n ImageGenProviderCredentialSummary,\n ImageProviderUiMetadata,\n} from '@/features/settings/use-image-provider-credentials';\nimport type { ProvidersSettingsMessages } from '@/i18n/messages';\nimport { settingsInputFocusClass } from '@/lib/form-field-width';\nimport type { StoredLanguage } from '@/lib/storage';\nimport { cn } from '@/lib/cn';\n\nfunction inputClass(): string {\n return cn(\n 'w-full rounded-lg border border-edge bg-surface-panel px-3 py-2 text-sm text-fg',\n 'placeholder:text-fg-subtle',\n settingsInputFocusClass,\n );\n}\n\nfunction selectClass(): string {\n return cn(inputClass(), 'appearance-none bg-[length:1rem] bg-[right_0.5rem_center] bg-no-repeat pr-9');\n}\n\nconst CUSTOM_SENTINEL = '__custom__';\n\nfunction dashscopeSelectValue(\n row: ImageProviderCredRow,\n regions: NonNullable<ImageProviderUiMetadata['regions']>,\n): string {\n if (!row.region.trim() && !row.imageBaseUrl.trim()) return '';\n const r = row.region.trim().toLowerCase();\n if (regions.some((x) => x.value === r)) return r;\n return CUSTOM_SENTINEL;\n}\n\nfunction baseUrlSelectValue(\n row: ImageProviderCredRow,\n presets: NonNullable<ImageProviderUiMetadata['baseUrlPresets']>,\n): string {\n const b = row.baseUrl.trim().replace(/\\/+$/, '');\n if (!b) return '';\n const norm = presets.map((p) => p.value.replace(/\\/+$/, ''));\n const idx = norm.indexOf(b);\n if (idx >= 0) return presets[idx].value;\n return CUSTOM_SENTINEL;\n}\n\nexport type ImageProviderCredentialsPanelMessages = {\n credentialsIntro: string;\n regionHint: string;\n endpointPresetsHint: string;\n apiKeyLabel: string;\n optionalPlaceholder: string;\n regionLabel: string;\n baseUrlLabel: string;\n imageBaseUrlLabel: string;\n saveCredentials: string;\n savingCredentials: string;\n credentialsSaved: string;\n discardCredentials: string;\n credentialsNothingToSave: string;\n credentialsSaveError: string;\n regionPresetDefault: string;\n regionPresetCustom: string;\n baseUrlPresetDefault: string;\n baseUrlPresetCustom: string;\n openExtensionSettings: string;\n openImageModelsPage: string;\n extensionSettingsLinkTitle: string;\n imageModelsLinkTitle: string;\n configured: string;\n missingKey: string;\n defaultModel: string;\n modelsLabel: string;\n imageBaseUrlPresetHint: string;\n dashscopeRegion_beijing: string;\n dashscopeRegion_singapore: string;\n dashscopeRegion_us: string;\n apiKeyMaskedHelp: string;\n apiKeyCopy: string;\n apiKeyCopied: string;\n apiKeyShow: string;\n apiKeyHide: string;\n apiKeyNotInConfigFile: string;\n apiKeyRevealFailed: string;\n minimaxClusterLabel: string;\n minimaxClusterHint: string;\n falQueueBaseLabel: string;\n falQueueBaseHint: string;\n};\n\nfunction translateDashscopeRegion(m: ImageProviderCredentialsPanelMessages, value: string, serverLabel: string) {\n if (value === 'beijing') return m.dashscopeRegion_beijing;\n if (value === 'singapore') return m.dashscopeRegion_singapore;\n if (value === 'us') return m.dashscopeRegion_us;\n return serverLabel;\n}\n\nfunction baseUrlPresetBlockTitle(\n t: ImageProviderCredentialsPanelMessages,\n kind: ImageProviderUiMetadata['baseUrlPresetKind'],\n): string {\n if (kind === 'minimax') return t.minimaxClusterLabel;\n if (kind === 'fal') return t.falQueueBaseLabel;\n return t.baseUrlLabel;\n}\n\nfunction baseUrlPresetBlockHint(\n t: ImageProviderCredentialsPanelMessages,\n kind: ImageProviderUiMetadata['baseUrlPresetKind'],\n): string | null {\n if (kind === 'minimax') return t.minimaxClusterHint;\n if (kind === 'fal') return t.falQueueBaseHint;\n return null;\n}\n\nexport function ImageProviderCredentialsPanel({\n summaries,\n credDraft,\n credDirty,\n credSaving,\n credError,\n credSavedFlash,\n credNoopFlash,\n updateCredRow,\n onDiscardCredentials,\n onSaveCredentials,\n extensionIds,\n showExtensionLinks,\n showImageModelsLink,\n language,\n apiKeyLinkLabels,\n messages: t,\n}: {\n summaries: ImageGenProviderCredentialSummary[];\n credDraft: Record<string, ImageProviderCredRow>;\n credDirty: boolean;\n credSaving: boolean;\n credError: string | null;\n credSavedFlash: boolean;\n credNoopFlash: boolean;\n updateCredRow: (id: string, patch: Partial<ImageProviderCredRow>) => void;\n onDiscardCredentials: () => void;\n onSaveCredentials: () => void;\n /** Extension ids present in gateway discovery (for deep links). */\n extensionIds: Set<string>;\n showExtensionLinks: boolean;\n showImageModelsLink: boolean;\n language: StoredLanguage;\n apiKeyLinkLabels: Pick<ProvidersSettingsMessages, 'getApiKey' | 'getApiKeyIntl' | 'getApiKeyCn'>;\n messages: ImageProviderCredentialsPanelMessages;\n}) {\n const empty = summaries.length === 0;\n\n if (empty) {\n return null;\n }\n\n const anyRegionUi = summaries.some((s) => (s.ui?.regions?.length ?? 0) > 0);\n const anyBaseUrlPresets = summaries.some((s) => (s.ui?.baseUrlPresets?.length ?? 0) > 0);\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-1 text-xs leading-relaxed text-fg-muted\">\n <p>{t.credentialsIntro}</p>\n {anyRegionUi ? <p className=\"text-fg-subtle\">{t.regionHint}</p> : null}\n {anyBaseUrlPresets ? <p className=\"text-fg-subtle\">{t.endpointPresetsHint}</p> : null}\n {showImageModelsLink ? (\n <p>\n <Link\n to=\"/settings/image-models\"\n className=\"font-medium text-accent hover:underline\"\n title={t.imageModelsLinkTitle}\n >\n {t.openImageModelsPage}\n </Link>\n </p>\n ) : null}\n </div>\n {credError ? (\n <div className=\"rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-700 dark:text-red-300\">\n {credError}\n </div>\n ) : null}\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {credSavedFlash ? (\n <span className=\"text-sm text-fg-muted\">{t.credentialsSaved}</span>\n ) : null}\n {credNoopFlash ? (\n <span className=\"text-sm text-fg-muted\">{t.credentialsNothingToSave}</span>\n ) : null}\n <Button type=\"button\" variant=\"secondary\" onClick={onDiscardCredentials} disabled={!credDirty || credSaving}>\n {t.discardCredentials}\n </Button>\n <Button type=\"button\" variant=\"primary\" onClick={onSaveCredentials} disabled={!credDirty || credSaving}>\n {credSaving ? (\n <>\n <Loader2 className=\"size-3.5 animate-spin\" />\n <span className=\"ml-1.5\">{t.savingCredentials}</span>\n </>\n ) : (\n <>\n <Save className=\"size-3.5\" />\n <span className=\"ml-1.5\">{t.saveCredentials}</span>\n </>\n )}\n </Button>\n </div>\n <div className=\"flex flex-col gap-4\">\n {summaries.map((p) => {\n const row = credDraft[p.id] ?? emptyImageProviderCredRow();\n const ui = p.ui;\n const extPath =\n showExtensionLinks && extensionIds.has(p.id)\n ? `/settings/ext/${encodeURIComponent(p.id)}`\n : null;\n return (\n <div\n key={p.id}\n className=\"rounded-lg border border-edge bg-surface-panel px-4 py-3 shadow-sm dark:shadow-none\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n <span className=\"text-sm font-semibold text-fg\">{p.label ?? p.id}</span>\n <span className=\"text-xs text-fg-subtle\">({p.id})</span>\n {extPath ? (\n <Link\n to={extPath}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline\"\n title={t.extensionSettingsLinkTitle}\n >\n <ExternalLink className=\"size-3\" />\n {t.openExtensionSettings}\n </Link>\n ) : null}\n </div>\n {p.configured ? (\n <span className=\"rounded-full bg-accent-soft px-2 py-0.5 text-xs font-medium text-accent-fg\">\n {t.configured}\n </span>\n ) : (\n <span className=\"rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300\">\n {t.missingKey}\n </span>\n )}\n </div>\n {p.defaultModel ? (\n <p className=\"mt-1 text-xs text-fg-subtle\">\n <span className=\"text-fg-muted\">{t.defaultModel}:</span> {p.id}/{p.defaultModel}\n </p>\n ) : null}\n {p.models.length > 0 ? (\n <p className=\"mt-0.5 text-xs text-fg-subtle\">\n <span className=\"text-fg-muted\">{t.modelsLabel}:</span>{' '}\n {p.models.map((mm) => `${p.id}/${mm}`).join(', ')}\n </p>\n ) : null}\n <div className=\"mt-4 grid gap-3 sm:grid-cols-2\">\n <ImageProviderApiKeyField\n providerId={p.id}\n value={row.apiKey}\n onChange={(next) => updateCredRow(p.id, { apiKey: next })}\n apiKeyLinks={getOrderedApiKeyLinks(p.id, language)}\n apiKeyLinkLabels={apiKeyLinkLabels}\n labels={{\n apiKeyLabel: t.apiKeyLabel,\n optionalPlaceholder: t.optionalPlaceholder,\n maskedHelp: t.apiKeyMaskedHelp,\n copy: t.apiKeyCopy,\n copied: t.apiKeyCopied,\n show: t.apiKeyShow,\n hide: t.apiKeyHide,\n notInConfigFile: t.apiKeyNotInConfigFile,\n loadFailed: t.apiKeyRevealFailed,\n }}\n />\n\n {ui?.regions?.length ? (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-region-preset-${p.id}`}>\n {t.regionLabel}\n </label>\n <select\n id={`img-cred-region-preset-${p.id}`}\n className={selectClass()}\n value={dashscopeSelectValue(row, ui.regions)}\n onChange={(e) => {\n const v = e.target.value;\n if (v === '') {\n updateCredRow(p.id, { region: '', imageBaseUrl: '' });\n return;\n }\n if (v === CUSTOM_SENTINEL) {\n updateCredRow(p.id, { region: '', imageBaseUrl: '' });\n return;\n }\n const opt = ui.regions!.find((x) => x.value === v);\n if (opt) {\n updateCredRow(p.id, { region: opt.value, imageBaseUrl: opt.imageBaseUrl });\n }\n }}\n >\n <option value=\"\">{t.regionPresetDefault}</option>\n {ui.regions.map((r) => (\n <option key={r.value} value={r.value}>\n {translateDashscopeRegion(t, r.value, r.label)}\n </option>\n ))}\n <option value={CUSTOM_SENTINEL}>{t.regionPresetCustom}</option>\n </select>\n {dashscopeSelectValue(row, ui.regions) === CUSTOM_SENTINEL ? (\n <div className=\"mt-2 grid gap-2 sm:grid-cols-2\">\n <input\n type=\"text\"\n className={inputClass()}\n value={row.region}\n placeholder=\"region\"\n onChange={(e) => updateCredRow(p.id, { region: e.target.value })}\n />\n <input\n type=\"url\"\n className={inputClass()}\n value={row.imageBaseUrl}\n placeholder={t.imageBaseUrlLabel}\n onChange={(e) => updateCredRow(p.id, { imageBaseUrl: e.target.value })}\n />\n </div>\n ) : null}\n </div>\n ) : null}\n\n {ui?.baseUrlPresets?.length ? (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-base-preset-${p.id}`}>\n {baseUrlPresetBlockTitle(t, ui.baseUrlPresetKind)}\n </label>\n {baseUrlPresetBlockHint(t, ui.baseUrlPresetKind) ? (\n <p className=\"text-[11px] text-fg-subtle\">{baseUrlPresetBlockHint(t, ui.baseUrlPresetKind)}</p>\n ) : null}\n <select\n id={`img-cred-base-preset-${p.id}`}\n className={selectClass()}\n value={baseUrlSelectValue(row, ui.baseUrlPresets)}\n onChange={(e) => {\n const v = e.target.value;\n if (v === '') {\n updateCredRow(p.id, { baseUrl: '' });\n return;\n }\n if (v === CUSTOM_SENTINEL) {\n updateCredRow(p.id, { baseUrl: '' });\n return;\n }\n updateCredRow(p.id, { baseUrl: v.replace(/\\/+$/, '') });\n }}\n >\n <option value=\"\">{t.baseUrlPresetDefault}</option>\n {ui.baseUrlPresets.map((b) => (\n <option key={b.value} value={b.value}>\n {b.label}\n </option>\n ))}\n <option value={CUSTOM_SENTINEL}>{t.baseUrlPresetCustom}</option>\n </select>\n {baseUrlSelectValue(row, ui.baseUrlPresets) === CUSTOM_SENTINEL ? (\n <input\n type=\"url\"\n className={cn(inputClass(), 'mt-2')}\n value={row.baseUrl}\n placeholder=\"https://…\"\n onChange={(e) => updateCredRow(p.id, { baseUrl: e.target.value })}\n />\n ) : null}\n </div>\n ) : null}\n\n {ui?.regions?.length && dashscopeSelectValue(row, ui.regions) !== CUSTOM_SENTINEL ? (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-imgbase-ro-${p.id}`}>\n {t.imageBaseUrlLabel}\n </label>\n <input\n id={`img-cred-imgbase-ro-${p.id}`}\n type=\"url\"\n readOnly\n className={cn(inputClass(), 'cursor-not-allowed opacity-90')}\n value={row.imageBaseUrl}\n title={t.imageBaseUrlPresetHint}\n />\n <p className=\"text-[11px] text-fg-subtle\">{t.imageBaseUrlPresetHint}</p>\n </div>\n ) : null}\n </div>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport { mutate } from 'swr';\n\nimport { useGatewayConfigSwr } from '@/features/gateway/gateway-config-swr';\nimport {\n buildImageProvidersConfigPatch,\n emptyImageProviderCredRow,\n imageProviderCredRowsFromConfigRoot,\n patchImageProvidersConfig,\n type ImageProviderCredRow,\n} from '@/features/settings/image-providers-config-api';\nimport { IMAGE_PROVIDERS_SWR_KEY } from '@/features/settings/image-providers-swr-key';\nimport { apiUrl } from '@/lib/url';\nimport { useGatewayStore } from '@/stores/gateway-store';\n\nexport type ImageProviderUiRegionOption = {\n value: string;\n label: string;\n imageBaseUrl: string;\n};\n\nexport type ImageProviderUiBaseUrlPreset = {\n value: string;\n label: string;\n};\n\nexport type ImageProviderUiMetadata = {\n regions?: ImageProviderUiRegionOption[];\n baseUrlPresets?: ImageProviderUiBaseUrlPreset[];\n baseUrlPresetKind?: 'fal' | 'minimax' | 'google' | 'openai';\n};\n\nexport type ImageGenProviderCredentialSummary = {\n id: string;\n label?: string;\n defaultModel?: string;\n models: string[];\n configured?: boolean;\n ui?: ImageProviderUiMetadata;\n};\n\nexport function useImageProviderCredentials(summaries: ImageGenProviderCredentialSummary[]) {\n const hasToken = useGatewayStore((s) => Boolean(s.token));\n const gwSwr = useGatewayConfigSwr(hasToken);\n const gwCfg = gwSwr.data;\n\n const ids = useMemo(() => summaries.map((s) => s.id), [summaries]);\n\n const [credDraft, setCredDraft] = useState<Record<string, ImageProviderCredRow>>({});\n const [credBaseline, setCredBaseline] = useState<Record<string, ImageProviderCredRow>>({});\n const [credSaving, setCredSaving] = useState(false);\n const [credError, setCredError] = useState<string | null>(null);\n const [credSavedFlash, setCredSavedFlash] = useState(false);\n const [credNoopFlash, setCredNoopFlash] = useState(false);\n\n const credRowsFromServer = useMemo(\n () => imageProviderCredRowsFromConfigRoot(gwCfg?.payload?.config, ids),\n [gwCfg?.payload?.config, ids],\n );\n\n const credDirty = useMemo(\n () => JSON.stringify(credDraft) !== JSON.stringify(credBaseline),\n [credDraft, credBaseline],\n );\n\n useEffect(() => {\n if (!credDirty) {\n setCredDraft(structuredClone(credRowsFromServer));\n setCredBaseline(structuredClone(credRowsFromServer));\n }\n }, [credRowsFromServer, credDirty]);\n\n const updateCredRow = useCallback((id: string, patch: Partial<ImageProviderCredRow>) => {\n setCredDraft((prev) => {\n const base = prev[id] ?? emptyImageProviderCredRow();\n return { ...prev, [id]: { ...base, ...patch } };\n });\n }, []);\n\n const onDiscardCredentials = useCallback(() => {\n setCredDraft(structuredClone(credBaseline));\n setCredError(null);\n setCredSavedFlash(false);\n setCredNoopFlash(false);\n }, [credBaseline]);\n\n const saveCredentials = useCallback(\n async (errorFallback: string) => {\n const patch = buildImageProvidersConfigPatch(ids, credDraft, credBaseline);\n if (Object.keys(patch).length === 0) {\n setCredNoopFlash(true);\n window.setTimeout(() => setCredNoopFlash(false), 2200);\n return;\n }\n setCredSaving(true);\n setCredError(null);\n setCredSavedFlash(false);\n try {\n await patchImageProvidersConfig(patch);\n const updated = await gwSwr.mutate?.();\n void mutate(apiUrl(IMAGE_PROVIDERS_SWR_KEY));\n const nextRows = imageProviderCredRowsFromConfigRoot(updated?.payload?.config, ids);\n setCredDraft(structuredClone(nextRows));\n setCredBaseline(structuredClone(nextRows));\n setCredSavedFlash(true);\n window.setTimeout(() => setCredSavedFlash(false), 2000);\n } catch (e) {\n setCredError(e instanceof Error ? e.message : errorFallback);\n } finally {\n setCredSaving(false);\n }\n },\n [ids, credDraft, credBaseline, gwSwr],\n );\n\n return {\n gwSwr,\n credDraft,\n credBaseline,\n credDirty,\n credSaving,\n credError,\n credSavedFlash,\n credNoopFlash,\n updateCredRow,\n onDiscardCredentials,\n saveCredentials,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-image-provider-credentials-CqMkyIiU.js","names":[],"sources":["../../../../../web/src/features/settings/image-providers-swr-key.ts","../../../../../web/src/features/settings/fetch-image-providers.ts","../../../../../web/src/features/settings/image-providers-config-api.ts","../../../../../web/src/features/settings/image-provider-api-key-field.tsx","../../../../../web/src/features/settings/image-provider-credentials-panel.tsx","../../../../../web/src/features/settings/use-image-provider-credentials.ts"],"sourcesContent":["/** Shared SWR key for GET `/api/image/providers` (image settings + extension image pages). */\nexport const IMAGE_PROVIDERS_SWR_KEY = '/api/image/providers';\n","import { fetchJson } from '@/lib/fetch';\nimport { apiUrl } from '@/lib/url';\n\nimport { IMAGE_PROVIDERS_SWR_KEY } from '@/features/settings/image-providers-swr-key';\nimport type { ImageGenProviderCredentialSummary } from '@/features/settings/use-image-provider-credentials';\n\nexport async function fetchImageProvidersList(): Promise<ImageGenProviderCredentialSummary[]> {\n const res = await fetchJson<{\n ok?: boolean;\n payload?: { providers?: ImageGenProviderCredentialSummary[] };\n }>(apiUrl(IMAGE_PROVIDERS_SWR_KEY));\n return res?.payload?.providers ?? [];\n}\n","import { isMaskedKey } from '@/features/settings/providers-api';\nimport { revalidateGatewayConfig } from '@/features/gateway/gateway-config-swr';\nimport { fetchJson } from '@/lib/fetch';\nimport { apiUrl } from '@/lib/url';\n\n/** One row of image-provider credential fields (matches PATCH `providersConfig` subset). */\nexport type ImageProviderCredRow = {\n apiKey: string;\n region: string;\n baseUrl: string;\n imageBaseUrl: string;\n};\n\nexport function emptyImageProviderCredRow(): ImageProviderCredRow {\n return { apiKey: '', region: '', baseUrl: '', imageBaseUrl: '' };\n}\n\nexport type SafeProviderAuthEntry = {\n apiKey: string;\n region?: string;\n baseUrl?: string;\n imageBaseUrl?: string;\n};\n\nfunction maskedApiKeyDisplay(safe?: SafeProviderAuthEntry): string {\n if (!safe?.apiKey) return '';\n return '••••••••••••';\n}\n\n/** Read `payload.config.providersConfig` from GET /api/config (masked). */\nexport function imageProviderCredRowsFromConfigRoot(\n config: unknown,\n imageProviderIds: string[],\n): Record<string, ImageProviderCredRow> {\n const pc = (() => {\n if (!config || typeof config !== 'object' || !('providersConfig' in config)) return undefined;\n const v = (config as { providersConfig?: unknown }).providersConfig;\n if (!v || typeof v !== 'object' || Array.isArray(v)) return undefined;\n return v as Record<string, SafeProviderAuthEntry>;\n })();\n\n const out: Record<string, ImageProviderCredRow> = {};\n for (const id of imageProviderIds) {\n const safe = pc?.[id];\n out[id] = {\n apiKey: maskedApiKeyDisplay(safe),\n region: safe?.region ?? '',\n baseUrl: safe?.baseUrl ?? '',\n imageBaseUrl: safe?.imageBaseUrl ?? '',\n };\n }\n return out;\n}\n\nfunction optionalStringField(\n draft: ImageProviderCredRow,\n baseline: ImageProviderCredRow,\n key: keyof Pick<ImageProviderCredRow, 'region' | 'baseUrl' | 'imageBaseUrl'>,\n): string | null | undefined {\n const d = draft[key].trim();\n const b = baseline[key].trim();\n if (d === b) return undefined;\n if (!d) return null;\n return d;\n}\n\nfunction apiKeyPatchValue(draftKey: string, baselineKey: string): string | null | undefined {\n const d = draftKey.trim();\n const b = baselineKey.trim();\n if (d === b) return undefined;\n if (isMaskedKey(d) && isMaskedKey(b)) return undefined;\n if (!d) {\n if (!b) return undefined;\n return null;\n }\n return d;\n}\n\n/**\n * Build `providersConfig` PATCH entries only for image providers whose row changed.\n * Omits `apiKey` when unchanged (still masked); sends `null` to clear stored key.\n */\nexport function buildImageProvidersConfigPatch(\n imageProviderIds: string[],\n draft: Record<string, ImageProviderCredRow>,\n baseline: Record<string, ImageProviderCredRow>,\n): Record<string, Record<string, unknown>> {\n const patch: Record<string, Record<string, unknown>> = {};\n for (const id of imageProviderIds) {\n const d = draft[id] ?? emptyImageProviderCredRow();\n const b = baseline[id] ?? emptyImageProviderCredRow();\n if (JSON.stringify(d) === JSON.stringify(b)) continue;\n\n const entry: Record<string, unknown> = {};\n const keyDelta = apiKeyPatchValue(d.apiKey, b.apiKey);\n if (keyDelta !== undefined) {\n entry.apiKey = keyDelta;\n }\n const region = optionalStringField(d, b, 'region');\n if (region !== undefined) entry.region = region;\n const baseUrl = optionalStringField(d, b, 'baseUrl');\n if (baseUrl !== undefined) entry.baseUrl = baseUrl;\n const imageBaseUrl = optionalStringField(d, b, 'imageBaseUrl');\n if (imageBaseUrl !== undefined) entry.imageBaseUrl = imageBaseUrl;\n\n if (Object.keys(entry).length > 0) {\n patch[id] = entry;\n }\n }\n return patch;\n}\n\nexport type RevealImageProviderApiKeyPayload = {\n id: string;\n apiKey: string | null;\n source: 'config' | 'none';\n};\n\n/** POST /api/image/providers/:id/reveal-api-key — plaintext only when stored in config file. */\nexport async function revealImageProviderConfigApiKey(providerId: string): Promise<RevealImageProviderApiKeyPayload> {\n const data = await fetchJson<{\n ok?: boolean;\n payload?: RevealImageProviderApiKeyPayload;\n error?: { message?: string };\n }>(apiUrl(`/api/image/providers/${encodeURIComponent(providerId)}/reveal-api-key`), {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: '{}',\n });\n if (!data.ok || !data.payload) {\n throw new Error(data.error?.message ?? 'Reveal failed');\n }\n return data.payload;\n}\n\nexport async function patchImageProvidersConfig(\n patch: Record<string, Record<string, unknown>>,\n): Promise<void> {\n if (Object.keys(patch).length === 0) return;\n await fetchJson(apiUrl('/api/config'), {\n method: 'PATCH',\n body: JSON.stringify({ providersConfig: patch }),\n });\n await revalidateGatewayConfig();\n}\n","import { CheckCircle2, Copy, ExternalLink, Eye, EyeOff, Loader2 } from 'lucide-react';\nimport { useCallback, useEffect, useState } from 'react';\n\nimport { revealImageProviderConfigApiKey } from '@/features/settings/image-providers-config-api';\nimport type { ApiKeyLinkKind } from '@/features/settings/provider-enrichment';\nimport { providerApiKeyLinkLabel } from '@/features/settings/provider-enrichment';\nimport { isMaskedKey } from '@/features/settings/providers-api';\nimport type { ProvidersSettingsMessages } from '@/i18n/messages';\nimport { settingsInputFocusClass } from '@/lib/form-field-width';\nimport { interaction } from '@/lib/interaction';\nimport { cn } from '@/lib/cn';\n\nexport type ImageProviderApiKeyFieldLabels = {\n apiKeyLabel: string;\n optionalPlaceholder: string;\n maskedHelp: string;\n copy: string;\n copied: string;\n show: string;\n hide: string;\n notInConfigFile: string;\n loadFailed: string;\n};\n\nexport function ImageProviderApiKeyField({\n providerId,\n value,\n onChange,\n labels,\n apiKeyLinks,\n apiKeyLinkLabels,\n}: {\n providerId: string;\n value: string;\n onChange: (next: string) => void;\n labels: ImageProviderApiKeyFieldLabels;\n apiKeyLinks: { href: string; kind: ApiKeyLinkKind }[];\n apiKeyLinkLabels: Pick<ProvidersSettingsMessages, 'getApiKey' | 'getApiKeyIntl' | 'getApiKeyCn'>;\n}) {\n const [showKey, setShowKey] = useState(false);\n /** `undefined` = not fetched; `null` = fetched, not in config file; string = plaintext from config */\n const [revealed, setRevealed] = useState<string | null | undefined>(undefined);\n const [revealLoading, setRevealLoading] = useState(false);\n const [revealErr, setRevealErr] = useState<string | null>(null);\n const [copied, setCopied] = useState(false);\n\n const masked = isMaskedKey(value);\n\n useEffect(() => {\n if (!masked) {\n setRevealed(undefined);\n setRevealErr(null);\n }\n }, [masked, value]);\n\n const inputValue = (() => {\n if (!masked) return value;\n if (showKey && typeof revealed === 'string') return revealed;\n return value;\n })();\n\n const inputType =\n !masked || (masked && showKey && typeof revealed === 'string') ? ('text' as const) : ('password' as const);\n\n const copyEnabled =\n (!masked && value.trim().length > 0 && !isMaskedKey(value)) ||\n (Boolean(showKey) && typeof revealed === 'string' && revealed.length > 0);\n\n const copyKey = useCallback(async () => {\n const text =\n !masked && value.trim() && !isMaskedKey(value)\n ? value.trim()\n : typeof revealed === 'string' && revealed.length > 0\n ? revealed\n : '';\n if (!text) return;\n try {\n await navigator.clipboard.writeText(text);\n setCopied(true);\n window.setTimeout(() => setCopied(false), 2000);\n } catch {\n /* ignore */\n }\n }, [masked, revealed, value]);\n\n const toggleEye = useCallback(async () => {\n setRevealErr(null);\n if (!masked) {\n setShowKey((s) => !s);\n return;\n }\n if (revealed !== undefined) {\n setShowKey((s) => !s);\n return;\n }\n setRevealLoading(true);\n try {\n const payload = await revealImageProviderConfigApiKey(providerId);\n setRevealed(payload.apiKey ?? null);\n setShowKey(true);\n } catch (e) {\n setRevealErr(e instanceof Error ? e.message : labels.loadFailed);\n setRevealed(null);\n } finally {\n setRevealLoading(false);\n }\n }, [masked, providerId, revealed, labels.loadFailed]);\n\n return (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-key-${providerId}`}>\n {labels.apiKeyLabel}\n </label>\n {apiKeyLinks.length > 0 ? (\n <div className=\"flex flex-col gap-1\">\n {apiKeyLinks.map((link) => (\n <a\n key={`${link.kind}-${link.href}`}\n href={link.href}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex w-fit items-center gap-1 text-xs font-medium text-accent-fg hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent\"\n >\n {providerApiKeyLinkLabel(link.kind, apiKeyLinkLabels)}\n <ExternalLink className=\"size-3\" aria-hidden />\n </a>\n ))}\n </div>\n ) : null}\n {masked ? <p className=\"text-[11px] text-fg-subtle\">{labels.maskedHelp}</p> : null}\n <div className=\"relative min-w-0\">\n <input\n id={`img-cred-key-${providerId}`}\n type={inputType}\n autoComplete=\"off\"\n spellCheck={false}\n className={cn(\n 'w-full rounded-lg border border-edge bg-surface-panel py-2 pl-3 pr-24 font-mono text-sm text-fg',\n 'placeholder:text-fg-subtle',\n settingsInputFocusClass,\n )}\n value={inputValue}\n placeholder={masked ? '••••••••' : labels.optionalPlaceholder}\n onChange={(e) => {\n const next = e.target.value;\n if (masked && typeof revealed === 'string' && showKey && next !== revealed) {\n setRevealed(undefined);\n setShowKey(false);\n }\n onChange(next);\n }}\n />\n <div className=\"absolute right-1 top-1/2 flex -translate-y-1/2 gap-0.5\">\n {copyEnabled ? (\n <button\n type=\"button\"\n className={cn(\n 'rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg',\n interaction.transition,\n interaction.press,\n interaction.focusRingPanel,\n )}\n title={copied ? labels.copied : labels.copy}\n aria-label={copied ? labels.copied : labels.copy}\n onClick={() => void copyKey()}\n >\n {copied ? <CheckCircle2 className=\"size-4\" /> : <Copy className=\"size-4\" />}\n </button>\n ) : null}\n <button\n type=\"button\"\n className={cn(\n 'rounded p-1.5 text-fg-subtle hover:bg-surface-hover hover:text-fg disabled:opacity-40',\n interaction.transition,\n interaction.press,\n interaction.focusRingPanel,\n )}\n title={showKey ? labels.hide : labels.show}\n aria-label={showKey ? labels.hide : labels.show}\n disabled={revealLoading}\n onClick={() => void toggleEye()}\n >\n {revealLoading ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden />\n ) : showKey ? (\n <EyeOff className=\"size-4\" aria-hidden />\n ) : (\n <Eye className=\"size-4\" aria-hidden />\n )}\n </button>\n </div>\n </div>\n {masked && showKey && revealed === null && !revealErr ? (\n <p className=\"text-xs text-amber-700 dark:text-amber-400/90\">{labels.notInConfigFile}</p>\n ) : null}\n {revealErr ? <p className=\"text-xs text-red-600 dark:text-red-400\">{revealErr}</p> : null}\n </div>\n );\n}\n","import { ExternalLink, Loader2, Save } from 'lucide-react';\nimport { Link } from 'react-router-dom';\n\nimport { Button } from '@/components/ui/button';\nimport { ImageProviderApiKeyField } from '@/features/settings/image-provider-api-key-field';\nimport { emptyImageProviderCredRow, type ImageProviderCredRow } from '@/features/settings/image-providers-config-api';\nimport { getOrderedApiKeyLinks } from '@/features/settings/provider-enrichment';\nimport type {\n ImageGenProviderCredentialSummary,\n ImageProviderUiMetadata,\n} from '@/features/settings/use-image-provider-credentials';\nimport type { ProvidersSettingsMessages } from '@/i18n/messages';\nimport { settingsInputFocusClass } from '@/lib/form-field-width';\nimport type { StoredLanguage } from '@/lib/storage';\nimport { cn } from '@/lib/cn';\n\nfunction inputClass(): string {\n return cn(\n 'w-full rounded-lg border border-edge bg-surface-panel px-3 py-2 text-sm text-fg',\n 'placeholder:text-fg-subtle',\n settingsInputFocusClass,\n );\n}\n\nfunction selectClass(): string {\n return cn(inputClass(), 'appearance-none bg-[length:1rem] bg-[right_0.5rem_center] bg-no-repeat pr-9');\n}\n\nconst CUSTOM_SENTINEL = '__custom__';\n\nfunction dashscopeSelectValue(\n row: ImageProviderCredRow,\n regions: NonNullable<ImageProviderUiMetadata['regions']>,\n): string {\n if (!row.region.trim() && !row.imageBaseUrl.trim()) return '';\n const r = row.region.trim().toLowerCase();\n if (regions.some((x) => x.value === r)) return r;\n return CUSTOM_SENTINEL;\n}\n\nfunction baseUrlSelectValue(\n row: ImageProviderCredRow,\n presets: NonNullable<ImageProviderUiMetadata['baseUrlPresets']>,\n): string {\n const b = row.baseUrl.trim().replace(/\\/+$/, '');\n if (!b) return '';\n const norm = presets.map((p) => p.value.replace(/\\/+$/, ''));\n const idx = norm.indexOf(b);\n if (idx >= 0) return presets[idx].value;\n return CUSTOM_SENTINEL;\n}\n\nexport type ImageProviderCredentialsPanelMessages = {\n credentialsIntro: string;\n regionHint: string;\n endpointPresetsHint: string;\n apiKeyLabel: string;\n optionalPlaceholder: string;\n regionLabel: string;\n baseUrlLabel: string;\n imageBaseUrlLabel: string;\n saveCredentials: string;\n savingCredentials: string;\n credentialsSaved: string;\n discardCredentials: string;\n credentialsNothingToSave: string;\n credentialsSaveError: string;\n regionPresetDefault: string;\n regionPresetCustom: string;\n baseUrlPresetDefault: string;\n baseUrlPresetCustom: string;\n openExtensionSettings: string;\n openImageModelsPage: string;\n extensionSettingsLinkTitle: string;\n imageModelsLinkTitle: string;\n configured: string;\n missingKey: string;\n defaultModel: string;\n modelsLabel: string;\n imageBaseUrlPresetHint: string;\n dashscopeRegion_beijing: string;\n dashscopeRegion_singapore: string;\n dashscopeRegion_us: string;\n apiKeyMaskedHelp: string;\n apiKeyCopy: string;\n apiKeyCopied: string;\n apiKeyShow: string;\n apiKeyHide: string;\n apiKeyNotInConfigFile: string;\n apiKeyRevealFailed: string;\n minimaxClusterLabel: string;\n minimaxClusterHint: string;\n falQueueBaseLabel: string;\n falQueueBaseHint: string;\n};\n\nfunction translateDashscopeRegion(m: ImageProviderCredentialsPanelMessages, value: string, serverLabel: string) {\n if (value === 'beijing') return m.dashscopeRegion_beijing;\n if (value === 'singapore') return m.dashscopeRegion_singapore;\n if (value === 'us') return m.dashscopeRegion_us;\n return serverLabel;\n}\n\nfunction baseUrlPresetBlockTitle(\n t: ImageProviderCredentialsPanelMessages,\n kind: ImageProviderUiMetadata['baseUrlPresetKind'],\n): string {\n if (kind === 'minimax') return t.minimaxClusterLabel;\n if (kind === 'fal') return t.falQueueBaseLabel;\n return t.baseUrlLabel;\n}\n\nfunction baseUrlPresetBlockHint(\n t: ImageProviderCredentialsPanelMessages,\n kind: ImageProviderUiMetadata['baseUrlPresetKind'],\n): string | null {\n if (kind === 'minimax') return t.minimaxClusterHint;\n if (kind === 'fal') return t.falQueueBaseHint;\n return null;\n}\n\nexport function ImageProviderCredentialsPanel({\n summaries,\n credDraft,\n credDirty,\n credSaving,\n credError,\n credSavedFlash,\n credNoopFlash,\n updateCredRow,\n onDiscardCredentials,\n onSaveCredentials,\n extensionIds,\n showExtensionLinks,\n showImageModelsLink,\n language,\n apiKeyLinkLabels,\n messages: t,\n}: {\n summaries: ImageGenProviderCredentialSummary[];\n credDraft: Record<string, ImageProviderCredRow>;\n credDirty: boolean;\n credSaving: boolean;\n credError: string | null;\n credSavedFlash: boolean;\n credNoopFlash: boolean;\n updateCredRow: (id: string, patch: Partial<ImageProviderCredRow>) => void;\n onDiscardCredentials: () => void;\n onSaveCredentials: () => void;\n /** Extension ids present in gateway discovery (for deep links). */\n extensionIds: Set<string>;\n showExtensionLinks: boolean;\n showImageModelsLink: boolean;\n language: StoredLanguage;\n apiKeyLinkLabels: Pick<ProvidersSettingsMessages, 'getApiKey' | 'getApiKeyIntl' | 'getApiKeyCn'>;\n messages: ImageProviderCredentialsPanelMessages;\n}) {\n const empty = summaries.length === 0;\n\n if (empty) {\n return null;\n }\n\n const anyRegionUi = summaries.some((s) => (s.ui?.regions?.length ?? 0) > 0);\n const anyBaseUrlPresets = summaries.some((s) => (s.ui?.baseUrlPresets?.length ?? 0) > 0);\n\n return (\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex flex-col gap-1 text-xs leading-relaxed text-fg-muted\">\n <p>{t.credentialsIntro}</p>\n {anyRegionUi ? <p className=\"text-fg-subtle\">{t.regionHint}</p> : null}\n {anyBaseUrlPresets ? <p className=\"text-fg-subtle\">{t.endpointPresetsHint}</p> : null}\n {showImageModelsLink ? (\n <p>\n <Link\n to=\"/settings/image-models\"\n className=\"font-medium text-accent hover:underline\"\n title={t.imageModelsLinkTitle}\n >\n {t.openImageModelsPage}\n </Link>\n </p>\n ) : null}\n </div>\n {credError ? (\n <div className=\"rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-700 dark:text-red-300\">\n {credError}\n </div>\n ) : null}\n <div className=\"flex flex-wrap items-center justify-end gap-2\">\n {credSavedFlash ? (\n <span className=\"text-sm text-fg-muted\">{t.credentialsSaved}</span>\n ) : null}\n {credNoopFlash ? (\n <span className=\"text-sm text-fg-muted\">{t.credentialsNothingToSave}</span>\n ) : null}\n <Button type=\"button\" variant=\"secondary\" onClick={onDiscardCredentials} disabled={!credDirty || credSaving}>\n {t.discardCredentials}\n </Button>\n <Button type=\"button\" variant=\"primary\" onClick={onSaveCredentials} disabled={!credDirty || credSaving}>\n {credSaving ? (\n <>\n <Loader2 className=\"size-3.5 animate-spin\" />\n <span className=\"ml-1.5\">{t.savingCredentials}</span>\n </>\n ) : (\n <>\n <Save className=\"size-3.5\" />\n <span className=\"ml-1.5\">{t.saveCredentials}</span>\n </>\n )}\n </Button>\n </div>\n <div className=\"flex flex-col gap-4\">\n {summaries.map((p) => {\n const row = credDraft[p.id] ?? emptyImageProviderCredRow();\n const ui = p.ui;\n const extPath =\n showExtensionLinks && extensionIds.has(p.id)\n ? `/settings/ext/${encodeURIComponent(p.id)}`\n : null;\n return (\n <div\n key={p.id}\n className=\"rounded-lg border border-edge bg-surface-panel px-4 py-3 shadow-sm dark:shadow-none\"\n >\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n <div className=\"flex min-w-0 flex-wrap items-center gap-2\">\n <span className=\"text-sm font-semibold text-fg\">{p.label ?? p.id}</span>\n <span className=\"text-xs text-fg-subtle\">({p.id})</span>\n {extPath ? (\n <Link\n to={extPath}\n className=\"inline-flex items-center gap-1 text-xs font-medium text-accent hover:underline\"\n title={t.extensionSettingsLinkTitle}\n >\n <ExternalLink className=\"size-3\" />\n {t.openExtensionSettings}\n </Link>\n ) : null}\n </div>\n {p.configured ? (\n <span className=\"rounded-full bg-accent-soft px-2 py-0.5 text-xs font-medium text-accent-fg\">\n {t.configured}\n </span>\n ) : (\n <span className=\"rounded-full border border-amber-500/40 bg-amber-500/10 px-2 py-0.5 text-xs font-medium text-amber-700 dark:text-amber-300\">\n {t.missingKey}\n </span>\n )}\n </div>\n {p.defaultModel ? (\n <p className=\"mt-1 text-xs text-fg-subtle\">\n <span className=\"text-fg-muted\">{t.defaultModel}:</span> {p.id}/{p.defaultModel}\n </p>\n ) : null}\n {p.models.length > 0 ? (\n <p className=\"mt-0.5 text-xs text-fg-subtle\">\n <span className=\"text-fg-muted\">{t.modelsLabel}:</span>{' '}\n {p.models.map((mm) => `${p.id}/${mm}`).join(', ')}\n </p>\n ) : null}\n <div className=\"mt-4 grid gap-3 sm:grid-cols-2\">\n <ImageProviderApiKeyField\n providerId={p.id}\n value={row.apiKey}\n onChange={(next) => updateCredRow(p.id, { apiKey: next })}\n apiKeyLinks={getOrderedApiKeyLinks(p.id, language)}\n apiKeyLinkLabels={apiKeyLinkLabels}\n labels={{\n apiKeyLabel: t.apiKeyLabel,\n optionalPlaceholder: t.optionalPlaceholder,\n maskedHelp: t.apiKeyMaskedHelp,\n copy: t.apiKeyCopy,\n copied: t.apiKeyCopied,\n show: t.apiKeyShow,\n hide: t.apiKeyHide,\n notInConfigFile: t.apiKeyNotInConfigFile,\n loadFailed: t.apiKeyRevealFailed,\n }}\n />\n\n {ui?.regions?.length ? (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-region-preset-${p.id}`}>\n {t.regionLabel}\n </label>\n <select\n id={`img-cred-region-preset-${p.id}`}\n className={selectClass()}\n value={dashscopeSelectValue(row, ui.regions)}\n onChange={(e) => {\n const v = e.target.value;\n if (v === '') {\n updateCredRow(p.id, { region: '', imageBaseUrl: '' });\n return;\n }\n if (v === CUSTOM_SENTINEL) {\n updateCredRow(p.id, { region: '', imageBaseUrl: '' });\n return;\n }\n const opt = ui.regions!.find((x) => x.value === v);\n if (opt) {\n updateCredRow(p.id, { region: opt.value, imageBaseUrl: opt.imageBaseUrl });\n }\n }}\n >\n <option value=\"\">{t.regionPresetDefault}</option>\n {ui.regions.map((r) => (\n <option key={r.value} value={r.value}>\n {translateDashscopeRegion(t, r.value, r.label)}\n </option>\n ))}\n <option value={CUSTOM_SENTINEL}>{t.regionPresetCustom}</option>\n </select>\n {dashscopeSelectValue(row, ui.regions) === CUSTOM_SENTINEL ? (\n <div className=\"mt-2 grid gap-2 sm:grid-cols-2\">\n <input\n type=\"text\"\n className={inputClass()}\n value={row.region}\n placeholder=\"region\"\n onChange={(e) => updateCredRow(p.id, { region: e.target.value })}\n />\n <input\n type=\"url\"\n className={inputClass()}\n value={row.imageBaseUrl}\n placeholder={t.imageBaseUrlLabel}\n onChange={(e) => updateCredRow(p.id, { imageBaseUrl: e.target.value })}\n />\n </div>\n ) : null}\n </div>\n ) : null}\n\n {ui?.baseUrlPresets?.length ? (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-base-preset-${p.id}`}>\n {baseUrlPresetBlockTitle(t, ui.baseUrlPresetKind)}\n </label>\n {baseUrlPresetBlockHint(t, ui.baseUrlPresetKind) ? (\n <p className=\"text-[11px] text-fg-subtle\">{baseUrlPresetBlockHint(t, ui.baseUrlPresetKind)}</p>\n ) : null}\n <select\n id={`img-cred-base-preset-${p.id}`}\n className={selectClass()}\n value={baseUrlSelectValue(row, ui.baseUrlPresets)}\n onChange={(e) => {\n const v = e.target.value;\n if (v === '') {\n updateCredRow(p.id, { baseUrl: '' });\n return;\n }\n if (v === CUSTOM_SENTINEL) {\n updateCredRow(p.id, { baseUrl: '' });\n return;\n }\n updateCredRow(p.id, { baseUrl: v.replace(/\\/+$/, '') });\n }}\n >\n <option value=\"\">{t.baseUrlPresetDefault}</option>\n {ui.baseUrlPresets.map((b) => (\n <option key={b.value} value={b.value}>\n {b.label}\n </option>\n ))}\n <option value={CUSTOM_SENTINEL}>{t.baseUrlPresetCustom}</option>\n </select>\n {baseUrlSelectValue(row, ui.baseUrlPresets) === CUSTOM_SENTINEL ? (\n <input\n type=\"url\"\n className={cn(inputClass(), 'mt-2')}\n value={row.baseUrl}\n placeholder=\"https://…\"\n onChange={(e) => updateCredRow(p.id, { baseUrl: e.target.value })}\n />\n ) : null}\n </div>\n ) : null}\n\n {ui?.regions?.length && dashscopeSelectValue(row, ui.regions) !== CUSTOM_SENTINEL ? (\n <div className=\"flex min-w-0 flex-col gap-1 sm:col-span-2\">\n <label className=\"text-xs font-medium text-fg-muted\" htmlFor={`img-cred-imgbase-ro-${p.id}`}>\n {t.imageBaseUrlLabel}\n </label>\n <input\n id={`img-cred-imgbase-ro-${p.id}`}\n type=\"url\"\n readOnly\n className={cn(inputClass(), 'cursor-not-allowed opacity-90')}\n value={row.imageBaseUrl}\n title={t.imageBaseUrlPresetHint}\n />\n <p className=\"text-[11px] text-fg-subtle\">{t.imageBaseUrlPresetHint}</p>\n </div>\n ) : null}\n </div>\n </div>\n );\n })}\n </div>\n </div>\n );\n}\n","import { useCallback, useEffect, useMemo, useState } from 'react';\nimport { mutate } from 'swr';\n\nimport { useGatewayConfigSwr } from '@/features/gateway/gateway-config-swr';\nimport {\n buildImageProvidersConfigPatch,\n emptyImageProviderCredRow,\n imageProviderCredRowsFromConfigRoot,\n patchImageProvidersConfig,\n type ImageProviderCredRow,\n} from '@/features/settings/image-providers-config-api';\nimport { IMAGE_PROVIDERS_SWR_KEY } from '@/features/settings/image-providers-swr-key';\nimport { apiUrl } from '@/lib/url';\nimport { useGatewayStore } from '@/stores/gateway-store';\n\nexport type ImageProviderUiRegionOption = {\n value: string;\n label: string;\n imageBaseUrl: string;\n};\n\nexport type ImageProviderUiBaseUrlPreset = {\n value: string;\n label: string;\n};\n\nexport type ImageProviderUiMetadata = {\n regions?: ImageProviderUiRegionOption[];\n baseUrlPresets?: ImageProviderUiBaseUrlPreset[];\n baseUrlPresetKind?: 'fal' | 'minimax' | 'google' | 'openai';\n};\n\nexport type ImageGenProviderCredentialSummary = {\n id: string;\n label?: string;\n defaultModel?: string;\n models: string[];\n configured?: boolean;\n ui?: ImageProviderUiMetadata;\n};\n\nexport function useImageProviderCredentials(summaries: ImageGenProviderCredentialSummary[]) {\n const hasToken = useGatewayStore((s) => Boolean(s.token));\n const gwSwr = useGatewayConfigSwr(hasToken);\n const gwCfg = gwSwr.data;\n\n const ids = useMemo(() => summaries.map((s) => s.id), [summaries]);\n\n const [credDraft, setCredDraft] = useState<Record<string, ImageProviderCredRow>>({});\n const [credBaseline, setCredBaseline] = useState<Record<string, ImageProviderCredRow>>({});\n const [credSaving, setCredSaving] = useState(false);\n const [credError, setCredError] = useState<string | null>(null);\n const [credSavedFlash, setCredSavedFlash] = useState(false);\n const [credNoopFlash, setCredNoopFlash] = useState(false);\n\n const credRowsFromServer = useMemo(\n () => imageProviderCredRowsFromConfigRoot(gwCfg?.payload?.config, ids),\n [gwCfg?.payload?.config, ids],\n );\n\n const credDirty = useMemo(\n () => JSON.stringify(credDraft) !== JSON.stringify(credBaseline),\n [credDraft, credBaseline],\n );\n\n useEffect(() => {\n if (!credDirty) {\n setCredDraft(structuredClone(credRowsFromServer));\n setCredBaseline(structuredClone(credRowsFromServer));\n }\n }, [credRowsFromServer, credDirty]);\n\n const updateCredRow = useCallback((id: string, patch: Partial<ImageProviderCredRow>) => {\n setCredDraft((prev) => {\n const base = prev[id] ?? emptyImageProviderCredRow();\n return { ...prev, [id]: { ...base, ...patch } };\n });\n }, []);\n\n const onDiscardCredentials = useCallback(() => {\n setCredDraft(structuredClone(credBaseline));\n setCredError(null);\n setCredSavedFlash(false);\n setCredNoopFlash(false);\n }, [credBaseline]);\n\n const saveCredentials = useCallback(\n async (errorFallback: string) => {\n const patch = buildImageProvidersConfigPatch(ids, credDraft, credBaseline);\n if (Object.keys(patch).length === 0) {\n setCredNoopFlash(true);\n window.setTimeout(() => setCredNoopFlash(false), 2200);\n return;\n }\n setCredSaving(true);\n setCredError(null);\n setCredSavedFlash(false);\n try {\n await patchImageProvidersConfig(patch);\n const updated = await gwSwr.mutate?.();\n void mutate(apiUrl(IMAGE_PROVIDERS_SWR_KEY));\n const nextRows = imageProviderCredRowsFromConfigRoot(updated?.payload?.config, ids);\n setCredDraft(structuredClone(nextRows));\n setCredBaseline(structuredClone(nextRows));\n setCredSavedFlash(true);\n window.setTimeout(() => setCredSavedFlash(false), 2000);\n } catch (e) {\n setCredError(e instanceof Error ? e.message : errorFallback);\n } finally {\n setCredSaving(false);\n }\n },\n [ids, credDraft, credBaseline, gwSwr],\n );\n\n return {\n gwSwr,\n credDraft,\n credBaseline,\n credDirty,\n credSaving,\n credError,\n credSavedFlash,\n credNoopFlash,\n updateCredRow,\n onDiscardCredentials,\n saveCredentials,\n };\n}\n"],"mappings":"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"}
@@ -9,7 +9,7 @@
9
9
  <link rel="icon" type="image/svg+xml" href="/logo.svg" />
10
10
  <link rel="apple-touch-icon" href="/logo.svg" />
11
11
  <title>xopc</title>
12
- <script type="module" crossorigin src="/assets/index-V5ZnG6RE.js"></script>
12
+ <script type="module" crossorigin src="/assets/index-8IFT6i7x.js"></script>
13
13
  <link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-DWdDZTNf.js">
14
14
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-CXAvob9m.js">
15
15
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DbimaAId.js">
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.0.52";
2
+ var version = "0.0.54";
3
3
  //#endregion
4
4
  export { version };
5
5
 
@@ -72,15 +72,49 @@ var ExtensionBrowserProvider = class {
72
72
  }
73
73
  });
74
74
  });
75
+ const wssEmitter = this.wss;
75
76
  await new Promise((resolve, reject) => {
76
- this.server.listen(this.config.port, this.config.host, () => {
77
+ let settled = false;
78
+ const disposeFailedStart = async () => {
79
+ if (this.wss) {
80
+ try {
81
+ this.wss.close();
82
+ } catch {}
83
+ this.wss = null;
84
+ }
85
+ if (this.server) {
86
+ await new Promise((r) => {
87
+ this.server.close(() => r());
88
+ });
89
+ this.server = null;
90
+ }
91
+ };
92
+ const onStartError = (err) => {
93
+ if (settled) return;
94
+ settled = true;
95
+ this.server.removeListener("error", onStartError);
96
+ wssEmitter.removeListener("error", onStartError);
97
+ disposeFailedStart().then(() => reject(err));
98
+ };
99
+ const onListening = () => {
100
+ if (settled) return;
101
+ settled = true;
102
+ this.server.removeListener("error", onStartError);
103
+ wssEmitter.removeListener("error", onStartError);
77
104
  log.info({
78
105
  port: this.config.port,
79
106
  host: this.config.host
80
107
  }, "Extension WS server started");
108
+ const onRuntimeError = (err) => {
109
+ log.error({ err }, "Extension WS bridge runtime error");
110
+ };
111
+ this.server.on("error", onRuntimeError);
112
+ wssEmitter.on("error", onRuntimeError);
81
113
  resolve();
82
- });
83
- this.server.on("error", reject);
114
+ };
115
+ this.server.on("error", onStartError);
116
+ wssEmitter.on("error", onStartError);
117
+ this.server.listen(this.config.port, this.config.host, onListening);
84
118
  });
85
119
  }
86
120
  /** Wait for the Chrome Extension to connect. */
@@ -1 +1 @@
1
- {"version":3,"file":"extension.js","names":[],"sources":["../../../../src/browser/providers/extension.ts"],"sourcesContent":["/**\n * Extension browser provider — connects to the xopc Chrome Extension via WebSocket.\n *\n * The xopc process starts a WebSocket server; the Chrome Extension connects as a client.\n * Commands are sent over the WS connection and results are returned asynchronously.\n */\n\nimport { createLogger } from '../../utils/logger.js';\n\nconst log = createLogger('ExtensionProvider');\n\n// ── Protocol types (mirrored from packages/browser-ext/src/protocol.ts) ──\n\nexport type ExtensionAction =\n | 'open' | 'navigate' | 'reload' | 'back' | 'forward' | 'get_url' | 'get_title'\n | 'state' | 'snapshot' | 'screenshot' | 'content'\n | 'evaluate'\n | 'click' | 'type' | 'scroll' | 'wait'\n | 'query_selector' | 'query_selector_all' | 'wait_for_selector'\n | 'mouse_move' | 'mouse_click' | 'mouse_down' | 'mouse_up' | 'mouse_wheel'\n | 'keys' | 'keyboard_type' | 'keyboard_down' | 'keyboard_up' | 'keyboard_insert_text'\n | 'get_bounding_box' | 'get_element_text' | 'get_element_attribute'\n | 'get_elements_count' | 'set_input_files' | 'scroll_into_view'\n | 'set_viewport_size' | 'get_viewport_size'\n | 'network_start' | 'network_events' | 'network_stop'\n | 'dialog'\n | 'get_cookies' | 'set_cookies' | 'clear_cookies'\n | 'close' | 'new_tab' | 'list_tabs'\n | 'cdp'\n | 'ping';\n\nexport interface ExtensionCommand {\n id: string;\n action: ExtensionAction;\n tabId?: number;\n args?: Record<string, unknown>;\n timeout?: number;\n}\n\nexport interface ExtensionResult {\n id: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n durationMs?: number;\n}\n\n// ── Configuration ────────────────────────────────────────────────────\n\nexport interface ExtensionProviderConfig {\n /** WebSocket server port. Default: 19820. */\n port?: number;\n /** Host to bind. Default: 127.0.0.1. */\n host?: string;\n /** Timeout waiting for extension connection (ms). Default: 30000. */\n connectionTimeout?: number;\n /** Default command timeout (ms). Default: 30000. */\n commandTimeout?: number;\n}\n\nconst DEFAULT_PORT = 19820;\nconst DEFAULT_HOST = '127.0.0.1';\nconst DEFAULT_CONNECTION_TIMEOUT = 30_000;\nconst DEFAULT_COMMAND_TIMEOUT = 30_000;\n\n// ── Provider ─────────────────────────────────────────────────────────\n\ninterface PendingRequest {\n resolve: (result: ExtensionResult) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Manages a WebSocket server that the Chrome Extension connects to.\n * Sends commands and receives results over the connection.\n */\nexport class ExtensionBrowserProvider {\n readonly name = 'extension';\n\n private server: import('http').Server | null = null;\n private wss: unknown = null; // WebSocketServer instance\n private clientWs: unknown = null; // connected client WebSocket\n private pending = new Map<string, PendingRequest>();\n private commandCounter = 0;\n private connected = false;\n private readonly config: Required<ExtensionProviderConfig>;\n private connectionWaiters: Array<{ resolve: () => void; reject: (e: Error) => void }> = [];\n\n constructor(config?: ExtensionProviderConfig) {\n this.config = {\n port: config?.port ?? DEFAULT_PORT,\n host: config?.host ?? DEFAULT_HOST,\n connectionTimeout: config?.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT,\n commandTimeout: config?.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT,\n };\n }\n\n /** Start the WebSocket server and wait for the extension to connect. */\n async start(): Promise<void> {\n if (this.server) return;\n\n // Handle CJS/ESM interop: tsx wraps CJS modules so WebSocketServer may need `.default`\n const wsModule: Record<string, unknown> = await import('ws') as never;\n let WssClass = wsModule.WebSocketServer;\n if (typeof WssClass !== 'function') {\n const def = wsModule.default as Record<string, unknown> | undefined;\n WssClass = def?.WebSocketServer ?? def;\n }\n if (typeof WssClass !== 'function') {\n throw new Error('Failed to resolve WebSocketServer from \"ws\" package');\n }\n const WebSocketServer = WssClass as unknown as new (opts: Record<string, unknown>) => unknown;\n const http = await import('node:http');\n\n this.server = http.createServer((_req, res) => {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, connected: this.connected }));\n });\n\n this.wss = new WebSocketServer({ server: this.server, path: '/browser-ext' });\n\n (this.wss as { on: Function }).on('connection', (ws: unknown) => {\n log.info('Chrome Extension connected');\n this.clientWs = ws;\n this.connected = true;\n\n // Resolve any pending connection waiters\n for (const waiter of this.connectionWaiters) {\n waiter.resolve();\n }\n this.connectionWaiters = [];\n\n (ws as { on: Function }).on('message', (data: Buffer | string) => {\n this._handleMessage(data.toString());\n });\n\n (ws as { on: Function }).on('close', () => {\n log.warn('Chrome Extension disconnected');\n this.clientWs = null;\n this.connected = false;\n // Reject all pending requests\n for (const [id, req] of this.pending) {\n clearTimeout(req.timer);\n req.reject(new Error('Extension disconnected'));\n this.pending.delete(id);\n }\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n this.server!.listen(this.config.port, this.config.host, () => {\n log.info({ port: this.config.port, host: this.config.host }, 'Extension WS server started');\n resolve();\n });\n this.server!.on('error', reject);\n });\n }\n\n /** Wait for the Chrome Extension to connect. */\n async waitForConnection(timeoutMs?: number): Promise<void> {\n if (this.connected) return;\n\n const timeout = timeoutMs ?? this.config.connectionTimeout;\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n const idx = this.connectionWaiters.findIndex(w => w.resolve === resolve);\n if (idx >= 0) this.connectionWaiters.splice(idx, 1);\n reject(new Error(`Extension connection timeout after ${timeout}ms. Is the Chrome Extension installed and enabled?`));\n }, timeout);\n\n this.connectionWaiters.push({\n resolve: () => { clearTimeout(timer); resolve(); },\n reject: (e) => { clearTimeout(timer); reject(e); },\n });\n });\n }\n\n /** Send a command to the extension and wait for the result. */\n async sendCommand(action: ExtensionAction, args?: Record<string, unknown>, options?: { tabId?: number; timeout?: number }): Promise<ExtensionResult> {\n if (!this.connected || !this.clientWs) {\n throw new Error('Extension not connected. Ensure the Chrome Extension is installed and connected.');\n }\n\n const id = `cmd_${++this.commandCounter}_${Date.now()}`;\n const cmd: ExtensionCommand = {\n id,\n action,\n args,\n tabId: options?.tabId,\n timeout: options?.timeout ?? this.config.commandTimeout,\n };\n\n return new Promise<ExtensionResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new Error(`Command timeout: ${action} (${cmd.timeout}ms)`));\n }, cmd.timeout!);\n\n this.pending.set(id, { resolve, reject, timer });\n\n try {\n (this.clientWs as { send: Function }).send(JSON.stringify(cmd));\n } catch (e) {\n clearTimeout(timer);\n this.pending.delete(id);\n reject(e instanceof Error ? e : new Error(String(e)));\n }\n });\n }\n\n /** Whether the extension is currently connected. */\n isConnected(): boolean {\n return this.connected;\n }\n\n /** Shutdown the WebSocket server. */\n async shutdown(): Promise<void> {\n // Reject pending waiters\n for (const waiter of this.connectionWaiters) {\n waiter.reject(new Error('Provider shutting down'));\n }\n this.connectionWaiters = [];\n\n // Reject pending requests\n for (const [id, req] of this.pending) {\n clearTimeout(req.timer);\n req.reject(new Error('Provider shutting down'));\n this.pending.delete(id);\n }\n\n if (this.clientWs) {\n try { (this.clientWs as { close: Function }).close(); } catch { /* */ }\n this.clientWs = null;\n }\n\n if (this.wss) {\n try { (this.wss as { close: Function }).close(); } catch { /* */ }\n this.wss = null;\n }\n\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server!.close(() => resolve());\n });\n this.server = null;\n }\n\n this.connected = false;\n log.info('Extension provider shut down');\n }\n\n private _handleMessage(raw: string): void {\n try {\n const msg = JSON.parse(raw);\n\n // Status events from extension (fire-and-forget, no pending match)\n if (msg.type === 'status') {\n log.debug(msg, 'Extension status');\n return;\n }\n\n // Network events\n if (msg.type === 'network_event') {\n log.debug({ listenerId: msg.listenerId, eventType: msg.eventType }, 'Network event');\n return;\n }\n\n // Command result\n const id = msg.id as string;\n const pending = this.pending.get(id);\n if (pending) {\n clearTimeout(pending.timer);\n this.pending.delete(id);\n pending.resolve(msg as ExtensionResult);\n }\n } catch (e) {\n log.error({ err: e }, 'Failed to parse extension message');\n }\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"extension.js","names":[],"sources":["../../../../src/browser/providers/extension.ts"],"sourcesContent":["/**\n * Extension browser provider — connects to the xopc Chrome Extension via WebSocket.\n *\n * The xopc process starts a WebSocket server; the Chrome Extension connects as a client.\n * Commands are sent over the WS connection and results are returned asynchronously.\n */\n\nimport { createLogger } from '../../utils/logger.js';\n\nconst log = createLogger('ExtensionProvider');\n\n// ── Protocol types (mirrored from packages/browser-ext/src/protocol.ts) ──\n\nexport type ExtensionAction =\n | 'open' | 'navigate' | 'reload' | 'back' | 'forward' | 'get_url' | 'get_title'\n | 'state' | 'snapshot' | 'screenshot' | 'content'\n | 'evaluate'\n | 'click' | 'type' | 'scroll' | 'wait'\n | 'query_selector' | 'query_selector_all' | 'wait_for_selector'\n | 'mouse_move' | 'mouse_click' | 'mouse_down' | 'mouse_up' | 'mouse_wheel'\n | 'keys' | 'keyboard_type' | 'keyboard_down' | 'keyboard_up' | 'keyboard_insert_text'\n | 'get_bounding_box' | 'get_element_text' | 'get_element_attribute'\n | 'get_elements_count' | 'set_input_files' | 'scroll_into_view'\n | 'set_viewport_size' | 'get_viewport_size'\n | 'network_start' | 'network_events' | 'network_stop'\n | 'dialog'\n | 'get_cookies' | 'set_cookies' | 'clear_cookies'\n | 'close' | 'new_tab' | 'list_tabs'\n | 'cdp'\n | 'ping';\n\nexport interface ExtensionCommand {\n id: string;\n action: ExtensionAction;\n tabId?: number;\n args?: Record<string, unknown>;\n timeout?: number;\n}\n\nexport interface ExtensionResult {\n id: string;\n ok: boolean;\n data?: unknown;\n error?: string;\n durationMs?: number;\n}\n\n// ── Configuration ────────────────────────────────────────────────────\n\nexport interface ExtensionProviderConfig {\n /** WebSocket server port. Default: 19820. */\n port?: number;\n /** Host to bind. Default: 127.0.0.1. */\n host?: string;\n /** Timeout waiting for extension connection (ms). Default: 30000. */\n connectionTimeout?: number;\n /** Default command timeout (ms). Default: 30000. */\n commandTimeout?: number;\n}\n\nconst DEFAULT_PORT = 19820;\nconst DEFAULT_HOST = '127.0.0.1';\nconst DEFAULT_CONNECTION_TIMEOUT = 30_000;\nconst DEFAULT_COMMAND_TIMEOUT = 30_000;\n\n// ── Provider ─────────────────────────────────────────────────────────\n\ninterface PendingRequest {\n resolve: (result: ExtensionResult) => void;\n reject: (error: Error) => void;\n timer: ReturnType<typeof setTimeout>;\n}\n\n/**\n * Manages a WebSocket server that the Chrome Extension connects to.\n * Sends commands and receives results over the connection.\n */\nexport class ExtensionBrowserProvider {\n readonly name = 'extension';\n\n private server: import('http').Server | null = null;\n private wss: unknown = null; // WebSocketServer instance\n private clientWs: unknown = null; // connected client WebSocket\n private pending = new Map<string, PendingRequest>();\n private commandCounter = 0;\n private connected = false;\n private readonly config: Required<ExtensionProviderConfig>;\n private connectionWaiters: Array<{ resolve: () => void; reject: (e: Error) => void }> = [];\n\n constructor(config?: ExtensionProviderConfig) {\n this.config = {\n port: config?.port ?? DEFAULT_PORT,\n host: config?.host ?? DEFAULT_HOST,\n connectionTimeout: config?.connectionTimeout ?? DEFAULT_CONNECTION_TIMEOUT,\n commandTimeout: config?.commandTimeout ?? DEFAULT_COMMAND_TIMEOUT,\n };\n }\n\n /** Start the WebSocket server and wait for the extension to connect. */\n async start(): Promise<void> {\n if (this.server) return;\n\n // Handle CJS/ESM interop: tsx wraps CJS modules so WebSocketServer may need `.default`\n const wsModule: Record<string, unknown> = await import('ws') as never;\n let WssClass = wsModule.WebSocketServer;\n if (typeof WssClass !== 'function') {\n const def = wsModule.default as Record<string, unknown> | undefined;\n WssClass = def?.WebSocketServer ?? def;\n }\n if (typeof WssClass !== 'function') {\n throw new Error('Failed to resolve WebSocketServer from \"ws\" package');\n }\n const WebSocketServer = WssClass as unknown as new (opts: Record<string, unknown>) => unknown;\n const http = await import('node:http');\n\n this.server = http.createServer((_req, res) => {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ ok: true, connected: this.connected }));\n });\n\n this.wss = new WebSocketServer({ server: this.server, path: '/browser-ext' });\n\n (this.wss as { on: Function }).on('connection', (ws: unknown) => {\n log.info('Chrome Extension connected');\n this.clientWs = ws;\n this.connected = true;\n\n // Resolve any pending connection waiters\n for (const waiter of this.connectionWaiters) {\n waiter.resolve();\n }\n this.connectionWaiters = [];\n\n (ws as { on: Function }).on('message', (data: Buffer | string) => {\n this._handleMessage(data.toString());\n });\n\n (ws as { on: Function }).on('close', () => {\n log.warn('Chrome Extension disconnected');\n this.clientWs = null;\n this.connected = false;\n // Reject all pending requests\n for (const [id, req] of this.pending) {\n clearTimeout(req.timer);\n req.reject(new Error('Extension disconnected'));\n this.pending.delete(id);\n }\n });\n });\n\n const wssEmitter = this.wss as import('node:events').EventEmitter;\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n\n const disposeFailedStart = async () => {\n if (this.wss) {\n try {\n (this.wss as { close: (cb?: () => void) => void }).close();\n } catch {\n /* */\n }\n this.wss = null;\n }\n if (this.server) {\n await new Promise<void>((r) => {\n this.server!.close(() => r());\n });\n this.server = null;\n }\n };\n\n const onStartError = (err: Error) => {\n if (settled) return;\n settled = true;\n this.server!.removeListener('error', onStartError);\n wssEmitter.removeListener('error', onStartError);\n void disposeFailedStart().then(() => reject(err));\n };\n\n const onListening = () => {\n if (settled) return;\n settled = true;\n this.server!.removeListener('error', onStartError);\n wssEmitter.removeListener('error', onStartError);\n\n log.info({ port: this.config.port, host: this.config.host }, 'Extension WS server started');\n\n const onRuntimeError = (err: Error) => {\n log.error({ err }, 'Extension WS bridge runtime error');\n };\n this.server!.on('error', onRuntimeError);\n wssEmitter.on('error', onRuntimeError);\n\n resolve();\n };\n\n // `listen` failures may surface on `http.Server` or on `ws` WebSocketServer; only\n // attaching `server.on('error')` leaves `error` on `wss` unhandled (process crash).\n this.server!.on('error', onStartError);\n wssEmitter.on('error', onStartError);\n this.server!.listen(this.config.port, this.config.host, onListening);\n });\n }\n\n /** Wait for the Chrome Extension to connect. */\n async waitForConnection(timeoutMs?: number): Promise<void> {\n if (this.connected) return;\n\n const timeout = timeoutMs ?? this.config.connectionTimeout;\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n const idx = this.connectionWaiters.findIndex(w => w.resolve === resolve);\n if (idx >= 0) this.connectionWaiters.splice(idx, 1);\n reject(new Error(`Extension connection timeout after ${timeout}ms. Is the Chrome Extension installed and enabled?`));\n }, timeout);\n\n this.connectionWaiters.push({\n resolve: () => { clearTimeout(timer); resolve(); },\n reject: (e) => { clearTimeout(timer); reject(e); },\n });\n });\n }\n\n /** Send a command to the extension and wait for the result. */\n async sendCommand(action: ExtensionAction, args?: Record<string, unknown>, options?: { tabId?: number; timeout?: number }): Promise<ExtensionResult> {\n if (!this.connected || !this.clientWs) {\n throw new Error('Extension not connected. Ensure the Chrome Extension is installed and connected.');\n }\n\n const id = `cmd_${++this.commandCounter}_${Date.now()}`;\n const cmd: ExtensionCommand = {\n id,\n action,\n args,\n tabId: options?.tabId,\n timeout: options?.timeout ?? this.config.commandTimeout,\n };\n\n return new Promise<ExtensionResult>((resolve, reject) => {\n const timer = setTimeout(() => {\n this.pending.delete(id);\n reject(new Error(`Command timeout: ${action} (${cmd.timeout}ms)`));\n }, cmd.timeout!);\n\n this.pending.set(id, { resolve, reject, timer });\n\n try {\n (this.clientWs as { send: Function }).send(JSON.stringify(cmd));\n } catch (e) {\n clearTimeout(timer);\n this.pending.delete(id);\n reject(e instanceof Error ? e : new Error(String(e)));\n }\n });\n }\n\n /** Whether the extension is currently connected. */\n isConnected(): boolean {\n return this.connected;\n }\n\n /** Shutdown the WebSocket server. */\n async shutdown(): Promise<void> {\n // Reject pending waiters\n for (const waiter of this.connectionWaiters) {\n waiter.reject(new Error('Provider shutting down'));\n }\n this.connectionWaiters = [];\n\n // Reject pending requests\n for (const [id, req] of this.pending) {\n clearTimeout(req.timer);\n req.reject(new Error('Provider shutting down'));\n this.pending.delete(id);\n }\n\n if (this.clientWs) {\n try { (this.clientWs as { close: Function }).close(); } catch { /* */ }\n this.clientWs = null;\n }\n\n if (this.wss) {\n try { (this.wss as { close: Function }).close(); } catch { /* */ }\n this.wss = null;\n }\n\n if (this.server) {\n await new Promise<void>((resolve) => {\n this.server!.close(() => resolve());\n });\n this.server = null;\n }\n\n this.connected = false;\n log.info('Extension provider shut down');\n }\n\n private _handleMessage(raw: string): void {\n try {\n const msg = JSON.parse(raw);\n\n // Status events from extension (fire-and-forget, no pending match)\n if (msg.type === 'status') {\n log.debug(msg, 'Extension status');\n return;\n }\n\n // Network events\n if (msg.type === 'network_event') {\n log.debug({ listenerId: msg.listenerId, eventType: msg.eventType }, 'Network event');\n return;\n }\n\n // Command result\n const id = msg.id as string;\n const pending = this.pending.get(id);\n if (pending) {\n clearTimeout(pending.timer);\n this.pending.delete(id);\n pending.resolve(msg as ExtensionResult);\n }\n } catch (e) {\n log.error({ err: e }, 'Failed to parse extension message');\n }\n }\n}\n"],"mappings":"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"}
@@ -9,6 +9,7 @@ export declare function computeInlineScriptHashes(html: string): string[];
9
9
  * For the gateway console, we use:
10
10
  * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)
11
11
  * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)
12
+ * - `media-src 'self' blob:` (chat voice previews / recorded clips use blob URLs)
12
13
  * - `frame-ancestors 'none'` (prevent clickjacking)
13
14
  * - `base-uri 'none'` (prevent base tag hijacking)
14
15
  * - `object-src 'none'` (prevent plugin execution)
@@ -27,6 +27,7 @@ function hasSrcAttribute(openTag) {
27
27
  * For the gateway console, we use:
28
28
  * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)
29
29
  * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)
30
+ * - `media-src 'self' blob:` (chat voice previews / recorded clips use blob URLs)
30
31
  * - `frame-ancestors 'none'` (prevent clickjacking)
31
32
  * - `base-uri 'none'` (prevent base tag hijacking)
32
33
  * - `object-src 'none'` (prevent plugin execution)
@@ -41,6 +42,7 @@ function buildGatewayConsoleCspHeader(options) {
41
42
  hashes?.length ? `script-src 'self' ${hashes.map((hash) => `'${hash}'`).join(" ")}` : "script-src 'self'",
42
43
  "style-src 'self' 'unsafe-inline'",
43
44
  "img-src 'self' data: blob: https:",
45
+ "media-src 'self' blob: data:",
44
46
  "font-src 'self'",
45
47
  `connect-src ${options?.connectSrc ?? "'self' ws: wss:"}`,
46
48
  "worker-src 'self'"
@@ -1 +1 @@
1
- {"version":3,"file":"csp.js","names":[],"sources":["../../../../src/gateway/security/csp.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\n/**\n * Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.\n * Only scripts without a `src` attribute are considered inline.\n */\nexport function computeInlineScriptHashes(html: string): string[] {\n const hashes: string[] = [];\n const scriptRegex = /<script(?:\\s[^>]*)?>([^]*?)<\\/script>/gi;\n let match: RegExpExecArray | null;\n while ((match = scriptRegex.exec(html)) !== null) {\n const openTag = match[0].slice(0, match[0].indexOf('>') + 1);\n if (hasSrcAttribute(openTag)) {\n continue;\n }\n const content = match[1];\n if (!content) {\n continue;\n }\n const hash = createHash('sha256').update(content, 'utf8').digest('base64');\n hashes.push(`sha256-${hash}`);\n }\n return hashes;\n}\n\nconst ATTRIBUTE_NAME_REGEX = /\\s([^\\s=/>]+)(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+))?/g;\n\nfunction hasSrcAttribute(openTag: string): boolean {\n return Array.from(openTag.matchAll(ATTRIBUTE_NAME_REGEX)).some(\n (attrMatch) => attrMatch[1]?.toLowerCase() === 'src',\n );\n}\n\n/**\n * Build a Content-Security-Policy header string.\n *\n * For the gateway console, we use:\n * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)\n * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)\n * - `frame-ancestors 'none'` (prevent clickjacking)\n * - `base-uri 'none'` (prevent base tag hijacking)\n * - `object-src 'none'` (prevent plugin execution)\n */\nexport function buildGatewayConsoleCspHeader(options?: {\n inlineScriptHashes?: string[];\n connectSrc?: string;\n}): string {\n const hashes = options?.inlineScriptHashes;\n const scriptSrc = hashes?.length\n ? `script-src 'self' ${hashes.map((hash) => `'${hash}'`).join(' ')}`\n : \"script-src 'self'\";\n const connectSrc = options?.connectSrc ?? \"'self' ws: wss:\";\n\n return [\n \"default-src 'self'\",\n \"base-uri 'none'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n scriptSrc,\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: blob: https:\",\n \"font-src 'self'\",\n `connect-src ${connectSrc}`,\n \"worker-src 'self'\",\n ].join('; ');\n}\n"],"mappings":";;;;;;AAMA,SAAgB,0BAA0B,MAAwB;CAChE,MAAM,SAAmB,EAAE;CAC3B,MAAM,cAAc;CACpB,IAAI;AACJ,SAAQ,QAAQ,YAAY,KAAK,KAAK,MAAM,MAAM;AAEhD,MAAI,gBADY,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,IAAI,GAAG,EAC/B,CAAC,CAC1B;EAEF,MAAM,UAAU,MAAM;AACtB,MAAI,CAAC,QACH;EAEF,MAAM,OAAO,WAAW,SAAS,CAAC,OAAO,SAAS,OAAO,CAAC,OAAO,SAAS;AAC1E,SAAO,KAAK,UAAU,OAAO;;AAE/B,QAAO;;AAGT,MAAM,uBAAuB;AAE7B,SAAS,gBAAgB,SAA0B;AACjD,QAAO,MAAM,KAAK,QAAQ,SAAS,qBAAqB,CAAC,CAAC,MACvD,cAAc,UAAU,IAAI,aAAa,KAAK,MAChD;;;;;;;;;;;;AAaH,SAAgB,6BAA6B,SAGlC;CACT,MAAM,SAAS,SAAS;AAMxB,QAAO;EACL;EACA;EACA;EACA;EATgB,QAAQ,SACtB,qBAAqB,OAAO,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,KAAK,IAAI,KAChE;EASF;EACA;EACA;EACA,eAXiB,SAAS,cAAc;EAYxC;EACD,CAAC,KAAK,KAAK"}
1
+ {"version":3,"file":"csp.js","names":[],"sources":["../../../../src/gateway/security/csp.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\n/**\n * Compute SHA-256 CSP hashes for inline `<script>` blocks in an HTML string.\n * Only scripts without a `src` attribute are considered inline.\n */\nexport function computeInlineScriptHashes(html: string): string[] {\n const hashes: string[] = [];\n const scriptRegex = /<script(?:\\s[^>]*)?>([^]*?)<\\/script>/gi;\n let match: RegExpExecArray | null;\n while ((match = scriptRegex.exec(html)) !== null) {\n const openTag = match[0].slice(0, match[0].indexOf('>') + 1);\n if (hasSrcAttribute(openTag)) {\n continue;\n }\n const content = match[1];\n if (!content) {\n continue;\n }\n const hash = createHash('sha256').update(content, 'utf8').digest('base64');\n hashes.push(`sha256-${hash}`);\n }\n return hashes;\n}\n\nconst ATTRIBUTE_NAME_REGEX = /\\s([^\\s=/>]+)(?:\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s>]+))?/g;\n\nfunction hasSrcAttribute(openTag: string): boolean {\n return Array.from(openTag.matchAll(ATTRIBUTE_NAME_REGEX)).some(\n (attrMatch) => attrMatch[1]?.toLowerCase() === 'src',\n );\n}\n\n/**\n * Build a Content-Security-Policy header string.\n *\n * For the gateway console, we use:\n * - `script-src 'self'` + optional SHA-256 hashes for inline scripts (no unsafe-inline)\n * - `style-src 'self' 'unsafe-inline'` (Tailwind + runtime style injection)\n * - `media-src 'self' blob:` (chat voice previews / recorded clips use blob URLs)\n * - `frame-ancestors 'none'` (prevent clickjacking)\n * - `base-uri 'none'` (prevent base tag hijacking)\n * - `object-src 'none'` (prevent plugin execution)\n */\nexport function buildGatewayConsoleCspHeader(options?: {\n inlineScriptHashes?: string[];\n connectSrc?: string;\n}): string {\n const hashes = options?.inlineScriptHashes;\n const scriptSrc = hashes?.length\n ? `script-src 'self' ${hashes.map((hash) => `'${hash}'`).join(' ')}`\n : \"script-src 'self'\";\n const connectSrc = options?.connectSrc ?? \"'self' ws: wss:\";\n\n return [\n \"default-src 'self'\",\n \"base-uri 'none'\",\n \"object-src 'none'\",\n \"frame-ancestors 'none'\",\n scriptSrc,\n \"style-src 'self' 'unsafe-inline'\",\n \"img-src 'self' data: blob: https:\",\n \"media-src 'self' blob: data:\",\n \"font-src 'self'\",\n `connect-src ${connectSrc}`,\n \"worker-src 'self'\",\n ].join('; ');\n}\n"],"mappings":";;;;;;AAMA,SAAgB,0BAA0B,MAAwB;CAChE,MAAM,SAAmB,EAAE;CAC3B,MAAM,cAAc;CACpB,IAAI;AACJ,SAAQ,QAAQ,YAAY,KAAK,KAAK,MAAM,MAAM;AAEhD,MAAI,gBADY,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,QAAQ,IAAI,GAAG,EAC/B,CAAC,CAC1B;EAEF,MAAM,UAAU,MAAM;AACtB,MAAI,CAAC,QACH;EAEF,MAAM,OAAO,WAAW,SAAS,CAAC,OAAO,SAAS,OAAO,CAAC,OAAO,SAAS;AAC1E,SAAO,KAAK,UAAU,OAAO;;AAE/B,QAAO;;AAGT,MAAM,uBAAuB;AAE7B,SAAS,gBAAgB,SAA0B;AACjD,QAAO,MAAM,KAAK,QAAQ,SAAS,qBAAqB,CAAC,CAAC,MACvD,cAAc,UAAU,IAAI,aAAa,KAAK,MAChD;;;;;;;;;;;;;AAcH,SAAgB,6BAA6B,SAGlC;CACT,MAAM,SAAS,SAAS;AAMxB,QAAO;EACL;EACA;EACA;EACA;EATgB,QAAQ,SACtB,qBAAqB,OAAO,KAAK,SAAS,IAAI,KAAK,GAAG,CAAC,KAAK,IAAI,KAChE;EASF;EACA;EACA;EACA;EACA,eAZiB,SAAS,cAAc;EAaxC;EACD,CAAC,KAAK,KAAK"}
@@ -401,11 +401,11 @@ var GatewayService = class {
401
401
  async startBrowserExtensionServerIfNeeded() {
402
402
  const browser = (this.config.agents?.defaults)?.browser;
403
403
  if (browser?.backend !== "extension") return;
404
+ const ext = browser.extension;
405
+ const port = typeof ext?.port === "number" ? ext.port : 19820;
406
+ const host = typeof ext?.host === "string" && ext.host ? ext.host : "127.0.0.1";
404
407
  try {
405
408
  const { acquireExtensionBrowserServer } = await import("../browser/providers/extension-ws-acquire.js");
406
- const ext = browser.extension;
407
- const port = typeof ext?.port === "number" ? ext.port : 19820;
408
- const host = typeof ext?.host === "string" && ext.host ? ext.host : "127.0.0.1";
409
409
  const { provider, release } = await acquireExtensionBrowserServer({
410
410
  port,
411
411
  host
@@ -417,7 +417,16 @@ var GatewayService = class {
417
417
  host
418
418
  }, "Browser extension WS server started");
419
419
  } catch (err) {
420
- log.error({ err }, "Failed to start browser extension WS server");
420
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
421
+ log.error({
422
+ err,
423
+ phase: "browser_extension_ws",
424
+ ...code === "EADDRINUSE" ? {
425
+ bindPort: port,
426
+ bindHost: host,
427
+ hint: "Another process holds this port (default 19820). Stop it or set agents.defaults.browser.extension.port."
428
+ } : {}
429
+ }, `Failed to start browser extension WS server: ${err instanceof Error ? err.message : String(err)}`);
421
430
  }
422
431
  }
423
432
  /**