@bodhiapp/bodhi-js-ext 0.0.37 → 0.0.38

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.
@@ -1 +1 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("@bodhiapp/bodhi-js-core"),l=require("@bodhiapp/bodhi-browser-types"),c={EXT2EXT_CLIENT_REQUEST:"EXT2EXT_CLIENT_REQUEST",EXT2EXT_CLIENT_RESPONSE:"EXT2EXT_CLIENT_RESPONSE",EXT2EXT_CLIENT_BROADCAST:"EXT2EXT_CLIENT_BROADCAST",EXT2EXT_CLIENT_API_REQUEST:"EXT2EXT_CLIENT_API_REQUEST",EXT2EXT_CLIENT_API_RESPONSE:"EXT2EXT_CLIENT_API_RESPONSE",EXT2EXT_CLIENT_STREAM_REQUEST:"EXT2EXT_CLIENT_STREAM_REQUEST",EXT2EXT_CLIENT_STREAM_CHUNK:"EXT2EXT_CLIENT_STREAM_CHUNK",EXT2EXT_CLIENT_STREAM_ERROR:"EXT2EXT_CLIENT_STREAM_ERROR",EXT2EXT_CLIENT_STREAM_API_ERROR:"EXT2EXT_CLIENT_STREAM_API_ERROR",EXT2EXT_CLIENT_STREAM_DONE:"EXT2EXT_CLIENT_STREAM_DONE",EXT2EXT_CLIENT_STREAM_TEXT_REQUEST:"EXT2EXT_CLIENT_STREAM_TEXT_REQUEST",EXT2EXT_CLIENT_STREAM_TEXT_START:"EXT2EXT_CLIENT_STREAM_TEXT_START",EXT2EXT_CLIENT_STREAM_TEXT_CHUNK:"EXT2EXT_CLIENT_STREAM_TEXT_CHUNK",EXT2EXT_CLIENT_STREAM_TEXT_DONE:"EXT2EXT_CLIENT_STREAM_TEXT_DONE",EXT2EXT_CLIENT_STREAM_TEXT_ERROR:"EXT2EXT_CLIENT_STREAM_TEXT_ERROR"},y="ext2ext-client-stream",f="ext2ext-client-stream-text",_={LOGIN:"login",LOGOUT:"logout",GET_AUTH_STATE:"getAuthState",DISCOVER_EXTENSION:"discoverBodhiExtension",GET_EXTENSION_ID:"get_extension_id",SET_EXTENSION_ID:"setExtensionId"},C=5e3,A=3,w=500,X=500,N=3e4;function P(m){return"error"in m}class O{async get(e){const r=(await chrome.storage.session.get(e))[e];return r!==void 0?String(r):null}async set(e){await chrome.storage.session.set(e)}async remove(e){await chrome.storage.session.remove(e)}}class b extends a.DirectClientBase{constructor(e,t){const r=a.createStoragePrefixWithBasePath(e.basePath,a.STORAGE_PREFIXES.EXT_DIRECT),i={authClientId:e.authClientId,authServerUrl:e.authServerUrl,storagePrefix:r,logLevel:e.logLevel,loggerPrefix:"DirectExtClient",apiTimeoutMs:e.apiTimeoutMs,storage:e.storage??new O,initialTokens:e.initialTokens};super(i,t)}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;e?.flowType==="redirect"&&this.logger.warn("Extension mode does not support redirect flow type; using popup instead");const r=e?.userRole??"scope_user_user";e?.onProgress?.("requesting");const i=new a.AccessRequestBuilder(this.authClientId).requestedRole(r).flowType("popup");e?.requested&&i.requested(e.requested);const o=i.build(),s=await this.requestAccess(o),{id:n,review_url:h}=a.unwrapResponse(s);e?.onProgress?.("reviewing"),await chrome.tabs.create({url:h});const E=await this.pollAccessRequestStatus(n,{intervalMs:e?.pollIntervalMs??a.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??a.DEFAULT_POLL_TIMEOUT_MS});if(E.status!=="approved")throw a.createOperationError("auth_error",`Access request ${E.status}`);const u=E.access_request_scope;return e?.onProgress?.("authenticating"),this.performOAuthPkce(`openid profile email roles ${u??""}`.trim())}async performOAuthPkce(e){const t=a.generateCodeVerifier(),r=await a.generateCodeChallenge(t),i=a.generateCodeVerifier();await this._storageSet({[this.storageKeys.CODE_VERIFIER]:t,[this.storageKeys.STATE]:i});const o=chrome.identity.getRedirectURL("callback"),s=new URL(this.authEndpoints.authorize);return s.searchParams.set("client_id",this.authClientId),s.searchParams.set("response_type","code"),s.searchParams.set("redirect_uri",o),s.searchParams.set("scope",e),s.searchParams.set("code_challenge",r),s.searchParams.set("code_challenge_method","S256"),s.searchParams.set("state",i),new Promise((n,h)=>{chrome.identity.launchWebAuthFlow({url:s.toString(),interactive:!0},async E=>{if(chrome.runtime.lastError){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(chrome.runtime.lastError);return}if(!E){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(a.createOperationError("oauth_error","No redirect URL received"));return}try{const u=new URL(E),T=u.searchParams.get("code"),d=u.searchParams.get("state"),g=await this._storageGet(this.storageKeys.STATE);if(d!==g){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(a.createOperationError("oauth_error","State mismatch"));return}if(!T){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(a.createOperationError("oauth_error","No authorization code"));return}await this.exchangeCodeForTokens(T);const p=await this.getAuthState();if(p.status!=="authenticated")throw a.createOperationError("oauth_error","Login failed");this.setAuthState(p),await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),n(p)}catch(u){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(u)}})})}_getRedirectUri(){return chrome.identity.getRedirectURL("callback")}}class k{constructor(e={},t){this.state={type:"extension",extension:"not-initialized",extensionId:null,server:a.PENDING_EXTENSION_READY},this.extensionId=null,this.broadcastListenerActive=!1,this.config=e,this.logger=new a.Logger("ExtClient",e?.logLevel||"warn"),this.onStateChange=t??a.NOOP_STATE_CALLBACK,this.apiTimeoutMs=e.apiTimeoutMs??N,this.authClientId=e.authClientId??""}setState(e){this.state=e,this.onStateChange({type:"client-state",state:e})}setAuthState(e){this.onStateChange({type:"auth-state",state:e})}setStateCallback(e){this.onStateChange=e}setupBroadcastListener(){this.broadcastListenerActive||(this.broadcastListenerActive=!0,chrome.runtime.onMessage.addListener(e=>{const t=e;return t?.type===c.EXT2EXT_CLIENT_BROADCAST&&t.event==="authStateChanged"&&this.handleAuthStateChangedBroadcast(),!1}),this.logger.debug("Broadcast listener setup complete"))}async handleAuthStateChangedBroadcast(){this.logger.debug("Received authStateChanged broadcast, refreshing auth state");const e=await this.getAuthState();this.setAuthState(e)}generateRequestId(){return crypto.randomUUID()}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){if(!e.testConnection&&!e.selectedConnection){this.logger.info("No testConnection or selectedConnection, returning not-initialized state");const i=a.createExtensionStateNotInitialized();return this.setState(i),i}if(this.extensionId&&!e.testConnection)return this.logger.debug("Already initialized with extensionId, skipping discovery"),this.state;const t=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??C,r=e.savedState?.extensionId;try{if(!this.extensionId){if(r)this.logger.info("Restoring with known extensionId:",r),await this.sendExtMessageWithTimeout(_.SET_EXTENSION_ID,{extensionId:r},t),this.extensionId=r;else{this.logger.info("Discovering bodhi-browser extension...");const s={attempts:this.config.initParams?.extension?.attempts,attemptWaitMs:this.config.initParams?.extension?.attemptWaitMs,attemptTimeout:this.config.initParams?.extension?.attemptTimeout},n=await this.sendExtMessageWithTimeout(_.DISCOVER_EXTENSION,s,t);this.extensionId=n.extensionId,this.logger.info("Extension discovered:",this.extensionId)}this.setupBroadcastListener()}const i={type:"extension",extension:"ready",extensionId:this.extensionId,server:a.PENDING_EXTENSION_READY};let o=a.PENDING_EXTENSION_READY;if(e.testConnection)try{o=await this.getServerState(),this.logger.info("Server connectivity tested, state:",o.status)}catch(s){this.logger.error("Failed to get server state:",s),o=a.BACKEND_SERVER_NOT_REACHABLE}return this.setState({...i,server:o}),this.state}catch(i){this.logger.error("Failed to initialize extension:",i),this.extensionId=null;const o=a.createExtensionStateNotFound();return this.setState(o),this.state}}async sendExtMessageWithTimeout(e,t,r=1e4){const i=new Promise((o,s)=>setTimeout(()=>s(new Error("Timeout")),r));return Promise.race([this.sendExtRequest(e,t),i])}async sendExtRequest(e,t){try{const r=this.generateRequestId(),i=await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_REQUEST,requestId:r,request:{action:e,params:t}});if(!i)throw a.createOperationError("extension_error","No response from background script");if(i.type!==c.EXT2EXT_CLIENT_RESPONSE)throw a.createOperationError("extension_error","Invalid response type from background script");const o=i.response;if(l.isExtError(o)){const s=o.error.type||"extension_error";throw a.createOperationError(s,o.error.message)}return o}catch(r){throw r instanceof a.BodhiError?r:a.createOperationError("extension_error",r instanceof Error?r.message:"Unknown error occurred")}}async sendRawApiMessage(e,t,r,i,o){const s=this.generateRequestId();return await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_API_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i,authenticated:o}})}async sendApiRequest(e,t,r,i,o){try{const s=new Promise((h,E)=>setTimeout(()=>E(new Error(`[bodhi-js-sdk/ext] network timeout: api request not completed within configured/default timeout of ${this.apiTimeoutMs}ms`)),this.apiTimeoutMs)),n=await Promise.race([this.sendRawApiMessage(e,t,r,i,o),s]);if(P(n)){const h=n.error.type||"extension_error";throw new a.BodhiError(h,n.error.message)}return n.response}catch(s){if(s instanceof a.BodhiError)throw s;const n=s instanceof Error?s.message:String(s);throw new a.BodhiError("network_error",n)}}async login(e){return new Promise((t,r)=>{const i=async o=>{if(o&&typeof o=="object"&&"type"in o&&o.type==="EXT2EXT_CLIENT_BROADCAST"&&"event"in o&&o.event==="authStateChanged"){chrome.runtime.onMessage.removeListener(i);try{const s=await this.getAuthState();if(a.isAuthError(s)){r(a.createOperationError("auth_error",`Login failed: ${s.error?.message}`));return}if(s.status!=="authenticated"){r(a.createOperationError("auth_error","Login failed: User is not logged in"));return}this.setAuthState(s),t(s)}catch(s){r(s)}}};chrome.runtime.onMessage.addListener(i),this.sendExtRequest(_.LOGIN,e).catch(o=>{chrome.runtime.onMessage.removeListener(i),r(o)})})}async logout(){await this.sendExtRequest(_.LOGOUT);const e={status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1};return this.setAuthState(e),e}async getAuthState(){return this.isClientInitialized()?(await this.sendExtRequest(_.GET_AUTH_STATE)).authState:a.INITIAL_AUTH_STATE}async pingApi(){return this.sendApiRequest("GET","/ping")}async getServerState(){let e;try{e=await this.sendApiRequest("GET","/bodhi/v1/info")}catch(o){const s=o instanceof Error?o.message:"Connection failed",n=o instanceof a.BodhiError?o.code:"extension_error";return{status:"not-reachable",version:null,error:{message:s,type:n}}}if(e.status>=400)return{status:"not-reachable",version:null,error:{message:"API error from server",type:"extension_error"}};const t=e.body,r=t.version||"unknown",i={deployment:t.deployment??null,client_id:t.client_id??null};switch(t.status){case"ready":return{status:"ready",version:r,error:null,...i};case"setup":return{status:"setup",version:r,error:t.error?{message:t.error.message,type:t.error.type}:{message:"Setup required",type:"extension_error"},...i};case"resource_admin":return{status:"resource_admin",version:r,error:t.error?{message:t.error.message,type:t.error.type}:{message:"Resource admin required",type:"extension_error"},...i};case"error":return{status:"error",version:r,error:t.error?{message:t.error.message,type:t.error.type}:{message:"Server error",type:"extension_error"},...i};default:return{status:"not-reachable",version:null,error:{message:"Unknown server status",type:"extension_error"}}}}async*stream(e,t,r,i,o=!0){const s=this.generateRequestId();this.logger.debug("Starting stream",{method:e,endpoint:t,requestId:s});const n=chrome.runtime.connect({name:y}),E=new ReadableStream({start:u=>{n.onMessage.addListener(T=>{if(T.requestId===s)switch(T.type){case c.EXT2EXT_CLIENT_STREAM_DONE:this.logger.debug("Stream complete",{requestId:s}),u.close(),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_ERROR:this.logger.error("Stream error",{requestId:s,error:JSON.stringify(T.error)}),u.error(new a.BodhiError("extension_error",T.error.message)),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_API_ERROR:{const d=T;this.logger.error("Stream API error",{requestId:s,error:d.response.body?.error}),u.error(new a.BodhiApiError(d.response.status,d.response.body,d.response.body?.error?.message||"API error")),n.disconnect();break}case c.EXT2EXT_CLIENT_STREAM_CHUNK:{const d=T;l.isApiSuccessResponse(d.response)&&u.enqueue(d.response.body);break}}}),n.onDisconnect.addListener(()=>{this.logger.debug("Port disconnected",{requestId:s});try{u.error(new a.BodhiError("connection_closed","Connection closed unexpectedly"))}catch{}}),n.postMessage({type:c.EXT2EXT_CLIENT_STREAM_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i,authenticated:o}})}}).getReader();try{for(;;){const{done:u,value:T}=await E.read();if(u){this.logger.debug("Stream iteration complete");break}yield T}}finally{E.releaseLock()}}async streamText(e,t,r,i,o=!0){const s=this.generateRequestId();return new Promise((n,h)=>{let E,u=!1;const T=new ReadableStream({start:g=>{E=g}}),d=chrome.runtime.connect({name:f});d.onMessage.addListener(g=>{if(g.requestId===s)switch(g.type){case c.EXT2EXT_CLIENT_STREAM_TEXT_START:{u=!0;async function*p(L){const x=L.getReader();try{for(;;){const{done:M,value:v}=await x.read();if(M)break;yield v}}finally{x.releaseLock()}}n({status:g.status,headers:g.headers,body:p(T)});break}case c.EXT2EXT_CLIENT_STREAM_TEXT_CHUNK:E.enqueue(g.chunk);break;case c.EXT2EXT_CLIENT_STREAM_TEXT_DONE:E.close(),d.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR:{const p=a.createOperationError("extension_error",g.error.message);u?E.error(p):h(p),d.disconnect();break}}}),d.onDisconnect.addListener(()=>{if(!u)h(a.createOperationError("extension_error","Connection closed unexpectedly"));else try{E.error(a.createOperationError("extension_error","Connection closed unexpectedly"))}catch{}}),d.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i,authenticated:o}})})}get chat(){return this._chat??=new a.Chat(this)}get models(){return this._models??=new a.Models(this)}get embeddings(){return this._embeddings??=new a.Embeddings(this)}get mcps(){return this._mcps??=new a.Mcps(this)}async requestAccess(e){return this.sendApiRequest("POST","/bodhi/v1/apps/request-access",e,{},!1)}async getAccessRequestStatus(e){return this.sendApiRequest("GET",`/bodhi/v1/apps/access-requests/${e}?app_client_id=${encodeURIComponent(this.authClientId)}`,void 0,{},!1)}async pollAccessRequestStatus(e,t){return a.pollAccessRequestUntilResolved(r=>this.getAccessRequestStatus(r),e,t)}serialize(){return{extensionId:this.extensionId??void 0}}async debug(){return{type:"ExtClient",state:this.state,authState:await this.getAuthState()}}}class U extends a.BaseFacadeClient{constructor(e,t,r){const i=t||{},o={basePath:i.basePath||"/",authServerUrl:i.authServerUrl||"https://id.getbodhi.app/realms/bodhi",logLevel:i.logLevel||"warn",apiTimeoutMs:i.apiTimeoutMs,storage:i.storage,initialTokens:i.initialTokens,initParams:i.initParams};super(e,o,r)}createLogger(e){return new a.Logger("ExtUIClient",e.logLevel)}createStoragePrefix(e){return a.createStoragePrefixWithBasePath(e.basePath,a.STORAGE_PREFIXES.EXT)}createExtClient(e,t){return new k({authClientId:this.authClientId,logLevel:e.logLevel,apiTimeoutMs:e.apiTimeoutMs,initParams:e.initParams},t)}createDirectClient(e,t,r){return new b({authClientId:e,authServerUrl:t.authServerUrl,logLevel:t.logLevel,basePath:t.basePath,apiTimeoutMs:t.apiTimeoutMs,storage:t.storage,initialTokens:t.initialTokens},r)}}const q=["ggedphdcbekjlomjaidbajglgihbeaon"],D=["bjdjhiombmfbcoeojijpfckljjghmjbf"],R="production",S=class S{constructor(e,t){this.isAuthenticating=!1,this.state="setup",this.listenersInitialized=!1,this.refreshPromise=null,this.activeStreamPorts=new Map,this.authClientId=e,this.authServerUrl=t?.authServerUrl||"https://id.getbodhi.app/realms/bodhi",this.extensionId=t?.extensionId,this.logger=new a.Logger("BodhiExtClient",t?.logLevel||"warn"),this.attempts=t?.attempts??A,this.attemptWaitMs=t?.attemptWaitMs??w,this.attemptTimeout=t?.attemptTimeout??X,this.authEndpoints={authorize:`${this.authServerUrl}/protocol/openid-connect/auth`,token:`${this.authServerUrl}/protocol/openid-connect/token`,userinfo:`${this.authServerUrl}/protocol/openid-connect/userinfo`,logout:`${this.authServerUrl}/protocol/openid-connect/logout`,revoke:`${this.authServerUrl}/protocol/openid-connect/revoke`},this.extensionId?this.logger.info(`[BodhiExtClient] Created client for extension: ${this.extensionId}`):this.logger.info("[BodhiExtClient] Created client without extension ID (call init() to discover)")}static base64UrlEncode(e){return btoa(String.fromCharCode(...new Uint8Array(e))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}static generateCodeVerifier(){const e=new Uint8Array(32);return crypto.getRandomValues(e),S.base64UrlEncode(e.buffer)}static async generateCodeChallenge(e){const r=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",r);return S.base64UrlEncode(i)}getState(){return this.state}getExtensionIdsForEnvironment(){const t=R!=="production"?q:D;return this.logger.info("[Ext2Ext/Registry] Environment: production"),this.logger.debug("[Ext2Ext/Registry] Using extension IDs:",t),t}async pingExtension(e){return this.logger.debug(`[Ext2Ext/Discovery] Pinging extension: ${e} with timeout ${this.attemptTimeout}ms`),new Promise((t,r)=>{const i=setTimeout(()=>{this.logger.debug(`[Ext2Ext/Discovery] Timeout waiting for extension ${e}`),r(new Error("Timeout"))},this.attemptTimeout);try{const o={type:l.MESSAGE_TYPES.EXT_REQUEST,requestId:crypto.randomUUID(),request:{action:"get_extension_id"}};this.logger.debug(`[Ext2Ext/Discovery] Sending message to ${e}:`,o),chrome.runtime.sendMessage(e,o,s=>{if(clearTimeout(i),chrome.runtime.lastError){this.logger.error(`[Ext2Ext/Discovery] Error from extension ${e}:`,chrome.runtime.lastError.message),r(new Error(chrome.runtime.lastError.message));return}this.logger.debug(`[Ext2Ext/Discovery] Response from ${e}:`,s);const n=s;n&&n.type===l.MESSAGE_TYPES.EXT_RESPONSE?(this.logger.debug(`[Ext2Ext/Discovery] ✓ Extension ${e} responded`),t(!0)):(this.logger.error(`[Ext2Ext/Discovery] Invalid response from ${e}:`,s),r(new Error("Invalid response")))})}catch(o){this.logger.error(`[Ext2Ext/Discovery] Exception pinging ${e}:`,o),clearTimeout(i),r(o)}})}sleep(e){return new Promise(t=>setTimeout(t,e))}async discoverBodhiExtension(e){const{attempts:t,attemptWaitMs:r,attemptTimeout:i}=e;this.logger.info(`[Ext2Ext/Discovery] Starting discovery: ${t} attempts per ID, ${i}ms timeout, ${r}ms between attempts`);const o=this.getExtensionIdsForEnvironment();this.logger.debug(`[Ext2Ext/Discovery] Will try ${o.length} extension(s):`,o);for(const h of o){for(let E=1;E<=t;E++){this.logger.debug(`[Ext2Ext/Discovery] Trying ${h} - attempt ${E}/${t}`);try{return await this.pingExtension(h),this.logger.info(`[Ext2Ext/Discovery] ✓ Found: ${h} on attempt ${E}`),{success:!0,extensionId:h}}catch(u){this.logger.debug(`[Ext2Ext/Discovery] Attempt ${E} failed for ${h}: ${u instanceof Error?u.message:"Unknown error"}`),E<t&&await this.sleep(r)}}this.logger.warn(`[Ext2Ext/Discovery] ✗ Not found: ${h} after ${t} attempts`)}const s=o.join(", "),n=`Extension not found. Tried ${o.length} IDs with ${t} attempts each: ${s}`;return this.logger.error(`[Ext2Ext/Discovery] ${n}`),{success:!1,error:n}}setupListeners(){if(this.listenersInitialized){this.logger.debug("[BodhiExtClient] Listeners already initialized, skipping");return}this.listenersInitialized=!0,chrome.runtime.onMessage.addListener((e,t,r)=>e.type!==c.EXT2EXT_CLIENT_REQUEST&&e.type!==c.EXT2EXT_CLIENT_API_REQUEST?!1:this.state!=="ready"&&!(e.type===c.EXT2EXT_CLIENT_REQUEST&&e.request.action===_.DISCOVER_EXTENSION)?(e.type===c.EXT2EXT_CLIENT_REQUEST?r({type:c.EXT2EXT_CLIENT_RESPONSE,requestId:e.requestId,response:{error:{message:this.createErrorClientNotInitialized(e),type:"NOT_INITIALIZED"}}}):r({type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:e.requestId,error:{message:`Client not initialized. Extension discovery not complete, cannot handle type:${e.type}, message:${JSON.stringify(e)}`,type:"NOT_INITIALIZED"}}),!0):(this.logger.debug(`[BodhiExtClient] Processing message.type=${e.type}`),(async()=>{const i=await this.handleAction(e);r(i)})(),!0)),this.registerStreamPortListener(y,c.EXT2EXT_CLIENT_STREAM_REQUEST,c.EXT2EXT_CLIENT_STREAM_ERROR,(e,t)=>this.handleStreamRequest(e,t)),this.registerStreamPortListener(f,c.EXT2EXT_CLIENT_STREAM_TEXT_REQUEST,c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR,(e,t)=>this.handleStreamTextRequest(e,t)),this.logger.info("[BodhiExtClient] Streaming listeners initialized")}async init(e){if(this.setupListeners(),this.extensionId){this.state="ready",this.logger.warn(`[BodhiExtClient] Already initialized with extension ID: ${this.extensionId}`);return}this.logger.info("[BodhiExtClient] Starting discovery");const t={attempts:e?.attempts??this.attempts,attemptWaitMs:e?.attemptWaitMs??this.attemptWaitMs,attemptTimeout:e?.attemptTimeout??this.attemptTimeout},r=await this.discoverBodhiExtension(t);if(!r.success||!r.extensionId)throw new Error(r.error||"Discovery failed");this.extensionId=r.extensionId,this.state="ready",this.logger.info(`[BodhiExtClient] ✓ Initialized: ${this.extensionId}`)}broadcastAuthStateChange(){chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_BROADCAST,event:"authStateChanged"}).catch(e=>{this.logger.debug("[BodhiExtClient] No listeners for broadcast:",e.message)})}async getExtensionIdFromExt(){this.logger.debug("[BodhiExtClient] Getting extension ID from bodhi-browser-ext");const e=await this.sendExtRequest("get_extension_id");return this.logger.debug("[BodhiExtClient] Extension ID response:",e),e.extension_id}async handleApiRequest(e){const{requestId:t}=e;this.logger.debug("[BodhiExtClient] Handling API request:",e.request);try{let r=e.request.headers||{};if(e.request.authenticated){const o=await this._getAccessTokenRaw();if(!o)return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"auth_error"}};r={...r,Authorization:`Bearer ${o}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const i=await this.sendApiRequest(e.request.method,e.request.endpoint,e.request.body,r);return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,response:i}}catch(r){return this.logger.error("[BodhiExtClient] API request failed:",r),{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:r instanceof Error?r.message:"Unknown error",type:"network_error"}}}}async handleExtClientRequest(e){const{requestId:t,request:r}=e,{action:i,params:o}=r;this.logger.debug(`[BodhiExtClient] Handling action: ${i}`);try{let s={};switch(i){case _.DISCOVER_EXTENSION:{const n=o;await this.init(n),this.logger.info("[BodhiExtClient] Discovery successful:",{extensionId:this.extensionId,environment:R}),s={extensionId:this.extensionId,environment:R};break}case _.SET_EXTENSION_ID:{const{extensionId:n}=o;this.extensionId=n,this.state="ready",this.logger.info("[BodhiExtClient] Extension ID set:",{extensionId:n}),s={success:!0};break}case _.GET_EXTENSION_ID:{const n=await this.sendExtRequestRaw(l.EXT_ACTIONS.GET_EXTENSION_ID,o);return l.isExtError(n.response)?{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:n.response.error.message||`Extension request failed to get extension ID: ${JSON.stringify(n.response)}`,type:n.response.error.type}}}:{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:n.response}}case _.LOGIN:{const n=o;await this.login(n),this.broadcastAuthStateChange();break}case _.LOGOUT:await this.logout(),this.broadcastAuthStateChange();break;case _.GET_AUTH_STATE:s={authState:await this.getAuthState()};break;default:return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown action: ${i}`,type:"UNKNOWN_ACTION"}}}}return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:s}}catch(s){return this.logger.error("[BodhiExtClient] Unexpected error:",s),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:s instanceof Error?s.message:`Unexpected error: ${JSON.stringify(s)}`}}}}}async handleAction(e){switch(e.type){case c.EXT2EXT_CLIENT_API_REQUEST:return this.handleApiRequest(e);case c.EXT2EXT_CLIENT_REQUEST:return this.handleExtClientRequest(e);default:{const{requestId:t}=e;return this.logger.error("[BodhiExtClient] Unknown message type:",e.type),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown message type: ${e.type}`,type:"UNKNOWN_MESSAGE_TYPE"}}}}}}async login(e){if(!(this.isAuthenticating||(await this.getAuthState()).status==="authenticated")){this.isAuthenticating=!0;try{if(!this.extensionId)throw new Error("Extension not discovered. Please detect Bodhi extension before login.");e?.flowType==="redirect"&&this.logger.warn("Extension mode does not support redirect flow type; using popup instead");const r=e?.userRole??"scope_user_user",i=new a.AccessRequestBuilder(this.authClientId).requestedRole(r).flowType("popup");e?.requested&&i.requested(e.requested);const o=i.build(),s=await this.requestAccess(o),{id:n,review_url:h}=a.unwrapResponse(s);await chrome.tabs.create({url:h});const E=await this.pollAccessRequestStatus(n,{intervalMs:e?.pollIntervalMs??a.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??a.DEFAULT_POLL_TIMEOUT_MS});if(E.status!=="approved")throw a.createOperationError("auth_error",`Access request ${E.status}`);const T=`openid profile email roles ${E.access_request_scope??""}`.trim();await this.performOAuthPkce(T)}finally{this.isAuthenticating=!1}}}async exchangeCodeForTokens(e){if((await this.getAuthState()).status==="authenticated")return;const{codeVerifier:r}=await chrome.storage.session.get("codeVerifier"),i=chrome.identity.getRedirectURL("callback"),o=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:i,client_id:this.authClientId,code_verifier:r})});if(!o.ok){const n=await o.text();throw new Error(`Token exchange failed: ${o.status} ${n}`)}const s=await o.json();await this.storeTokens({accessToken:s.access_token,refreshToken:s.refresh_token,idToken:s.id_token,expiresIn:s.expires_in})}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1};try{const t=this.parseJwt(e);return{status:"authenticated",user:{sub:t.sub,email:t.email,name:t.name,given_name:t.given_name,family_name:t.family_name,preferred_username:t.preferred_username},accessToken:e,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}catch(t){return this.logger.error("Failed to parse token:",t),{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}}async logout(){const{refreshToken:e}=await chrome.storage.session.get("refreshToken");if(e)try{await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"})})}catch(t){this.logger.warn("[OAuth] Token revocation failed:",t)}await this.clearTokens()}async requestAccess(e){return this.sendApiRequest("POST","/bodhi/v1/apps/request-access",e)}async getAccessRequestStatus(e){return this.sendApiRequest("GET",`/bodhi/v1/apps/access-requests/${e}?app_client_id=${encodeURIComponent(this.authClientId)}`)}async pollAccessRequestStatus(e,t){return a.pollAccessRequestUntilResolved(r=>this.getAccessRequestStatus(r),e,t)}async performOAuthPkce(e){const t=S.generateCodeVerifier(),r=await S.generateCodeChallenge(t),i=S.generateCodeVerifier();await chrome.storage.session.set({codeVerifier:t,state:i,authInProgress:!0});const o=chrome.identity.getRedirectURL("callback"),s=new URL(this.authEndpoints.authorize);return s.searchParams.set("client_id",this.authClientId),s.searchParams.set("response_type","code"),s.searchParams.set("redirect_uri",o),s.searchParams.set("scope",e),s.searchParams.set("code_challenge",r),s.searchParams.set("code_challenge_method","S256"),s.searchParams.set("state",i),new Promise((n,h)=>{chrome.identity.launchWebAuthFlow({url:s.toString(),interactive:!0},async E=>{if(await chrome.storage.session.set({authInProgress:!1}),chrome.runtime.lastError){await chrome.storage.session.remove(["codeVerifier","state"]),h(chrome.runtime.lastError);return}if(!E){await chrome.storage.session.remove(["codeVerifier","state"]),h(a.createOperationError("oauth_error","No redirect URL received"));return}try{const u=new URL(E),T=u.searchParams.get("code"),d=u.searchParams.get("state"),{state:g}=await chrome.storage.session.get("state");if(d!==g){await chrome.storage.session.remove(["codeVerifier","state"]),h(a.createOperationError("oauth_error","State mismatch"));return}if(!T){await chrome.storage.session.remove(["codeVerifier","state"]),h(a.createOperationError("oauth_error","No authorization code"));return}await this.exchangeCodeForTokens(T),await chrome.storage.session.remove(["codeVerifier","state"]);const p=await this.getAuthState();if(p.status!=="authenticated")throw a.createOperationError("oauth_error","Login failed");n(p)}catch(u){await chrome.storage.session.remove(["codeVerifier","state"]),h(u)}})})}async sendExtRequest(e,t){const r=await this.sendExtRequestRaw(e,t);if(l.isExtError(r.response))throw this.logger.error("[BodhiExtClient] Extension error:",r.response.error),new Error(r.response.error.message||`Extension request failed: ${JSON.stringify(r.response)}`);return r.response}async sendApiRequest(e,t,r,i){if(!this.extensionId)throw new a.BodhiError("not_initialized",this.createErrorClientNotInitialized({type:"api",method:e,endpoint:t}));this.logger.debug(`[BodhiExtClient] Sending API_REQUEST: method=${e}, endpoint=${t}`,r?{body:r}:"");const o=crypto.randomUUID(),s={type:l.MESSAGE_TYPES.API_REQUEST,requestId:o,request:{method:e,endpoint:t,body:r,headers:i}};return this.logger.debug(`[BodhiExtClient] Request ID: ${o}, Extension: ${this.extensionId}`),new Promise((n,h)=>{try{chrome.runtime.sendMessage(this.extensionId,s,E=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${o}:`,chrome.runtime.lastError),h(new a.BodhiError("extension_error",chrome.runtime.lastError.message??"Chrome runtime error"));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${o}:`,E),!E){this.logger.error(`[BodhiExtClient] No response received for request ${o}`),h(new a.BodhiError("extension_error","No response from extension"));return}E.type===l.MESSAGE_TYPES.API_RESPONSE&&E.requestId===o?"error"in E?(this.logger.error(`[BodhiExtClient] API error for ${o}:`,E.error),h(new a.BodhiError(E.error.type||"extension_error",E.error.message))):(this.logger.debug(`[BodhiExtClient] ✓ Valid API_RESPONSE for ${o}`),n(E.response)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${o}:`,E),h(new a.BodhiError("extension_error","Invalid response format")))})}catch(E){this.logger.error(`[BodhiExtClient] Exception sending message for ${o}:`,E),h(E)}})}async sendExtRequestRaw(e,t){if(!this.extensionId)throw new a.BodhiError("not_initialized",this.createErrorClientNotInitialized({type:"ext",action:e,params:t}));this.logger.debug(`[BodhiExtClient] Sending EXT_REQUEST (raw): action=${e}`,t?{params:t}:"");const r=crypto.randomUUID(),i={type:l.MESSAGE_TYPES.EXT_REQUEST,requestId:r,request:{action:e,params:t}};return this.logger.debug(`[BodhiExtClient] Request ID: ${r}, Extension: ${this.extensionId}`),new Promise((o,s)=>{try{chrome.runtime.sendMessage(this.extensionId,i,n=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${r}:`,chrome.runtime.lastError),s(new Error(chrome.runtime.lastError.message));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${r}:`,n),!n){this.logger.error(`[BodhiExtClient] No response received for request ${r}`),s(new Error("No response from extension"));return}n.type===l.MESSAGE_TYPES.EXT_RESPONSE&&n.requestId===r?(this.logger.debug(`[BodhiExtClient] ✓ Valid EXT_RESPONSE for ${r}`),o(n)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${r}:`,n),s(new a.BodhiError("extension_error","Invalid response format")))})}catch(n){this.logger.error(`[BodhiExtClient] Exception sending message for ${r}:`,n),s(n)}})}registerStreamPortListener(e,t,r,i){chrome.runtime.onConnect.addListener(o=>{o.name===e&&(this.logger.info(`[BodhiExtClient] Port connected: ${e}`),o.onMessage.addListener(async s=>{if(s.type!==t){this.logger.warn(`[BodhiExtClient] Unknown message type on ${e}:`,s.type),o.postMessage({type:r,requestId:s.requestId,error:{message:"Unknown message type",type:"extension_error"}});return}await i(o,s)}),o.onDisconnect.addListener(()=>{this.logger.info(`[BodhiExtClient] Port disconnected: ${e}`)}))})}async handleGenericStreamRelay(e,t,r,i,o,s,n){if(this.logger.debug("[BodhiExtClient] Processing stream relay:",{requestId:t,method:r.method,endpoint:r.endpoint,bodhiPortName:i}),!this.extensionId){e.postMessage({type:s,requestId:t,error:{message:"Client not initialized (no extensionId)",type:"extension_error"}});return}try{let h={...r.headers};if(r.authenticated!==!1){const d=await this._getAccessTokenRaw();if(!d){e.postMessage({type:s,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"extension_error"}});return}h={...h,Authorization:`Bearer ${d}`},this.logger.debug("[BodhiExtClient] Injected auth token for stream relay")}const E=chrome.runtime.connect(this.extensionId,{name:i});this.activeStreamPorts.set(t,E);const u=setTimeout(()=>{this.activeStreamPorts.has(t)&&(this.logger.error(`[BodhiExtClient] Stream timeout for ${t}`),e.postMessage({type:s,requestId:t,error:{message:"Stream request timed out",type:"timeout_error"}}),this.cleanupStreamPort(t))},S.STREAM_TIMEOUT);E.onMessage.addListener(d=>{n(d,e,t)&&(clearTimeout(u),this.cleanupStreamPort(t))}),E.onDisconnect.addListener(()=>{clearTimeout(u),this.activeStreamPorts.has(t)&&(this.logger.error(`[BodhiExtClient] Bodhi port disconnected for ${t}`),e.postMessage({type:s,requestId:t,error:{message:"Connection to Bodhi extension closed unexpectedly",type:"network_error"}}),this.activeStreamPorts.delete(t))});const T={type:o,requestId:t,request:{method:r.method,endpoint:r.endpoint,body:r.body,headers:h}};this.logger.debug("[BodhiExtClient] Sending stream request to bodhi port:",T),E.postMessage(T)}catch(h){const E=h;this.logger.error("[BodhiExtClient] Stream relay error:",JSON.stringify(E.message)),e.postMessage({type:s,requestId:t,error:{message:`uncaught error: ${JSON.stringify({error:E,message:E.message})}`,type:"extension_error"}})}}cleanupStreamPort(e){const t=this.activeStreamPorts.get(e);if(t){try{t.disconnect()}catch{}this.activeStreamPorts.delete(e)}}async handleStreamRequest(e,t){const{requestId:r,request:i}=t;await this.handleGenericStreamRelay(e,r,i,l.BODHI_STREAM_PORT,l.MESSAGE_TYPES.STREAM_REQUEST,c.EXT2EXT_CLIENT_STREAM_ERROR,(o,s,n)=>{if(l.isStreamChunk(o)){const h=o.response,E=h.body;return h.status>=400?(s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:n,response:h}),!1):E?.done?(s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_DONE,requestId:n}),this.logger.info(`[BodhiExtClient] Stream complete for ${n}`),!0):(s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_CHUNK,requestId:n,response:h}),!1)}else{if(l.isStreamApiError(o))return this.logger.error(`[BodhiExtClient] Stream API error for ${n}: ${o.response.status}`),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:n,response:o.response}),!1;if(l.isStreamError(o))return this.logger.error(`[BodhiExtClient] Stream error for ${n}:`,o.error.message),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:n,error:{message:`stream error: ${JSON.stringify(o)}`,type:"extension_error"}}),!0}return!1})}async handleStreamTextRequest(e,t){const{requestId:r,request:i}=t;await this.handleGenericStreamRelay(e,r,i,l.BODHI_STREAM_TEXT_PORT,l.MESSAGE_TYPES.STREAM_TEXT_REQUEST,c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR,(o,s,n)=>{switch(o.type){case l.MESSAGE_TYPES.STREAM_TEXT_START:return s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_START,requestId:n,status:o.status,headers:o.headers}),!1;case l.MESSAGE_TYPES.STREAM_TEXT_CHUNK:return s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_CHUNK,requestId:n,chunk:o.chunk}),!1;case l.MESSAGE_TYPES.STREAM_TEXT_DONE:return this.logger.info(`[BodhiExtClient] Stream text complete for ${n}`),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_DONE,requestId:n}),!0;case l.MESSAGE_TYPES.STREAM_TEXT_ERROR:return this.logger.error(`[BodhiExtClient] Stream text error for ${n}:`,o.error.message),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR,requestId:n,error:o.error}),!0}return!1})}async storeTokens(e){const t=Date.now()+(e.expiresIn||3600)*1e3;await chrome.storage.session.set({accessToken:e.accessToken,refreshToken:e.refreshToken,idToken:e.idToken,expiresAt:t})}async _getAccessTokenRaw(){const{accessToken:e,expiresAt:t}=await chrome.storage.session.get(["accessToken","expiresAt"]);if(!e||!t)return null;if(Date.now()>=t-5*1e3){const{refreshToken:r}=await chrome.storage.session.get("refreshToken");return r?this._tryRefreshToken(r):null}return e}async _tryRefreshToken(e){if(this.refreshPromise)return this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise;this.refreshPromise=this._doRefreshToken(e);try{return await this.refreshPromise}finally{this.refreshPromise=null}}async _doRefreshToken(e){this.logger.debug("Refreshing access token");try{const t=await a.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t.success)return await this._storeRefreshedTokens(t.tokens),this.logger.info("Token refreshed successfully"),this.broadcastAuthStateChange(),t.tokens.access_token;if(t.error==="invalid_grant")return this.logger.warn("Refresh token expired or revoked, clearing tokens and logging out"),await this.clearTokens(),this.broadcastAuthStateChange(),null}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),a.createOperationError("auth_error","Access token expired and unable to refresh. Try logging out and logging in again.")}async _storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3,r={accessToken:e.access_token,expiresAt:t};e.refresh_token&&(r.refreshToken=e.refresh_token),e.id_token&&(r.idToken=e.id_token),await chrome.storage.session.set(r)}async clearTokens(){await chrome.storage.session.remove(["accessToken","refreshToken","idToken","expiresAt","codeVerifier","state","authInProgress","bodhiUserInfo"])}parseJwt(e){const r=e.split(".")[1].replace(/-/g,"+").replace(/_/g,"/"),i=decodeURIComponent(atob(r).split("").map(o=>"%"+("00"+o.charCodeAt(0).toString(16)).slice(-2)).join(""));return JSON.parse(i)}createErrorClientNotInitialized(e){return`Client not initialized. Extension discovery not triggered nor extensionId set, cannot handle request: ${JSON.stringify(e)}`}};S.STREAM_TIMEOUT=6e4;let I=S;const B="production";Object.defineProperty(exports,"InMemoryStorage",{enumerable:!0,get:()=>a.InMemoryStorage});exports.BodhiExtClient=I;exports.ChromeSessionStorageAdapter=O;exports.DEFAULT_API_TIMEOUT_MS=N;exports.DISCOVERY_ATTEMPTS=A;exports.DISCOVERY_ATTEMPT_TIMEOUT=X;exports.DISCOVERY_ATTEMPT_WAIT_MS=w;exports.DISCOVERY_TIMEOUT_MS=C;exports.EXT2EXT_CLIENT_ACTIONS=_;exports.EXT2EXT_CLIENT_MESSAGE_TYPES=c;exports.EXT2EXT_CLIENT_STREAM_PORT=y;exports.EXT2EXT_CLIENT_STREAM_TEXT_PORT=f;exports.EXT_BUILD_MODE=B;exports.ExtUIClient=U;exports.isExtClientApiError=P;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("@bodhiapp/bodhi-js-core"),l=require("@bodhiapp/bodhi-browser-types"),c={EXT2EXT_CLIENT_REQUEST:"EXT2EXT_CLIENT_REQUEST",EXT2EXT_CLIENT_RESPONSE:"EXT2EXT_CLIENT_RESPONSE",EXT2EXT_CLIENT_BROADCAST:"EXT2EXT_CLIENT_BROADCAST",EXT2EXT_CLIENT_API_REQUEST:"EXT2EXT_CLIENT_API_REQUEST",EXT2EXT_CLIENT_API_RESPONSE:"EXT2EXT_CLIENT_API_RESPONSE",EXT2EXT_CLIENT_STREAM_REQUEST:"EXT2EXT_CLIENT_STREAM_REQUEST",EXT2EXT_CLIENT_STREAM_CHUNK:"EXT2EXT_CLIENT_STREAM_CHUNK",EXT2EXT_CLIENT_STREAM_ERROR:"EXT2EXT_CLIENT_STREAM_ERROR",EXT2EXT_CLIENT_STREAM_API_ERROR:"EXT2EXT_CLIENT_STREAM_API_ERROR",EXT2EXT_CLIENT_STREAM_DONE:"EXT2EXT_CLIENT_STREAM_DONE",EXT2EXT_CLIENT_STREAM_TEXT_REQUEST:"EXT2EXT_CLIENT_STREAM_TEXT_REQUEST",EXT2EXT_CLIENT_STREAM_TEXT_START:"EXT2EXT_CLIENT_STREAM_TEXT_START",EXT2EXT_CLIENT_STREAM_TEXT_CHUNK:"EXT2EXT_CLIENT_STREAM_TEXT_CHUNK",EXT2EXT_CLIENT_STREAM_TEXT_DONE:"EXT2EXT_CLIENT_STREAM_TEXT_DONE",EXT2EXT_CLIENT_STREAM_TEXT_ERROR:"EXT2EXT_CLIENT_STREAM_TEXT_ERROR"},y="ext2ext-client-stream",f="ext2ext-client-stream-text",_={LOGIN:"login",LOGOUT:"logout",GET_AUTH_STATE:"getAuthState",DISCOVER_EXTENSION:"discoverBodhiExtension",GET_EXTENSION_ID:"get_extension_id",SET_EXTENSION_ID:"setExtensionId"},C=5e3,A=3,w=500,X=500,N=3e4;function P(m){return"error"in m}class O{async get(e){const r=(await chrome.storage.session.get(e))[e];return r!==void 0?String(r):null}async set(e){await chrome.storage.session.set(e)}async remove(e){await chrome.storage.session.remove(e)}}class b extends a.DirectClientBase{constructor(e,t){const r=a.createStoragePrefixWithNamespace(e.basePath,a.STORAGE_PREFIXES.EXT_DIRECT),i={authClientId:e.authClientId,authServerUrl:e.authServerUrl,storagePrefix:r,logLevel:e.logLevel,loggerPrefix:"DirectExtClient",apiTimeoutMs:e.apiTimeoutMs,storage:e.storage??new O,initialTokens:e.initialTokens};super(i,t)}async login(e){const t=await this.getAuthState();if(t.status==="authenticated")return t;e?.flowType==="redirect"&&this.logger.warn("Extension mode does not support redirect flow type; using popup instead");const r=e?.userRole??"scope_user_user";e?.onProgress?.("requesting");const i=new a.AccessRequestBuilder(this.authClientId).requestedRole(r).flowType("popup");e?.requested&&i.requested(e.requested);const o=i.build(),s=await this.requestAccess(o),{id:n,review_url:h}=a.unwrapResponse(s);e?.onProgress?.("reviewing"),await chrome.tabs.create({url:h});const E=await this.pollAccessRequestStatus(n,{intervalMs:e?.pollIntervalMs??a.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??a.DEFAULT_POLL_TIMEOUT_MS});if(E.status!=="approved")throw a.createOperationError("auth_error",`Access request ${E.status}`);const u=E.access_request_scope;return e?.onProgress?.("authenticating"),this.performOAuthPkce(`openid profile email roles ${u??""}`.trim())}async performOAuthPkce(e){const t=a.generateCodeVerifier(),r=await a.generateCodeChallenge(t),i=a.generateCodeVerifier();await this._storageSet({[this.storageKeys.CODE_VERIFIER]:t,[this.storageKeys.STATE]:i});const o=chrome.identity.getRedirectURL("callback"),s=new URL(this.authEndpoints.authorize);return s.searchParams.set("client_id",this.authClientId),s.searchParams.set("response_type","code"),s.searchParams.set("redirect_uri",o),s.searchParams.set("scope",e),s.searchParams.set("code_challenge",r),s.searchParams.set("code_challenge_method","S256"),s.searchParams.set("state",i),new Promise((n,h)=>{chrome.identity.launchWebAuthFlow({url:s.toString(),interactive:!0},async E=>{if(chrome.runtime.lastError){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(chrome.runtime.lastError);return}if(!E){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(a.createOperationError("oauth_error","No redirect URL received"));return}try{const u=new URL(E),T=u.searchParams.get("code"),d=u.searchParams.get("state"),g=await this._storageGet(this.storageKeys.STATE);if(d!==g){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(a.createOperationError("oauth_error","State mismatch"));return}if(!T){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(a.createOperationError("oauth_error","No authorization code"));return}await this.exchangeCodeForTokens(T);const p=await this.getAuthState();if(p.status!=="authenticated")throw a.createOperationError("oauth_error","Login failed");this.setAuthState(p),await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),n(p)}catch(u){await this._storageRemove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),h(u)}})})}_getRedirectUri(){return chrome.identity.getRedirectURL("callback")}}class k{constructor(e={},t){this.state={type:"extension",extension:"not-initialized",extensionId:null,server:a.PENDING_EXTENSION_READY},this.extensionId=null,this.broadcastListenerActive=!1,this.config=e,this.logger=new a.Logger("ExtClient",e?.logLevel||"warn"),this.onStateChange=t??a.NOOP_STATE_CALLBACK,this.apiTimeoutMs=e.apiTimeoutMs??N,this.authClientId=e.authClientId??""}setState(e){this.state=e,this.onStateChange({type:"client-state",state:e})}setAuthState(e){this.onStateChange({type:"auth-state",state:e})}setStateCallback(e){this.onStateChange=e}setupBroadcastListener(){this.broadcastListenerActive||(this.broadcastListenerActive=!0,chrome.runtime.onMessage.addListener(e=>{const t=e;return t?.type===c.EXT2EXT_CLIENT_BROADCAST&&t.event==="authStateChanged"&&this.handleAuthStateChangedBroadcast(),!1}),this.logger.debug("Broadcast listener setup complete"))}async handleAuthStateChangedBroadcast(){this.logger.debug("Received authStateChanged broadcast, refreshing auth state");const e=await this.getAuthState();this.setAuthState(e)}generateRequestId(){return crypto.randomUUID()}getState(){return this.state}isClientInitialized(){return this.state.extension==="ready"}isServerReady(){return this.isClientInitialized()&&this.state.server.status==="ready"}async init(e={}){if(!e.testConnection&&!e.selectedConnection){this.logger.info("No testConnection or selectedConnection, returning not-initialized state");const i=a.createExtensionStateNotInitialized();return this.setState(i),i}if(this.extensionId&&!e.testConnection)return this.logger.debug("Already initialized with extensionId, skipping discovery"),this.state;const t=e.timeoutMs??this.config.initParams?.extension?.timeoutMs??C,r=e.savedState?.extensionId;try{if(!this.extensionId){if(r)this.logger.info("Restoring with known extensionId:",r),await this.sendExtMessageWithTimeout(_.SET_EXTENSION_ID,{extensionId:r},t),this.extensionId=r;else{this.logger.info("Discovering bodhi-browser extension...");const s={attempts:this.config.initParams?.extension?.attempts,attemptWaitMs:this.config.initParams?.extension?.attemptWaitMs,attemptTimeout:this.config.initParams?.extension?.attemptTimeout},n=await this.sendExtMessageWithTimeout(_.DISCOVER_EXTENSION,s,t);this.extensionId=n.extensionId,this.logger.info("Extension discovered:",this.extensionId)}this.setupBroadcastListener()}const i={type:"extension",extension:"ready",extensionId:this.extensionId,server:a.PENDING_EXTENSION_READY};let o=a.PENDING_EXTENSION_READY;if(e.testConnection)try{o=await this.getServerState(),this.logger.info("Server connectivity tested, state:",o.status)}catch(s){this.logger.error("Failed to get server state:",s),o=a.BACKEND_SERVER_NOT_REACHABLE}return this.setState({...i,server:o}),this.state}catch(i){this.logger.error("Failed to initialize extension:",i),this.extensionId=null;const o=a.createExtensionStateNotFound();return this.setState(o),this.state}}async sendExtMessageWithTimeout(e,t,r=1e4){const i=new Promise((o,s)=>setTimeout(()=>s(new Error("Timeout")),r));return Promise.race([this.sendExtRequest(e,t),i])}async sendExtRequest(e,t){try{const r=this.generateRequestId(),i=await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_REQUEST,requestId:r,request:{action:e,params:t}});if(!i)throw a.createOperationError("extension_error","No response from background script");if(i.type!==c.EXT2EXT_CLIENT_RESPONSE)throw a.createOperationError("extension_error","Invalid response type from background script");const o=i.response;if(l.isExtError(o)){const s=o.error.type||"extension_error";throw a.createOperationError(s,o.error.message)}return o}catch(r){throw r instanceof a.BodhiError?r:a.createOperationError("extension_error",r instanceof Error?r.message:"Unknown error occurred")}}async sendRawApiMessage(e,t,r,i,o){const s=this.generateRequestId();return await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_API_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i,authenticated:o}})}async sendApiRequest(e,t,r,i,o){try{const s=new Promise((h,E)=>setTimeout(()=>E(new Error(`[bodhi-js-sdk/ext] network timeout: api request not completed within configured/default timeout of ${this.apiTimeoutMs}ms`)),this.apiTimeoutMs)),n=await Promise.race([this.sendRawApiMessage(e,t,r,i,o),s]);if(P(n)){const h=n.error.type||"extension_error";throw new a.BodhiError(h,n.error.message)}return n.response}catch(s){if(s instanceof a.BodhiError)throw s;const n=s instanceof Error?s.message:String(s);throw new a.BodhiError("network_error",n)}}async login(e){return new Promise((t,r)=>{const i=async o=>{if(o&&typeof o=="object"&&"type"in o&&o.type==="EXT2EXT_CLIENT_BROADCAST"&&"event"in o&&o.event==="authStateChanged"){chrome.runtime.onMessage.removeListener(i);try{const s=await this.getAuthState();if(a.isAuthError(s)){r(a.createOperationError("auth_error",`Login failed: ${s.error?.message}`));return}if(s.status!=="authenticated"){r(a.createOperationError("auth_error","Login failed: User is not logged in"));return}this.setAuthState(s),t(s)}catch(s){r(s)}}};chrome.runtime.onMessage.addListener(i),this.sendExtRequest(_.LOGIN,e).catch(o=>{chrome.runtime.onMessage.removeListener(i),r(o)})})}async logout(){await this.sendExtRequest(_.LOGOUT);const e={status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1};return this.setAuthState(e),e}async getAuthState(){return this.isClientInitialized()?(await this.sendExtRequest(_.GET_AUTH_STATE)).authState:a.INITIAL_AUTH_STATE}async pingApi(){return this.sendApiRequest("GET","/ping")}async getServerState(){let e;try{e=await this.sendApiRequest("GET","/bodhi/v1/info")}catch(o){const s=o instanceof Error?o.message:"Connection failed",n=o instanceof a.BodhiError?o.code:"extension_error";return{status:"not-reachable",version:null,error:{message:s,type:n}}}if(e.status>=400)return{status:"not-reachable",version:null,error:{message:"API error from server",type:"extension_error"}};const t=e.body,r=t.version||"unknown",i={deployment:t.deployment??null,client_id:t.client_id??null};switch(t.status){case"ready":return{status:"ready",version:r,error:null,...i};case"setup":return{status:"setup",version:r,error:t.error?{message:t.error.message,type:t.error.type}:{message:"Setup required",type:"extension_error"},...i};case"resource_admin":return{status:"resource_admin",version:r,error:t.error?{message:t.error.message,type:t.error.type}:{message:"Resource admin required",type:"extension_error"},...i};case"error":return{status:"error",version:r,error:t.error?{message:t.error.message,type:t.error.type}:{message:"Server error",type:"extension_error"},...i};default:return{status:"not-reachable",version:null,error:{message:"Unknown server status",type:"extension_error"}}}}async*stream(e,t,r,i,o=!0){const s=this.generateRequestId();this.logger.debug("Starting stream",{method:e,endpoint:t,requestId:s});const n=chrome.runtime.connect({name:y}),E=new ReadableStream({start:u=>{n.onMessage.addListener(T=>{if(T.requestId===s)switch(T.type){case c.EXT2EXT_CLIENT_STREAM_DONE:this.logger.debug("Stream complete",{requestId:s}),u.close(),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_ERROR:this.logger.error("Stream error",{requestId:s,error:JSON.stringify(T.error)}),u.error(new a.BodhiError("extension_error",T.error.message)),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_API_ERROR:{const d=T;this.logger.error("Stream API error",{requestId:s,error:d.response.body?.error}),u.error(new a.BodhiApiError(d.response.status,d.response.body,d.response.body?.error?.message||"API error")),n.disconnect();break}case c.EXT2EXT_CLIENT_STREAM_CHUNK:{const d=T;l.isApiSuccessResponse(d.response)&&u.enqueue(d.response.body);break}}}),n.onDisconnect.addListener(()=>{this.logger.debug("Port disconnected",{requestId:s});try{u.error(new a.BodhiError("connection_closed","Connection closed unexpectedly"))}catch{}}),n.postMessage({type:c.EXT2EXT_CLIENT_STREAM_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i,authenticated:o}})}}).getReader();try{for(;;){const{done:u,value:T}=await E.read();if(u){this.logger.debug("Stream iteration complete");break}yield T}}finally{E.releaseLock()}}async streamText(e,t,r,i,o=!0){const s=this.generateRequestId();return new Promise((n,h)=>{let E,u=!1;const T=new ReadableStream({start:g=>{E=g}}),d=chrome.runtime.connect({name:f});d.onMessage.addListener(g=>{if(g.requestId===s)switch(g.type){case c.EXT2EXT_CLIENT_STREAM_TEXT_START:{u=!0;async function*p(L){const x=L.getReader();try{for(;;){const{done:M,value:v}=await x.read();if(M)break;yield v}}finally{x.releaseLock()}}n({status:g.status,headers:g.headers,body:p(T)});break}case c.EXT2EXT_CLIENT_STREAM_TEXT_CHUNK:E.enqueue(g.chunk);break;case c.EXT2EXT_CLIENT_STREAM_TEXT_DONE:E.close(),d.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR:{const p=a.createOperationError("extension_error",g.error.message);u?E.error(p):h(p),d.disconnect();break}}}),d.onDisconnect.addListener(()=>{if(!u)h(a.createOperationError("extension_error","Connection closed unexpectedly"));else try{E.error(a.createOperationError("extension_error","Connection closed unexpectedly"))}catch{}}),d.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i,authenticated:o}})})}get chat(){return this._chat??=new a.Chat(this)}get models(){return this._models??=new a.Models(this)}get embeddings(){return this._embeddings??=new a.Embeddings(this)}get mcps(){return this._mcps??=new a.Mcps(this)}async requestAccess(e){return this.sendApiRequest("POST","/bodhi/v1/apps/request-access",e,{},!1)}async getAccessRequestStatus(e){return this.sendApiRequest("GET",`/bodhi/v1/apps/access-requests/${e}?app_client_id=${encodeURIComponent(this.authClientId)}`,void 0,{},!1)}async pollAccessRequestStatus(e,t){return a.pollAccessRequestUntilResolved(r=>this.getAccessRequestStatus(r),e,t)}serialize(){return{extensionId:this.extensionId??void 0}}async debug(){return{type:"ExtClient",state:this.state,authState:await this.getAuthState()}}}class U extends a.BaseFacadeClient{constructor(e,t,r){const i=t||{},o={basePath:i.basePath||"/",authServerUrl:i.authServerUrl||"https://id.getbodhi.app/realms/bodhi",logLevel:i.logLevel||"warn",apiTimeoutMs:i.apiTimeoutMs,storage:i.storage,initialTokens:i.initialTokens,initParams:i.initParams};super(e,o,r)}createLogger(e){return new a.Logger("ExtUIClient",e.logLevel)}createStoragePrefix(e){return a.createStoragePrefixWithNamespace(e.basePath,a.STORAGE_PREFIXES.EXT)}createExtClient(e,t){return new k({authClientId:this.authClientId,logLevel:e.logLevel,apiTimeoutMs:e.apiTimeoutMs,initParams:e.initParams},t)}createDirectClient(e,t,r){return new b({authClientId:e,authServerUrl:t.authServerUrl,logLevel:t.logLevel,basePath:t.basePath,apiTimeoutMs:t.apiTimeoutMs,storage:t.storage,initialTokens:t.initialTokens},r)}}const q=["ggedphdcbekjlomjaidbajglgihbeaon"],D=["bjdjhiombmfbcoeojijpfckljjghmjbf"],R="production",S=class S{constructor(e,t){this.isAuthenticating=!1,this.state="setup",this.listenersInitialized=!1,this.refreshPromise=null,this.activeStreamPorts=new Map,this.authClientId=e,this.authServerUrl=t?.authServerUrl||"https://id.getbodhi.app/realms/bodhi",this.extensionId=t?.extensionId,this.logger=new a.Logger("BodhiExtClient",t?.logLevel||"warn"),this.attempts=t?.attempts??A,this.attemptWaitMs=t?.attemptWaitMs??w,this.attemptTimeout=t?.attemptTimeout??X,this.authEndpoints={authorize:`${this.authServerUrl}/protocol/openid-connect/auth`,token:`${this.authServerUrl}/protocol/openid-connect/token`,userinfo:`${this.authServerUrl}/protocol/openid-connect/userinfo`,logout:`${this.authServerUrl}/protocol/openid-connect/logout`,revoke:`${this.authServerUrl}/protocol/openid-connect/revoke`},this.extensionId?this.logger.info(`[BodhiExtClient] Created client for extension: ${this.extensionId}`):this.logger.info("[BodhiExtClient] Created client without extension ID (call init() to discover)")}static base64UrlEncode(e){return btoa(String.fromCharCode(...new Uint8Array(e))).replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}static generateCodeVerifier(){const e=new Uint8Array(32);return crypto.getRandomValues(e),S.base64UrlEncode(e.buffer)}static async generateCodeChallenge(e){const r=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",r);return S.base64UrlEncode(i)}getState(){return this.state}getExtensionIdsForEnvironment(){const t=R!=="production"?q:D;return this.logger.info("[Ext2Ext/Registry] Environment: production"),this.logger.debug("[Ext2Ext/Registry] Using extension IDs:",t),t}async pingExtension(e){return this.logger.debug(`[Ext2Ext/Discovery] Pinging extension: ${e} with timeout ${this.attemptTimeout}ms`),new Promise((t,r)=>{const i=setTimeout(()=>{this.logger.debug(`[Ext2Ext/Discovery] Timeout waiting for extension ${e}`),r(new Error("Timeout"))},this.attemptTimeout);try{const o={type:l.MESSAGE_TYPES.EXT_REQUEST,requestId:crypto.randomUUID(),request:{action:"get_extension_id"}};this.logger.debug(`[Ext2Ext/Discovery] Sending message to ${e}:`,o),chrome.runtime.sendMessage(e,o,s=>{if(clearTimeout(i),chrome.runtime.lastError){this.logger.error(`[Ext2Ext/Discovery] Error from extension ${e}:`,chrome.runtime.lastError.message),r(new Error(chrome.runtime.lastError.message));return}this.logger.debug(`[Ext2Ext/Discovery] Response from ${e}:`,s);const n=s;n&&n.type===l.MESSAGE_TYPES.EXT_RESPONSE?(this.logger.debug(`[Ext2Ext/Discovery] ✓ Extension ${e} responded`),t(!0)):(this.logger.error(`[Ext2Ext/Discovery] Invalid response from ${e}:`,s),r(new Error("Invalid response")))})}catch(o){this.logger.error(`[Ext2Ext/Discovery] Exception pinging ${e}:`,o),clearTimeout(i),r(o)}})}sleep(e){return new Promise(t=>setTimeout(t,e))}async discoverBodhiExtension(e){const{attempts:t,attemptWaitMs:r,attemptTimeout:i}=e;this.logger.info(`[Ext2Ext/Discovery] Starting discovery: ${t} attempts per ID, ${i}ms timeout, ${r}ms between attempts`);const o=this.getExtensionIdsForEnvironment();this.logger.debug(`[Ext2Ext/Discovery] Will try ${o.length} extension(s):`,o);for(const h of o){for(let E=1;E<=t;E++){this.logger.debug(`[Ext2Ext/Discovery] Trying ${h} - attempt ${E}/${t}`);try{return await this.pingExtension(h),this.logger.info(`[Ext2Ext/Discovery] ✓ Found: ${h} on attempt ${E}`),{success:!0,extensionId:h}}catch(u){this.logger.debug(`[Ext2Ext/Discovery] Attempt ${E} failed for ${h}: ${u instanceof Error?u.message:"Unknown error"}`),E<t&&await this.sleep(r)}}this.logger.warn(`[Ext2Ext/Discovery] ✗ Not found: ${h} after ${t} attempts`)}const s=o.join(", "),n=`Extension not found. Tried ${o.length} IDs with ${t} attempts each: ${s}`;return this.logger.error(`[Ext2Ext/Discovery] ${n}`),{success:!1,error:n}}setupListeners(){if(this.listenersInitialized){this.logger.debug("[BodhiExtClient] Listeners already initialized, skipping");return}this.listenersInitialized=!0,chrome.runtime.onMessage.addListener((e,t,r)=>e.type!==c.EXT2EXT_CLIENT_REQUEST&&e.type!==c.EXT2EXT_CLIENT_API_REQUEST?!1:this.state!=="ready"&&!(e.type===c.EXT2EXT_CLIENT_REQUEST&&e.request.action===_.DISCOVER_EXTENSION)?(e.type===c.EXT2EXT_CLIENT_REQUEST?r({type:c.EXT2EXT_CLIENT_RESPONSE,requestId:e.requestId,response:{error:{message:this.createErrorClientNotInitialized(e),type:"NOT_INITIALIZED"}}}):r({type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:e.requestId,error:{message:`Client not initialized. Extension discovery not complete, cannot handle type:${e.type}, message:${JSON.stringify(e)}`,type:"NOT_INITIALIZED"}}),!0):(this.logger.debug(`[BodhiExtClient] Processing message.type=${e.type}`),(async()=>{const i=await this.handleAction(e);r(i)})(),!0)),this.registerStreamPortListener(y,c.EXT2EXT_CLIENT_STREAM_REQUEST,c.EXT2EXT_CLIENT_STREAM_ERROR,(e,t)=>this.handleStreamRequest(e,t)),this.registerStreamPortListener(f,c.EXT2EXT_CLIENT_STREAM_TEXT_REQUEST,c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR,(e,t)=>this.handleStreamTextRequest(e,t)),this.logger.info("[BodhiExtClient] Streaming listeners initialized")}async init(e){if(this.setupListeners(),this.extensionId){this.state="ready",this.logger.warn(`[BodhiExtClient] Already initialized with extension ID: ${this.extensionId}`);return}this.logger.info("[BodhiExtClient] Starting discovery");const t={attempts:e?.attempts??this.attempts,attemptWaitMs:e?.attemptWaitMs??this.attemptWaitMs,attemptTimeout:e?.attemptTimeout??this.attemptTimeout},r=await this.discoverBodhiExtension(t);if(!r.success||!r.extensionId)throw new Error(r.error||"Discovery failed");this.extensionId=r.extensionId,this.state="ready",this.logger.info(`[BodhiExtClient] ✓ Initialized: ${this.extensionId}`)}broadcastAuthStateChange(){chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_BROADCAST,event:"authStateChanged"}).catch(e=>{this.logger.debug("[BodhiExtClient] No listeners for broadcast:",e.message)})}async getExtensionIdFromExt(){this.logger.debug("[BodhiExtClient] Getting extension ID from bodhi-browser-ext");const e=await this.sendExtRequest("get_extension_id");return this.logger.debug("[BodhiExtClient] Extension ID response:",e),e.extension_id}async handleApiRequest(e){const{requestId:t}=e;this.logger.debug("[BodhiExtClient] Handling API request:",e.request);try{let r=e.request.headers||{};if(e.request.authenticated){const o=await this._getAccessTokenRaw();if(!o)return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"auth_error"}};r={...r,Authorization:`Bearer ${o}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const i=await this.sendApiRequest(e.request.method,e.request.endpoint,e.request.body,r);return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,response:i}}catch(r){return this.logger.error("[BodhiExtClient] API request failed:",r),{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:r instanceof Error?r.message:"Unknown error",type:"network_error"}}}}async handleExtClientRequest(e){const{requestId:t,request:r}=e,{action:i,params:o}=r;this.logger.debug(`[BodhiExtClient] Handling action: ${i}`);try{let s={};switch(i){case _.DISCOVER_EXTENSION:{const n=o;await this.init(n),this.logger.info("[BodhiExtClient] Discovery successful:",{extensionId:this.extensionId,environment:R}),s={extensionId:this.extensionId,environment:R};break}case _.SET_EXTENSION_ID:{const{extensionId:n}=o;this.extensionId=n,this.state="ready",this.logger.info("[BodhiExtClient] Extension ID set:",{extensionId:n}),s={success:!0};break}case _.GET_EXTENSION_ID:{const n=await this.sendExtRequestRaw(l.EXT_ACTIONS.GET_EXTENSION_ID,o);return l.isExtError(n.response)?{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:n.response.error.message||`Extension request failed to get extension ID: ${JSON.stringify(n.response)}`,type:n.response.error.type}}}:{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:n.response}}case _.LOGIN:{const n=o;await this.login(n),this.broadcastAuthStateChange();break}case _.LOGOUT:await this.logout(),this.broadcastAuthStateChange();break;case _.GET_AUTH_STATE:s={authState:await this.getAuthState()};break;default:return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown action: ${i}`,type:"UNKNOWN_ACTION"}}}}return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:s}}catch(s){return this.logger.error("[BodhiExtClient] Unexpected error:",s),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:s instanceof Error?s.message:`Unexpected error: ${JSON.stringify(s)}`}}}}}async handleAction(e){switch(e.type){case c.EXT2EXT_CLIENT_API_REQUEST:return this.handleApiRequest(e);case c.EXT2EXT_CLIENT_REQUEST:return this.handleExtClientRequest(e);default:{const{requestId:t}=e;return this.logger.error("[BodhiExtClient] Unknown message type:",e.type),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown message type: ${e.type}`,type:"UNKNOWN_MESSAGE_TYPE"}}}}}}async login(e){if(!(this.isAuthenticating||(await this.getAuthState()).status==="authenticated")){this.isAuthenticating=!0;try{if(!this.extensionId)throw new Error("Extension not discovered. Please detect Bodhi extension before login.");e?.flowType==="redirect"&&this.logger.warn("Extension mode does not support redirect flow type; using popup instead");const r=e?.userRole??"scope_user_user",i=new a.AccessRequestBuilder(this.authClientId).requestedRole(r).flowType("popup");e?.requested&&i.requested(e.requested);const o=i.build(),s=await this.requestAccess(o),{id:n,review_url:h}=a.unwrapResponse(s);await chrome.tabs.create({url:h});const E=await this.pollAccessRequestStatus(n,{intervalMs:e?.pollIntervalMs??a.DEFAULT_POLL_INTERVAL_MS,timeoutMs:e?.pollTimeoutMs??a.DEFAULT_POLL_TIMEOUT_MS});if(E.status!=="approved")throw a.createOperationError("auth_error",`Access request ${E.status}`);const T=`openid profile email roles ${E.access_request_scope??""}`.trim();await this.performOAuthPkce(T)}finally{this.isAuthenticating=!1}}}async exchangeCodeForTokens(e){if((await this.getAuthState()).status==="authenticated")return;const{codeVerifier:r}=await chrome.storage.session.get("codeVerifier"),i=chrome.identity.getRedirectURL("callback"),o=await fetch(this.authEndpoints.token,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({grant_type:"authorization_code",code:e,redirect_uri:i,client_id:this.authClientId,code_verifier:r})});if(!o.ok){const n=await o.text();throw new Error(`Token exchange failed: ${o.status} ${n}`)}const s=await o.json();await this.storeTokens({accessToken:s.access_token,refreshToken:s.refresh_token,idToken:s.id_token,expiresIn:s.expires_in})}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1};try{const t=this.parseJwt(e);return{status:"authenticated",user:{sub:t.sub,email:t.email,name:t.name,given_name:t.given_name,family_name:t.family_name,preferred_username:t.preferred_username},accessToken:e,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}catch(t){return this.logger.error("Failed to parse token:",t),{status:"unauthenticated",user:null,accessToken:null,error:null,refreshToken:null,expiresAt:null,isTokenRefresh:!1}}}async logout(){const{refreshToken:e}=await chrome.storage.session.get("refreshToken");if(e)try{await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({token:e,client_id:this.authClientId,token_type_hint:"refresh_token"})})}catch(t){this.logger.warn("[OAuth] Token revocation failed:",t)}await this.clearTokens()}async requestAccess(e){return this.sendApiRequest("POST","/bodhi/v1/apps/request-access",e)}async getAccessRequestStatus(e){return this.sendApiRequest("GET",`/bodhi/v1/apps/access-requests/${e}?app_client_id=${encodeURIComponent(this.authClientId)}`)}async pollAccessRequestStatus(e,t){return a.pollAccessRequestUntilResolved(r=>this.getAccessRequestStatus(r),e,t)}async performOAuthPkce(e){const t=S.generateCodeVerifier(),r=await S.generateCodeChallenge(t),i=S.generateCodeVerifier();await chrome.storage.session.set({codeVerifier:t,state:i,authInProgress:!0});const o=chrome.identity.getRedirectURL("callback"),s=new URL(this.authEndpoints.authorize);return s.searchParams.set("client_id",this.authClientId),s.searchParams.set("response_type","code"),s.searchParams.set("redirect_uri",o),s.searchParams.set("scope",e),s.searchParams.set("code_challenge",r),s.searchParams.set("code_challenge_method","S256"),s.searchParams.set("state",i),new Promise((n,h)=>{chrome.identity.launchWebAuthFlow({url:s.toString(),interactive:!0},async E=>{if(await chrome.storage.session.set({authInProgress:!1}),chrome.runtime.lastError){await chrome.storage.session.remove(["codeVerifier","state"]),h(chrome.runtime.lastError);return}if(!E){await chrome.storage.session.remove(["codeVerifier","state"]),h(a.createOperationError("oauth_error","No redirect URL received"));return}try{const u=new URL(E),T=u.searchParams.get("code"),d=u.searchParams.get("state"),{state:g}=await chrome.storage.session.get("state");if(d!==g){await chrome.storage.session.remove(["codeVerifier","state"]),h(a.createOperationError("oauth_error","State mismatch"));return}if(!T){await chrome.storage.session.remove(["codeVerifier","state"]),h(a.createOperationError("oauth_error","No authorization code"));return}await this.exchangeCodeForTokens(T),await chrome.storage.session.remove(["codeVerifier","state"]);const p=await this.getAuthState();if(p.status!=="authenticated")throw a.createOperationError("oauth_error","Login failed");n(p)}catch(u){await chrome.storage.session.remove(["codeVerifier","state"]),h(u)}})})}async sendExtRequest(e,t){const r=await this.sendExtRequestRaw(e,t);if(l.isExtError(r.response))throw this.logger.error("[BodhiExtClient] Extension error:",r.response.error),new Error(r.response.error.message||`Extension request failed: ${JSON.stringify(r.response)}`);return r.response}async sendApiRequest(e,t,r,i){if(!this.extensionId)throw new a.BodhiError("not_initialized",this.createErrorClientNotInitialized({type:"api",method:e,endpoint:t}));this.logger.debug(`[BodhiExtClient] Sending API_REQUEST: method=${e}, endpoint=${t}`,r?{body:r}:"");const o=crypto.randomUUID(),s={type:l.MESSAGE_TYPES.API_REQUEST,requestId:o,request:{method:e,endpoint:t,body:r,headers:i}};return this.logger.debug(`[BodhiExtClient] Request ID: ${o}, Extension: ${this.extensionId}`),new Promise((n,h)=>{try{chrome.runtime.sendMessage(this.extensionId,s,E=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${o}:`,chrome.runtime.lastError),h(new a.BodhiError("extension_error",chrome.runtime.lastError.message??"Chrome runtime error"));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${o}:`,E),!E){this.logger.error(`[BodhiExtClient] No response received for request ${o}`),h(new a.BodhiError("extension_error","No response from extension"));return}E.type===l.MESSAGE_TYPES.API_RESPONSE&&E.requestId===o?"error"in E?(this.logger.error(`[BodhiExtClient] API error for ${o}:`,E.error),h(new a.BodhiError(E.error.type||"extension_error",E.error.message))):(this.logger.debug(`[BodhiExtClient] ✓ Valid API_RESPONSE for ${o}`),n(E.response)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${o}:`,E),h(new a.BodhiError("extension_error","Invalid response format")))})}catch(E){this.logger.error(`[BodhiExtClient] Exception sending message for ${o}:`,E),h(E)}})}async sendExtRequestRaw(e,t){if(!this.extensionId)throw new a.BodhiError("not_initialized",this.createErrorClientNotInitialized({type:"ext",action:e,params:t}));this.logger.debug(`[BodhiExtClient] Sending EXT_REQUEST (raw): action=${e}`,t?{params:t}:"");const r=crypto.randomUUID(),i={type:l.MESSAGE_TYPES.EXT_REQUEST,requestId:r,request:{action:e,params:t}};return this.logger.debug(`[BodhiExtClient] Request ID: ${r}, Extension: ${this.extensionId}`),new Promise((o,s)=>{try{chrome.runtime.sendMessage(this.extensionId,i,n=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${r}:`,chrome.runtime.lastError),s(new Error(chrome.runtime.lastError.message));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${r}:`,n),!n){this.logger.error(`[BodhiExtClient] No response received for request ${r}`),s(new Error("No response from extension"));return}n.type===l.MESSAGE_TYPES.EXT_RESPONSE&&n.requestId===r?(this.logger.debug(`[BodhiExtClient] ✓ Valid EXT_RESPONSE for ${r}`),o(n)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${r}:`,n),s(new a.BodhiError("extension_error","Invalid response format")))})}catch(n){this.logger.error(`[BodhiExtClient] Exception sending message for ${r}:`,n),s(n)}})}registerStreamPortListener(e,t,r,i){chrome.runtime.onConnect.addListener(o=>{o.name===e&&(this.logger.info(`[BodhiExtClient] Port connected: ${e}`),o.onMessage.addListener(async s=>{if(s.type!==t){this.logger.warn(`[BodhiExtClient] Unknown message type on ${e}:`,s.type),o.postMessage({type:r,requestId:s.requestId,error:{message:"Unknown message type",type:"extension_error"}});return}await i(o,s)}),o.onDisconnect.addListener(()=>{this.logger.info(`[BodhiExtClient] Port disconnected: ${e}`)}))})}async handleGenericStreamRelay(e,t,r,i,o,s,n){if(this.logger.debug("[BodhiExtClient] Processing stream relay:",{requestId:t,method:r.method,endpoint:r.endpoint,bodhiPortName:i}),!this.extensionId){e.postMessage({type:s,requestId:t,error:{message:"Client not initialized (no extensionId)",type:"extension_error"}});return}try{let h={...r.headers};if(r.authenticated!==!1){const d=await this._getAccessTokenRaw();if(!d){e.postMessage({type:s,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"extension_error"}});return}h={...h,Authorization:`Bearer ${d}`},this.logger.debug("[BodhiExtClient] Injected auth token for stream relay")}const E=chrome.runtime.connect(this.extensionId,{name:i});this.activeStreamPorts.set(t,E);const u=setTimeout(()=>{this.activeStreamPorts.has(t)&&(this.logger.error(`[BodhiExtClient] Stream timeout for ${t}`),e.postMessage({type:s,requestId:t,error:{message:"Stream request timed out",type:"timeout_error"}}),this.cleanupStreamPort(t))},S.STREAM_TIMEOUT);E.onMessage.addListener(d=>{n(d,e,t)&&(clearTimeout(u),this.cleanupStreamPort(t))}),E.onDisconnect.addListener(()=>{clearTimeout(u),this.activeStreamPorts.has(t)&&(this.logger.error(`[BodhiExtClient] Bodhi port disconnected for ${t}`),e.postMessage({type:s,requestId:t,error:{message:"Connection to Bodhi extension closed unexpectedly",type:"network_error"}}),this.activeStreamPorts.delete(t))});const T={type:o,requestId:t,request:{method:r.method,endpoint:r.endpoint,body:r.body,headers:h}};this.logger.debug("[BodhiExtClient] Sending stream request to bodhi port:",T),E.postMessage(T)}catch(h){const E=h;this.logger.error("[BodhiExtClient] Stream relay error:",JSON.stringify(E.message)),e.postMessage({type:s,requestId:t,error:{message:`uncaught error: ${JSON.stringify({error:E,message:E.message})}`,type:"extension_error"}})}}cleanupStreamPort(e){const t=this.activeStreamPorts.get(e);if(t){try{t.disconnect()}catch{}this.activeStreamPorts.delete(e)}}async handleStreamRequest(e,t){const{requestId:r,request:i}=t;await this.handleGenericStreamRelay(e,r,i,l.BODHI_STREAM_PORT,l.MESSAGE_TYPES.STREAM_REQUEST,c.EXT2EXT_CLIENT_STREAM_ERROR,(o,s,n)=>{if(l.isStreamChunk(o)){const h=o.response,E=h.body;return h.status>=400?(s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:n,response:h}),!1):E?.done?(s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_DONE,requestId:n}),this.logger.info(`[BodhiExtClient] Stream complete for ${n}`),!0):(s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_CHUNK,requestId:n,response:h}),!1)}else{if(l.isStreamApiError(o))return this.logger.error(`[BodhiExtClient] Stream API error for ${n}: ${o.response.status}`),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:n,response:o.response}),!1;if(l.isStreamError(o))return this.logger.error(`[BodhiExtClient] Stream error for ${n}:`,o.error.message),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:n,error:{message:`stream error: ${JSON.stringify(o)}`,type:"extension_error"}}),!0}return!1})}async handleStreamTextRequest(e,t){const{requestId:r,request:i}=t;await this.handleGenericStreamRelay(e,r,i,l.BODHI_STREAM_TEXT_PORT,l.MESSAGE_TYPES.STREAM_TEXT_REQUEST,c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR,(o,s,n)=>{switch(o.type){case l.MESSAGE_TYPES.STREAM_TEXT_START:return s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_START,requestId:n,status:o.status,headers:o.headers}),!1;case l.MESSAGE_TYPES.STREAM_TEXT_CHUNK:return s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_CHUNK,requestId:n,chunk:o.chunk}),!1;case l.MESSAGE_TYPES.STREAM_TEXT_DONE:return this.logger.info(`[BodhiExtClient] Stream text complete for ${n}`),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_DONE,requestId:n}),!0;case l.MESSAGE_TYPES.STREAM_TEXT_ERROR:return this.logger.error(`[BodhiExtClient] Stream text error for ${n}:`,o.error.message),s.postMessage({type:c.EXT2EXT_CLIENT_STREAM_TEXT_ERROR,requestId:n,error:o.error}),!0}return!1})}async storeTokens(e){const t=Date.now()+(e.expiresIn||3600)*1e3;await chrome.storage.session.set({accessToken:e.accessToken,refreshToken:e.refreshToken,idToken:e.idToken,expiresAt:t})}async _getAccessTokenRaw(){const{accessToken:e,expiresAt:t}=await chrome.storage.session.get(["accessToken","expiresAt"]);if(!e||!t)return null;if(Date.now()>=t-5*1e3){const{refreshToken:r}=await chrome.storage.session.get("refreshToken");return r?this._tryRefreshToken(r):null}return e}async _tryRefreshToken(e){if(this.refreshPromise)return this.logger.debug("Refresh already in progress, returning existing promise"),this.refreshPromise;this.refreshPromise=this._doRefreshToken(e);try{return await this.refreshPromise}finally{this.refreshPromise=null}}async _doRefreshToken(e){this.logger.debug("Refreshing access token");try{const t=await a.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t.success)return await this._storeRefreshedTokens(t.tokens),this.logger.info("Token refreshed successfully"),this.broadcastAuthStateChange(),t.tokens.access_token;if(t.error==="invalid_grant")return this.logger.warn("Refresh token expired or revoked, clearing tokens and logging out"),await this.clearTokens(),this.broadcastAuthStateChange(),null}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),a.createOperationError("auth_error","Access token expired and unable to refresh. Try logging out and logging in again.")}async _storeRefreshedTokens(e){const t=Date.now()+e.expires_in*1e3,r={accessToken:e.access_token,expiresAt:t};e.refresh_token&&(r.refreshToken=e.refresh_token),e.id_token&&(r.idToken=e.id_token),await chrome.storage.session.set(r)}async clearTokens(){await chrome.storage.session.remove(["accessToken","refreshToken","idToken","expiresAt","codeVerifier","state","authInProgress","bodhiUserInfo"])}parseJwt(e){const r=e.split(".")[1].replace(/-/g,"+").replace(/_/g,"/"),i=decodeURIComponent(atob(r).split("").map(o=>"%"+("00"+o.charCodeAt(0).toString(16)).slice(-2)).join(""));return JSON.parse(i)}createErrorClientNotInitialized(e){return`Client not initialized. Extension discovery not triggered nor extensionId set, cannot handle request: ${JSON.stringify(e)}`}};S.STREAM_TIMEOUT=6e4;let I=S;const B="production";Object.defineProperty(exports,"InMemoryStorage",{enumerable:!0,get:()=>a.InMemoryStorage});exports.BodhiExtClient=I;exports.ChromeSessionStorageAdapter=O;exports.DEFAULT_API_TIMEOUT_MS=N;exports.DISCOVERY_ATTEMPTS=A;exports.DISCOVERY_ATTEMPT_TIMEOUT=X;exports.DISCOVERY_ATTEMPT_WAIT_MS=w;exports.DISCOVERY_TIMEOUT_MS=C;exports.EXT2EXT_CLIENT_ACTIONS=_;exports.EXT2EXT_CLIENT_MESSAGE_TYPES=c;exports.EXT2EXT_CLIENT_STREAM_PORT=y;exports.EXT2EXT_CLIENT_STREAM_TEXT_PORT=f;exports.EXT_BUILD_MODE=B;exports.ExtUIClient=U;exports.isExtClientApiError=P;
@@ -1,4 +1,4 @@
1
- import { DirectClientBase as $, createStoragePrefixWithBasePath as X, STORAGE_PREFIXES as N, AccessRequestBuilder as v, unwrapResponse as L, DEFAULT_POLL_TIMEOUT_MS as P, DEFAULT_POLL_INTERVAL_MS as b, createOperationError as l, generateCodeVerifier as A, generateCodeChallenge as B, PENDING_EXTENSION_READY as f, Logger as x, NOOP_STATE_CALLBACK as V, createExtensionStateNotInitialized as z, BACKEND_SERVER_NOT_REACHABLE as K, createExtensionStateNotFound as F, BodhiError as T, INITIAL_AUTH_STATE as G, BodhiApiError as Q, Chat as H, Models as W, Embeddings as J, Mcps as Y, pollAccessRequestUntilResolved as k, isAuthError as Z, BaseFacadeClient as j, refreshAccessToken as ee } from "@bodhiapp/bodhi-js-core";
1
+ import { DirectClientBase as $, createStoragePrefixWithNamespace as X, STORAGE_PREFIXES as N, AccessRequestBuilder as v, unwrapResponse as L, DEFAULT_POLL_TIMEOUT_MS as P, DEFAULT_POLL_INTERVAL_MS as b, createOperationError as l, generateCodeVerifier as A, generateCodeChallenge as B, PENDING_EXTENSION_READY as f, Logger as x, NOOP_STATE_CALLBACK as V, createExtensionStateNotInitialized as z, BACKEND_SERVER_NOT_REACHABLE as K, createExtensionStateNotFound as F, BodhiError as T, INITIAL_AUTH_STATE as G, BodhiApiError as Q, Chat as H, Models as W, Embeddings as J, Mcps as Y, pollAccessRequestUntilResolved as k, isAuthError as Z, BaseFacadeClient as j, refreshAccessToken as ee } from "@bodhiapp/bodhi-js-core";
2
2
  import { InMemoryStorage as Ce } from "@bodhiapp/bodhi-js-core";
3
3
  import { isExtError as I, isApiSuccessResponse as te, MESSAGE_TYPES as _, EXT_ACTIONS as re, BODHI_STREAM_PORT as se, isStreamChunk as oe, isStreamApiError as ne, isStreamError as ie, BODHI_STREAM_TEXT_PORT as ae } from "@bodhiapp/bodhi-browser-types";
4
4
  const c = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bodhiapp/bodhi-js-ext",
3
- "version": "0.0.37",
3
+ "version": "0.0.38",
4
4
  "description": "Extension SDK for Bodhi Browser - chrome.runtime communication",
5
5
  "type": "module",
6
6
  "main": "dist/bodhi-ext.cjs.js",
@@ -37,9 +37,9 @@
37
37
  "typecheck": "tsc --noEmit"
38
38
  },
39
39
  "dependencies": {
40
- "@bodhiapp/bodhi-browser-types": "0.0.37",
41
- "@bodhiapp/bodhi-js-core": "0.0.37",
42
- "@bodhiapp/ts-client": "0.1.31"
40
+ "@bodhiapp/bodhi-browser-types": "0.0.38",
41
+ "@bodhiapp/bodhi-js-core": "0.0.38",
42
+ "@bodhiapp/ts-client": "0.1.32"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@eslint/js": "^9.23.0",