@bodhiapp/bodhi-js-ext 0.0.3 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bodhi-browser-ext/src/types/bodhiext.d.ts +0 -1
- package/dist/bodhi-browser-ext/src/types/protocol.d.ts +0 -1
- package/dist/bodhi-ext.cjs.js +1 -1
- package/dist/bodhi-ext.esm.d.ts +1 -0
- package/dist/bodhi-ext.esm.js +312 -310
- package/dist/bodhi-js-sdk/core/src/build-info.d.ts +1 -0
- package/dist/bodhi-js-sdk/core/src/errors.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/facade-client-base.d.ts +1 -2
- package/dist/bodhi-js-sdk/core/src/index.d.ts +1 -0
- package/dist/bodhi-js-sdk/core/src/interface.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/logger.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/oauth.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/onboarding/config.d.ts +1 -2
- package/dist/bodhi-js-sdk/core/src/onboarding/modal.d.ts +1 -2
- package/dist/bodhi-js-sdk/core/src/onboarding/protocol-utils.d.ts +1 -2
- package/dist/bodhi-js-sdk/core/src/platform.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/storage.d.ts +12 -0
- package/dist/bodhi-js-sdk/core/src/types/api.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/callback.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/client-state.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/config.d.ts +0 -1
- package/dist/bodhi-js-sdk/core/src/types/platform.d.ts +1 -2
- package/dist/bodhi-js-sdk/ext/src/build-info.d.ts +1 -0
- package/dist/bodhi-js-sdk/ext/src/direct-client.d.ts +1 -1
- package/dist/bodhi-js-sdk/ext/src/ext2ext-client.d.ts +1 -2
- package/dist/bodhi-js-sdk/ext/src/facade-client.d.ts +2 -1
- package/dist/bodhi-js-sdk/ext/src/index.d.ts +1 -0
- package/dist/bodhi-js-sdk/ext/src/messages.d.ts +0 -1
- package/dist/setup-modal/src/types/message-types.d.ts +0 -1
- package/dist/setup-modal/src/types/protocol.d.ts +0 -1
- package/dist/setup-modal/src/types/state.d.ts +0 -1
- package/dist/setup-modal/src/types/type-guards.d.ts +0 -1
- package/package.json +6 -5
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { OpenAiApiError, PingResponse, CreateChatCompletionRequest, CreateChatCompletionResponse, CreateChatCompletionStreamResponse } from '@bodhiapp/ts-client';
|
|
2
|
-
|
|
3
2
|
/**
|
|
4
3
|
* HTTP response wrapper - body can be success type OR error type
|
|
5
4
|
* Use isApiErrorResponse() to narrow the type based on status
|
package/dist/bodhi-ext.cjs.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("@bodhiapp/bodhi-js-core"),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"},x="ext2ext-client-stream",S={LOGIN:"login",LOGOUT:"logout",GET_AUTH_STATE:"getAuthState",DISCOVER_EXTENSION:"discoverBodhiExtension",GET_EXTENSION_ID:"get_extension_id",SET_EXTENSION_ID:"setExtensionId"},w=5e3,N=3,O=500,X=500;function P(a){return"error"in a}class L extends h.DirectClientBase{constructor(e,t){super({...e,storagePrefix:h.STORAGE_PREFIXES.EXT},"DirectExtClient",t)}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;const t=await this.requestResourceAccess(),r=`openid profile email roles ${this.userScope} ${t}`,i=h.generateCodeVerifier(),s=await h.generateCodeChallenge(i),o=h.generateCodeVerifier();await chrome.storage.session.set({[this.storageKeys.CODE_VERIFIER]:i,[this.storageKeys.STATE]:o});const n=chrome.identity.getRedirectURL("callback"),E=new URL(this.authEndpoints.authorize);return E.searchParams.set("client_id",this.authClientId),E.searchParams.set("response_type","code"),E.searchParams.set("redirect_uri",n),E.searchParams.set("scope",r),E.searchParams.set("code_challenge",s),E.searchParams.set("code_challenge_method","S256"),E.searchParams.set("state",o),new Promise((d,u)=>{chrome.identity.launchWebAuthFlow({url:E.toString(),interactive:!0},async l=>{if(chrome.runtime.lastError){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(chrome.runtime.lastError);return}if(!l){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(h.createOperationError("No redirect URL received","oauth-error"));return}try{const T=new URL(l),p=T.searchParams.get("code"),g=T.searchParams.get("state"),R=(await chrome.storage.session.get(this.storageKeys.STATE))[this.storageKeys.STATE];if(g!==R){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(h.createOperationError("State mismatch - possible CSRF","oauth-error"));return}if(!p){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(h.createOperationError("No authorization code received","oauth-error"));return}await this.exchangeCodeForTokens(p);const C=await this.getAuthState();if(!C.isLoggedIn)throw h.createOperationError("Login failed","oauth-error");const A=C;this.setAuthState(A),await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),d(A)}catch(T){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(T)}})})}async logout(){const t=(await chrome.storage.session.get(this.storageKeys.REFRESH_TOKEN))[this.storageKeys.REFRESH_TOKEN];if(t)try{const i=new URLSearchParams({token:t,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:i})}catch(i){this.logger.warn("Token revocation failed:",i)}await chrome.storage.session.remove([this.storageKeys.ACCESS_TOKEN,this.storageKeys.REFRESH_TOKEN,this.storageKeys.EXPIRES_AT,this.storageKeys.RESOURCE_SCOPE]);const r={isLoggedIn:!1};return this.setAuthState(r),r}async requestResourceAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId},{},!1);if(h.isApiResultOperationError(e))throw new Error("Failed to get resource access scope from server");if(!h.isApiResultSuccess(e))throw new Error("Failed to get resource access scope from server: API error");const t=e.body.scope;return await chrome.storage.session.set({[this.storageKeys.RESOURCE_SCOPE]:t}),t}async exchangeCodeForTokens(e){const r=(await chrome.storage.session.get(this.storageKeys.CODE_VERIFIER))[this.storageKeys.CODE_VERIFIER],i=chrome.identity.getRedirectURL("callback"),s=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(!s.ok){const E=await s.text();throw new Error(`Token exchange failed: ${s.status} ${E}`)}const o=await s.json(),n=Date.now()+(o.expires_in||3600)*1e3;await chrome.storage.session.set({[this.storageKeys.ACCESS_TOKEN]:o.access_token,[this.storageKeys.REFRESH_TOKEN]:o.refresh_token,[this.storageKeys.EXPIRES_AT]:n}),await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE])}async _storageGet(e){const r=(await chrome.storage.session.get(e))[e];return r!==void 0?String(r):null}async _storageSet(e){await chrome.storage.session.set(e)}async _storageRemove(e){await chrome.storage.session.remove(e)}_getRedirectUri(){return chrome.identity.getRedirectURL("callback")}}function b(a){return a!==null&&typeof a=="object"}function k(a){return b(a)&&"message"in a&&typeof a.message=="string"&&"type"in a&&typeof a.type=="string"}const m={API_REQUEST:"BODHI_API_REQUEST",API_RESPONSE:"BODHI_API_RESPONSE",STREAM_REQUEST:"BODHI_STREAM_REQUEST",STREAM_CHUNK:"BODHI_STREAM_CHUNK",STREAM_ERROR:"BODHI_STREAM_ERROR",STREAM_API_ERROR:"BODHI_STREAM_API_ERROR",ERROR:"BODHI_ERROR",EXT_REQUEST:"BODHI_EXT_REQUEST",EXT_RESPONSE:"BODHI_EXT_RESPONSE"};function v(a){return a!==null&&typeof a=="object"&&typeof a.status=="number"&&a.status>=200&&a.status<300&&"body"in a}function U(a){return a!==null&&typeof a=="object"&&a.type===m.STREAM_CHUNK}function D(a){return a!==null&&typeof a=="object"&&a.type===m.STREAM_API_ERROR}function M(a){return a!==null&&typeof a=="object"&&a.type===m.STREAM_ERROR}function y(a){return a!==null&&typeof a=="object"&&"error"in a}function q(a){return a instanceof Error&&"error"in a&&!("response"in a)&&k(a.error)}const B={GET_EXTENSION_ID:"get_extension_id",TEST_CONNECTION:"test_connection"},$="BODHI_STREAM_PORT";class K{constructor(e={},t){this.state={type:"extension",extension:"not-initialized",server:h.PENDING_EXTENSION_READY},this.extensionId=null,this.broadcastListenerActive=!1,this.config=e,this.logger=new h.Logger("ExtClient",(e==null?void 0:e.logLevel)||"warn"),this.onStateChange=t??h.NOOP_STATE_CALLBACK}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==null?void 0: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={}){var i,s,o,n,E,d,u,l,T;if(!e.testConnection&&!e.selectedConnection){this.logger.info("No testConnection or selectedConnection, returning not-initialized state");const p=h.createExtensionStateNotInitialized();return this.setState(p),p}if(this.extensionId&&!e.testConnection)return this.logger.debug("Already initialized with extensionId, skipping discovery"),this.state;const t=e.timeoutMs??((s=(i=this.config.initParams)==null?void 0:i.extension)==null?void 0:s.timeoutMs)??w,r=(o=e.savedState)==null?void 0:o.extensionId;try{if(!this.extensionId){if(r)this.logger.info("Restoring with known extensionId:",r),await this.sendExtMessageWithTimeout(S.SET_EXTENSION_ID,{extensionId:r},t),this.extensionId=r;else{this.logger.info("Discovering bodhi-browser extension...");const _={attempts:(E=(n=this.config.initParams)==null?void 0:n.extension)==null?void 0:E.attempts,attemptWaitMs:(u=(d=this.config.initParams)==null?void 0:d.extension)==null?void 0:u.attemptWaitMs,attemptTimeout:(T=(l=this.config.initParams)==null?void 0:l.extension)==null?void 0:T.attemptTimeout},R=await this.sendExtMessageWithTimeout(S.DISCOVER_EXTENSION,_,t);this.extensionId=R.extensionId,this.logger.info("Extension discovered:",this.extensionId)}this.setupBroadcastListener()}const p={type:"extension",extension:"ready",extensionId:this.extensionId,server:h.PENDING_EXTENSION_READY};let g=h.PENDING_EXTENSION_READY;if(e.testConnection)try{g=await this.getServerState(),this.logger.info("Server connectivity tested, state:",g.status)}catch(_){this.logger.error("Failed to get server state:",_),g=h.BACKEND_SERVER_NOT_REACHABLE}return this.setState({...p,server:g}),this.state}catch(p){this.logger.error("Failed to initialize extension:",p),this.extensionId=null;const g=h.createExtensionStateNotFound();return this.setState(g),this.state}}async sendExtMessageWithTimeout(e,t,r=1e4){const i=new Promise((s,o)=>setTimeout(()=>o(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 h.createOperationError("No response from background script","extension_error");if(i.type!==c.EXT2EXT_CLIENT_RESPONSE)throw h.createOperationError("Invalid response type from background script","extension_error");const s=i.response;if(y(s)){const o=s.error.type||"extension_error";throw h.createOperationError(s.error.message,o)}return s}catch(r){throw q(r)?r:h.createOperationError(r instanceof Error?r.message:"Unknown error occurred","extension_error")}}async sendRawApiMessage(e,t,r,i,s){const o=this.generateRequestId();return await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_API_REQUEST,requestId:o,request:{method:e,endpoint:t,body:r,headers:i,authenticated:s}})}async sendApiRequest(e,t,r,i,s){const o=await this.sendRawApiMessage(e,t,r,i,s);if(P(o)){const n=o.error.type||"extension_error";return{error:{message:o.error.message,type:n}}}return o.response}async login(){return new Promise((e,t)=>{const r=async i=>{var s;if(i&&typeof i=="object"&&"type"in i&&i.type==="EXT2EXT_CLIENT_BROADCAST"&&"event"in i&&i.event==="authStateChanged"){chrome.runtime.onMessage.removeListener(r);try{const o=await this.getAuthState();if(h.isAuthError(o)){t(h.createOperationError(`Login failed: ${(s=o.error)==null?void 0:s.message}`,"auth-error"));return}if(h.isAuthLoggedOut(o)){t(h.createOperationError("Login failed: User is not logged in","auth-error"));return}this.setAuthState(o),e(o)}catch(o){t(o)}}};chrome.runtime.onMessage.addListener(r),this.sendExtRequest(S.LOGIN).catch(i=>{chrome.runtime.onMessage.removeListener(r),t(i)})})}async logout(){await this.sendExtRequest(S.LOGOUT);const e={isLoggedIn:!1};return this.setAuthState(e),e}async getAuthState(){return this.isClientInitialized()?(await this.sendExtRequest(S.GET_AUTH_STATE)).authState:h.AUTH_EXT_NOT_INITIALIZED}async pingApi(){return this.sendApiRequest("GET","/ping")}async fetchModels(){return this.sendApiRequest("GET","/v1/models",void 0,void 0,!0)}async getServerState(){const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(h.isApiResultOperationError(e))return{status:"not-reachable",error:e.error};if(!h.isApiResultSuccess(e))return{status:"not-reachable",error:{message:"API error from server",type:"extension_error"}};const t=e.body;switch(t.status){case"ready":return{status:"ready",version:t.version||"unknown"};case"setup":return{status:"setup",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Setup required",type:"extension_error"}};case"resource-admin":return{status:"resource-admin",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Resource admin required",type:"extension_error"}};case"error":return{status:"error",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Server error",type:"extension_error"}};default:return{status:"not-reachable",error:{message:"Unknown server status",type:"extension_error"}}}}async*stream(e,t,r,i,s=!0){const o=this.generateRequestId();console.log("[ExtClient] Starting stream",{method:e,endpoint:t,requestId:o});const n=chrome.runtime.connect({name:x}),d=new ReadableStream({start:u=>{n.onMessage.addListener(l=>{var T,p,g;if(l.requestId===o)switch(l.type){case c.EXT2EXT_CLIENT_STREAM_DONE:console.log("[ExtClient] Stream complete",{requestId:o}),u.close(),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_ERROR:console.error("[ExtClient] Stream error",{requestId:o,error:JSON.stringify(l.error)}),u.error(h.createOperationError(l.error.message,"extension_error")),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_API_ERROR:{const _=l;console.error("[ExtClient] Stream API error",{requestId:o,error:(T=_.response.body)==null?void 0:T.error}),u.error(h.createApiError(((g=(p=_.response.body)==null?void 0:p.error)==null?void 0:g.message)||"API error",_.response.status,_.response.body)),n.disconnect();break}case c.EXT2EXT_CLIENT_STREAM_CHUNK:{const _=l;v(_.response)&&u.enqueue(_.response.body);break}}}),n.onDisconnect.addListener(()=>{console.log("[ExtClient] Port disconnected",{requestId:o});try{u.error(h.createOperationError("Connection closed unexpectedly","extension_error"))}catch{}}),n.postMessage({type:c.EXT2EXT_CLIENT_STREAM_REQUEST,requestId:o,request:{method:e,endpoint:t,body:r,headers:i,authenticated:s}})}}).getReader();try{for(;;){const{done:u,value:l}=await d.read();if(u){console.log("[ExtClient] Stream iteration complete");break}yield l}}finally{d.releaseLock()}}async*streamChat(e,t,r=!0){yield*this.stream("POST","/v1/chat/completions",{model:e,messages:[{role:"user",content:t}],stream:!0},void 0,r)}serialize(){return{extensionId:this.extensionId??void 0}}async debug(){return{type:"ExtClient",state:this.state,authState:await this.getAuthState()}}}class V extends h.BaseFacadeClient{constructor(e,t,r,i){const s={authServerUrl:t.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:t.userScope||"scope_user_user",logLevel:t.logLevel||"warn",initParams:t.initParams};super(e,s,r,i)}createLogger(e){return new h.Logger("ExtUIClient",e.logLevel)}createExtClient(e,t){return new K({logLevel:e.logLevel,initParams:e.initParams},t)}createDirectClient(e,t,r){return new L({authClientId:e,authServerUrl:t.authServerUrl,userScope:t.userScope,logLevel:t.logLevel,storagePrefix:h.STORAGE_PREFIXES.EXT},r)}}const F=["bjdjhiombmfbcoeojijpfckljjghmjbf"],I=class I{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==null?void 0:t.authServerUrl)||"https://id.getbodhi.app/realms/bodhi",this.userScope=(t==null?void 0:t.userScope)||"scope_user_user",this.extensionId=t==null?void 0:t.extensionId,this.logger=new h.Logger("BodhiExtClient",(t==null?void 0:t.logLevel)||"warn"),this.attempts=(t==null?void 0:t.attempts)??N,this.attemptWaitMs=(t==null?void 0:t.attemptWaitMs)??O,this.attemptTimeout=(t==null?void 0: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),I.base64UrlEncode(e.buffer)}static async generateCodeChallenge(e){const r=new TextEncoder().encode(e),i=await crypto.subtle.digest("SHA-256",r);return I.base64UrlEncode(i)}getState(){return this.state}getExtensionIdsForEnvironment(){const t=F;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 s={type:m.EXT_REQUEST,requestId:crypto.randomUUID(),request:{action:"get_extension_id"}};this.logger.debug(`[Ext2Ext/Discovery] Sending message to ${e}:`,s),chrome.runtime.sendMessage(e,s,o=>{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}:`,o);const n=o;n&&n.type===m.EXT_RESPONSE?(this.logger.debug(`[Ext2Ext/Discovery] ✓ Extension ${e} responded`),t(!0)):(this.logger.error(`[Ext2Ext/Discovery] Invalid response from ${e}:`,o),r(new Error("Invalid response")))})}catch(s){this.logger.error(`[Ext2Ext/Discovery] Exception pinging ${e}:`,s),clearTimeout(i),r(s)}})}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 s=this.getExtensionIdsForEnvironment();this.logger.debug(`[Ext2Ext/Discovery] Will try ${s.length} extension(s):`,s);for(const E of s){for(let d=1;d<=t;d++){this.logger.debug(`[Ext2Ext/Discovery] Trying ${E} - attempt ${d}/${t}`);try{return await this.pingExtension(E),this.logger.info(`[Ext2Ext/Discovery] ✓ Found: ${E} on attempt ${d}`),{success:!0,extensionId:E}}catch(u){this.logger.debug(`[Ext2Ext/Discovery] Attempt ${d} failed for ${E}: ${u instanceof Error?u.message:"Unknown error"}`),d<t&&await this.sleep(r)}}this.logger.warn(`[Ext2Ext/Discovery] ✗ Not found: ${E} after ${t} attempts`)}const o=s.join(", "),n=`Extension not found. Tried ${s.length} IDs with ${t} attempts each: ${o}`;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===S.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)),chrome.runtime.onConnect.addListener(e=>{if(e.name!==x){this.logger.debug("[BodhiExtClient] Ignoring port with name:",e.name);return}this.logger.info("[BodhiExtClient] Streaming port connected"),e.onMessage.addListener(async t=>{if(t.type!==c.EXT2EXT_CLIENT_STREAM_REQUEST){this.logger.warn("[BodhiExtClient] Unknown stream message type:",t.type),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:t.requestId,error:{message:"Unknown stream message type",type:"extension_error"}});return}await this.handleStreamRequest(e,t)}),e.onDisconnect.addListener(()=>{this.logger.info("[BodhiExtClient] Streaming port disconnected")})}),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==null?void 0:e.attempts)??this.attempts,attemptWaitMs:(e==null?void 0:e.attemptWaitMs)??this.attemptWaitMs,attemptTimeout:(e==null?void 0: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 s=await this._getAccessTokenRaw();if(!s)return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"auth_error"}};r={...r,Authorization:`Bearer ${s}`},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:s}=r;this.logger.debug(`[BodhiExtClient] Handling action: ${i}`);try{let o={};switch(i){case S.DISCOVER_EXTENSION:{const n=s;await this.init(n);const E="production";this.logger.info("[BodhiExtClient] Discovery successful:",{extensionId:this.extensionId,environment:E}),o={extensionId:this.extensionId,environment:E};break}case S.SET_EXTENSION_ID:{const{extensionId:n}=s;this.extensionId=n,this.state="ready",this.logger.info("[BodhiExtClient] Extension ID set:",{extensionId:n}),o={success:!0};break}case S.GET_EXTENSION_ID:{const n=await this.sendExtRequestRaw(B.GET_EXTENSION_ID,s);return y(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 S.LOGIN:await this.login(),this.broadcastAuthStateChange();break;case S.LOGOUT:await this.logout(),this.broadcastAuthStateChange();break;case S.GET_AUTH_STATE:o={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:o}}catch(o){return this.logger.error("[BodhiExtClient] Unexpected error:",o),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:o instanceof Error?o.message:`Unexpected error: ${JSON.stringify(o)}`}}}}}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(){if(!(this.isAuthenticating||(await this.getAuthState()).isLoggedIn)){this.isAuthenticating=!0;try{if(!this.extensionId)throw new Error("Extension not discovered. Please detect Bodhi extension before login.");const t=await this.requestAccess(),r=`openid profile email roles ${this.userScope} ${t}`,i=I.generateCodeVerifier(),s=await I.generateCodeChallenge(i),o=I.generateCodeVerifier();await chrome.storage.session.set({codeVerifier:i,state:o,authInProgress:!0});const n=chrome.identity.getRedirectURL("callback"),E=new URL(this.authEndpoints.authorize);return E.searchParams.set("client_id",this.authClientId),E.searchParams.set("response_type","code"),E.searchParams.set("redirect_uri",n),E.searchParams.set("scope",r),E.searchParams.set("code_challenge",s),E.searchParams.set("code_challenge_method","S256"),E.searchParams.set("state",o),new Promise((d,u)=>{chrome.identity.launchWebAuthFlow({url:E.toString(),interactive:!0},async l=>{if(await chrome.storage.session.set({authInProgress:!1}),chrome.runtime.lastError){await chrome.storage.session.remove(["codeVerifier","state"]),u(chrome.runtime.lastError);return}if(!l){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("No redirect URL received"));return}try{const T=new URL(l),p=T.searchParams.get("code"),g=T.searchParams.get("state"),{state:_}=await chrome.storage.session.get("state");if(g!==_){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("State mismatch - possible CSRF"));return}if(!p){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("No authorization code received"));return}await this.exchangeCodeForTokens(p),await chrome.storage.session.remove(["codeVerifier","state"]),d()}catch(T){await chrome.storage.session.remove(["codeVerifier","state"]),u(T)}})})}finally{this.isAuthenticating=!1}}}async exchangeCodeForTokens(e){if((await this.getAuthState()).isLoggedIn)return;const{codeVerifier:r}=await chrome.storage.session.get("codeVerifier"),i=chrome.identity.getRedirectURL("callback"),s=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(!s.ok){const n=await s.text();throw new Error(`Token exchange failed: ${s.status} ${n}`)}const o=await s.json();await this.storeTokens({accessToken:o.access_token,refreshToken:o.refresh_token,idToken:o.id_token,expiresIn:o.expires_in})}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{isLoggedIn:!1};try{const t=this.parseJwt(e);return{isLoggedIn:!0,userInfo:{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}}catch(t){return this.logger.error("Failed to parse token:",t),{isLoggedIn:!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 sendExtRequest(e,t){const r=await this.sendExtRequestRaw(e,t);if(y(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 Error(this.createErrorClientNotInitialized({type:"api",method:e,endpoint:t}));this.logger.debug(`[BodhiExtClient] Sending API_REQUEST: method=${e}, endpoint=${t}`,r?{body:r}:"");const s=crypto.randomUUID(),o={type:m.API_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:i}};return this.logger.debug(`[BodhiExtClient] Request ID: ${s}, Extension: ${this.extensionId}`),new Promise((n,E)=>{try{chrome.runtime.sendMessage(this.extensionId,o,d=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${s}:`,chrome.runtime.lastError),E(new Error(chrome.runtime.lastError.message));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${s}:`,d),!d){this.logger.error(`[BodhiExtClient] No response received for request ${s}`),E(new Error("No response from extension"));return}d.type===m.API_RESPONSE&&d.requestId===s?"error"in d?(this.logger.error(`[BodhiExtClient] API error for ${s}:`,d.error),E(new Error(d.error.message))):(this.logger.debug(`[BodhiExtClient] ✓ Valid API_RESPONSE for ${s}`),n(d.response)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${s}:`,d),E(new Error("Invalid response format")))})}catch(d){this.logger.error(`[BodhiExtClient] Exception sending message for ${s}:`,d),E(d)}})}async sendExtRequestRaw(e,t){if(!this.extensionId)throw new Error(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:m.EXT_REQUEST,requestId:r,request:{action:e,params:t}};return this.logger.debug(`[BodhiExtClient] Request ID: ${r}, Extension: ${this.extensionId}`),new Promise((s,o)=>{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),o(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}`),o(new Error("No response from extension"));return}n.type===m.EXT_RESPONSE&&n.requestId===r?(this.logger.debug(`[BodhiExtClient] ✓ Valid EXT_RESPONSE for ${r}`),s(n)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${r}:`,n),o(new Error("Invalid response format")))})}catch(n){this.logger.error(`[BodhiExtClient] Exception sending message for ${r}:`,n),o(n)}})}async handleStreamRequest(e,t){const{requestId:r,request:i}=t,{method:s,endpoint:o,body:n,headers:E,authenticated:d}=i;this.logger.debug("[BodhiExtClient] Processing stream request:",{requestId:r,method:s,endpoint:o,authenticated:d}),this.extensionId||e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:this.createErrorClientNotInitialized(t),type:"extension_error"}});try{let u={...E};if(d!==!1){const g=await this._getAccessTokenRaw();if(!g){e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Not authenticated. Please log in first.",type:"extension_error"}});return}u={...u,Authorization:`Bearer ${g}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const l=chrome.runtime.connect(this.extensionId,{name:$});this.activeStreamPorts.set(r,l);const T=setTimeout(()=>{this.activeStreamPorts.has(r)&&(this.logger.error(`[BodhiExtClient] Stream timeout for ${r}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Stream request timed out",type:"timeout_error"}}),this.cleanupStreamPort(r))},I.STREAM_TIMEOUT);l.onMessage.addListener(g=>{if(U(g)){const _=g.response,R=_.body;_.status>=400?e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:r,response:_}):R!=null&&R.done?(e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_DONE,requestId:r}),this.logger.info(`[BodhiExtClient] Stream complete for ${r}`),clearTimeout(T),this.cleanupStreamPort(r)):e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_CHUNK,requestId:r,response:_})}else D(g)?(this.logger.error(`[BodhiExtClient] Stream API error for ${r}: ${g.response.status}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:r,response:g.response})):M(g)&&(this.logger.error(`[BodhiExtClient] Stream error for ${r}:`,g.error.message),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:`stream error: ${JSON.stringify(g)}`,type:"extension_error"}}),clearTimeout(T),this.cleanupStreamPort(r))}),l.onDisconnect.addListener(()=>{clearTimeout(T),this.activeStreamPorts.has(r)&&(this.logger.error(`[BodhiExtClient] Bodhi port disconnected for ${r}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Connection to Bodhi extension closed unexpectedly",type:"network_error"}}),this.activeStreamPorts.delete(r))});const p={type:m.STREAM_REQUEST,requestId:r,request:{method:s,endpoint:o,body:n,headers:u}};this.logger.debug("[BodhiExtClient] Sending stream request to bodhi port:",p),l.postMessage(p)}catch(u){const l=u;this.logger.error("[BodhiExtClient] Stream error:",JSON.stringify(l.message)),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:`uncaught error: ${JSON.stringify({error:l,message:l.message})}`,type:"extension_error"}})}}cleanupStreamPort(e){const t=this.activeStreamPorts.get(e);if(t){try{t.disconnect()}catch{}this.activeStreamPorts.delete(e)}}async requestAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId});if(!v(e))throw this.logger.error("[BodhiExtClient] Failed to get resource access scope: API error"),new Error("Failed to get resource access scope: API error");return e.body.scope}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 h.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t)return await this._storeRefreshedTokens(t),this.logger.info("Token refreshed successfully"),this.broadcastAuthStateChange(),t.access_token}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),h.createOperationError("Access token expired and unable to refresh. Try logging out and logging in again.","token_refresh_failed")}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(s=>"%"+("00"+s.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)}`}};I.STREAM_TIMEOUT=12e4;let f=I;exports.BodhiExtClient=f;exports.DISCOVERY_ATTEMPTS=N;exports.DISCOVERY_ATTEMPT_TIMEOUT=X;exports.DISCOVERY_ATTEMPT_WAIT_MS=O;exports.DISCOVERY_TIMEOUT_MS=w;exports.EXT2EXT_CLIENT_ACTIONS=S;exports.EXT2EXT_CLIENT_MESSAGE_TYPES=c;exports.EXT2EXT_CLIENT_STREAM_PORT=x;exports.ExtUIClient=V;exports.isExtClientApiError=P;
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("@bodhiapp/bodhi-js-core"),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"},x="ext2ext-client-stream",_={LOGIN:"login",LOGOUT:"logout",GET_AUTH_STATE:"getAuthState",DISCOVER_EXTENSION:"discoverBodhiExtension",GET_EXTENSION_ID:"get_extension_id",SET_EXTENSION_ID:"setExtensionId"},w=5e3,N=3,O=500,P=500;function X(a){return"error"in a}class b extends E.DirectClientBase{constructor(e,t){const r=e.basePath||"/",o=E.createStoragePrefixWithBasePath(r,E.STORAGE_PREFIXES.EXT);super({...e,storagePrefix:o},"DirectExtClient",t)}async login(){const e=await this.getAuthState();if(e.isLoggedIn)return e;const t=await this.requestResourceAccess(),r=`openid profile email roles ${this.userScope} ${t}`,o=E.generateCodeVerifier(),s=await E.generateCodeChallenge(o),i=E.generateCodeVerifier();await chrome.storage.session.set({[this.storageKeys.CODE_VERIFIER]:o,[this.storageKeys.STATE]:i});const n=chrome.identity.getRedirectURL("callback"),h=new URL(this.authEndpoints.authorize);return h.searchParams.set("client_id",this.authClientId),h.searchParams.set("response_type","code"),h.searchParams.set("redirect_uri",n),h.searchParams.set("scope",r),h.searchParams.set("code_challenge",s),h.searchParams.set("code_challenge_method","S256"),h.searchParams.set("state",i),new Promise((d,u)=>{chrome.identity.launchWebAuthFlow({url:h.toString(),interactive:!0},async g=>{if(chrome.runtime.lastError){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(chrome.runtime.lastError);return}if(!g){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(E.createOperationError("No redirect URL received","oauth-error"));return}try{const l=new URL(g),m=l.searchParams.get("code"),T=l.searchParams.get("state"),R=(await chrome.storage.session.get(this.storageKeys.STATE))[this.storageKeys.STATE];if(T!==R){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(E.createOperationError("State mismatch - possible CSRF","oauth-error"));return}if(!m){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(E.createOperationError("No authorization code received","oauth-error"));return}await this.exchangeCodeForTokens(m);const C=await this.getAuthState();if(!C.isLoggedIn)throw E.createOperationError("Login failed","oauth-error");const A=C;this.setAuthState(A),await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),d(A)}catch(l){await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE]),u(l)}})})}async logout(){const t=(await chrome.storage.session.get(this.storageKeys.REFRESH_TOKEN))[this.storageKeys.REFRESH_TOKEN];if(t)try{const o=new URLSearchParams({token:t,client_id:this.authClientId,token_type_hint:"refresh_token"});await fetch(this.authEndpoints.revoke,{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:o})}catch(o){this.logger.warn("Token revocation failed:",o)}await chrome.storage.session.remove([this.storageKeys.ACCESS_TOKEN,this.storageKeys.REFRESH_TOKEN,this.storageKeys.EXPIRES_AT,this.storageKeys.RESOURCE_SCOPE]);const r={isLoggedIn:!1};return this.setAuthState(r),r}async requestResourceAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId},{},!1);if(E.isApiResultOperationError(e))throw new Error("Failed to get resource access scope from server");if(!E.isApiResultSuccess(e))throw new Error("Failed to get resource access scope from server: API error");const t=e.body.scope;return await chrome.storage.session.set({[this.storageKeys.RESOURCE_SCOPE]:t}),t}async exchangeCodeForTokens(e){const r=(await chrome.storage.session.get(this.storageKeys.CODE_VERIFIER))[this.storageKeys.CODE_VERIFIER],o=chrome.identity.getRedirectURL("callback"),s=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:o,client_id:this.authClientId,code_verifier:r})});if(!s.ok){const h=await s.text();throw new Error(`Token exchange failed: ${s.status} ${h}`)}const i=await s.json(),n=Date.now()+(i.expires_in||3600)*1e3;await chrome.storage.session.set({[this.storageKeys.ACCESS_TOKEN]:i.access_token,[this.storageKeys.REFRESH_TOKEN]:i.refresh_token,[this.storageKeys.EXPIRES_AT]:n}),await chrome.storage.session.remove([this.storageKeys.CODE_VERIFIER,this.storageKeys.STATE])}async _storageGet(e){const r=(await chrome.storage.session.get(e))[e];return r!==void 0?String(r):null}async _storageSet(e){await chrome.storage.session.set(e)}async _storageRemove(e){await chrome.storage.session.remove(e)}_getRedirectUri(){return chrome.identity.getRedirectURL("callback")}}function L(a){return a!==null&&typeof a=="object"}function k(a){return L(a)&&"message"in a&&typeof a.message=="string"&&"type"in a&&typeof a.type=="string"}const p={API_REQUEST:"BODHI_API_REQUEST",API_RESPONSE:"BODHI_API_RESPONSE",STREAM_REQUEST:"BODHI_STREAM_REQUEST",STREAM_CHUNK:"BODHI_STREAM_CHUNK",STREAM_ERROR:"BODHI_STREAM_ERROR",STREAM_API_ERROR:"BODHI_STREAM_API_ERROR",ERROR:"BODHI_ERROR",EXT_REQUEST:"BODHI_EXT_REQUEST",EXT_RESPONSE:"BODHI_EXT_RESPONSE"};function v(a){return a!==null&&typeof a=="object"&&typeof a.status=="number"&&a.status>=200&&a.status<300&&"body"in a}function D(a){return a!==null&&typeof a=="object"&&a.type===p.STREAM_CHUNK}function U(a){return a!==null&&typeof a=="object"&&a.type===p.STREAM_API_ERROR}function M(a){return a!==null&&typeof a=="object"&&a.type===p.STREAM_ERROR}function y(a){return a!==null&&typeof a=="object"&&"error"in a}function q(a){return a instanceof Error&&"error"in a&&!("response"in a)&&k(a.error)}const B={GET_EXTENSION_ID:"get_extension_id",TEST_CONNECTION:"test_connection"},$="BODHI_STREAM_PORT";class K{constructor(e={},t){this.state={type:"extension",extension:"not-initialized",server:E.PENDING_EXTENSION_READY},this.extensionId=null,this.broadcastListenerActive=!1,this.config=e,this.logger=new E.Logger("ExtClient",e?.logLevel||"warn"),this.onStateChange=t??E.NOOP_STATE_CALLBACK}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 o=E.createExtensionStateNotInitialized();return this.setState(o),o}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??w,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 i={attempts:this.config.initParams?.extension?.attempts,attemptWaitMs:this.config.initParams?.extension?.attemptWaitMs,attemptTimeout:this.config.initParams?.extension?.attemptTimeout},n=await this.sendExtMessageWithTimeout(_.DISCOVER_EXTENSION,i,t);this.extensionId=n.extensionId,this.logger.info("Extension discovered:",this.extensionId)}this.setupBroadcastListener()}const o={type:"extension",extension:"ready",extensionId:this.extensionId,server:E.PENDING_EXTENSION_READY};let s=E.PENDING_EXTENSION_READY;if(e.testConnection)try{s=await this.getServerState(),this.logger.info("Server connectivity tested, state:",s.status)}catch(i){this.logger.error("Failed to get server state:",i),s=E.BACKEND_SERVER_NOT_REACHABLE}return this.setState({...o,server:s}),this.state}catch(o){this.logger.error("Failed to initialize extension:",o),this.extensionId=null;const s=E.createExtensionStateNotFound();return this.setState(s),this.state}}async sendExtMessageWithTimeout(e,t,r=1e4){const o=new Promise((s,i)=>setTimeout(()=>i(new Error("Timeout")),r));return Promise.race([this.sendExtRequest(e,t),o])}async sendExtRequest(e,t){try{const r=this.generateRequestId(),o=await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_REQUEST,requestId:r,request:{action:e,params:t}});if(!o)throw E.createOperationError("No response from background script","extension_error");if(o.type!==c.EXT2EXT_CLIENT_RESPONSE)throw E.createOperationError("Invalid response type from background script","extension_error");const s=o.response;if(y(s)){const i=s.error.type||"extension_error";throw E.createOperationError(s.error.message,i)}return s}catch(r){throw q(r)?r:E.createOperationError(r instanceof Error?r.message:"Unknown error occurred","extension_error")}}async sendRawApiMessage(e,t,r,o,s){const i=this.generateRequestId();return await chrome.runtime.sendMessage({type:c.EXT2EXT_CLIENT_API_REQUEST,requestId:i,request:{method:e,endpoint:t,body:r,headers:o,authenticated:s}})}async sendApiRequest(e,t,r,o,s){const i=await this.sendRawApiMessage(e,t,r,o,s);if(X(i)){const n=i.error.type||"extension_error";return{error:{message:i.error.message,type:n}}}return i.response}async login(){return new Promise((e,t)=>{const r=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(r);try{const s=await this.getAuthState();if(E.isAuthError(s)){t(E.createOperationError(`Login failed: ${s.error?.message}`,"auth-error"));return}if(E.isAuthLoggedOut(s)){t(E.createOperationError("Login failed: User is not logged in","auth-error"));return}this.setAuthState(s),e(s)}catch(s){t(s)}}};chrome.runtime.onMessage.addListener(r),this.sendExtRequest(_.LOGIN).catch(o=>{chrome.runtime.onMessage.removeListener(r),t(o)})})}async logout(){await this.sendExtRequest(_.LOGOUT);const e={isLoggedIn:!1};return this.setAuthState(e),e}async getAuthState(){return this.isClientInitialized()?(await this.sendExtRequest(_.GET_AUTH_STATE)).authState:E.AUTH_EXT_NOT_INITIALIZED}async pingApi(){return this.sendApiRequest("GET","/ping")}async fetchModels(){return this.sendApiRequest("GET","/v1/models",void 0,void 0,!0)}async getServerState(){const e=await this.sendApiRequest("GET","/bodhi/v1/info");if(E.isApiResultOperationError(e))return{status:"not-reachable",error:e.error};if(!E.isApiResultSuccess(e))return{status:"not-reachable",error:{message:"API error from server",type:"extension_error"}};const t=e.body;switch(t.status){case"ready":return{status:"ready",version:t.version||"unknown"};case"setup":return{status:"setup",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Setup required",type:"extension_error"}};case"resource-admin":return{status:"resource-admin",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Resource admin required",type:"extension_error"}};case"error":return{status:"error",version:t.version||"unknown",error:t.error?{message:t.error.message,type:t.error.type}:{message:"Server error",type:"extension_error"}};default:return{status:"not-reachable",error:{message:"Unknown server status",type:"extension_error"}}}}async*stream(e,t,r,o,s=!0){const i=this.generateRequestId();this.logger.debug("Starting stream",{method:e,endpoint:t,requestId:i});const n=chrome.runtime.connect({name:x}),d=new ReadableStream({start:u=>{n.onMessage.addListener(g=>{if(g.requestId===i)switch(g.type){case c.EXT2EXT_CLIENT_STREAM_DONE:this.logger.debug("Stream complete",{requestId:i}),u.close(),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_ERROR:this.logger.error("Stream error",{requestId:i,error:JSON.stringify(g.error)}),u.error(E.createOperationError(g.error.message,"extension_error")),n.disconnect();break;case c.EXT2EXT_CLIENT_STREAM_API_ERROR:{const l=g;this.logger.error("Stream API error",{requestId:i,error:l.response.body?.error}),u.error(E.createApiError(l.response.body?.error?.message||"API error",l.response.status,l.response.body)),n.disconnect();break}case c.EXT2EXT_CLIENT_STREAM_CHUNK:{const l=g;v(l.response)&&u.enqueue(l.response.body);break}}}),n.onDisconnect.addListener(()=>{this.logger.debug("Port disconnected",{requestId:i});try{u.error(E.createOperationError("Connection closed unexpectedly","extension_error"))}catch{}}),n.postMessage({type:c.EXT2EXT_CLIENT_STREAM_REQUEST,requestId:i,request:{method:e,endpoint:t,body:r,headers:o,authenticated:s}})}}).getReader();try{for(;;){const{done:u,value:g}=await d.read();if(u){this.logger.debug("Stream iteration complete");break}yield g}}finally{d.releaseLock()}}async*streamChat(e,t,r=!0){yield*this.stream("POST","/v1/chat/completions",{model:e,messages:[{role:"user",content:t}],stream:!0},void 0,r)}serialize(){return{extensionId:this.extensionId??void 0}}async debug(){return{type:"ExtClient",state:this.state,authState:await this.getAuthState()}}}class V extends E.BaseFacadeClient{constructor(e,t,r,o){const s={authServerUrl:t.authServerUrl||"https://id.getbodhi.app/realms/bodhi",userScope:t.userScope||"scope_user_user",basePath:t.basePath||"/",logLevel:t.logLevel||"warn",initParams:t.initParams};super(e,s,r,o,t.basePath)}createLogger(e){return new E.Logger("ExtUIClient",e.logLevel)}createExtClient(e,t){return new K({logLevel:e.logLevel,initParams:e.initParams},t)}createDirectClient(e,t,r){return new b({authClientId:e,authServerUrl:t.authServerUrl,userScope:t.userScope,logLevel:t.logLevel,storagePrefix:E.STORAGE_PREFIXES.EXT,basePath:t.basePath},r)}}const F=["bjdjhiombmfbcoeojijpfckljjghmjbf"],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.userScope=t?.userScope||"scope_user_user",this.extensionId=t?.extensionId,this.logger=new E.Logger("BodhiExtClient",t?.logLevel||"warn"),this.attempts=t?.attempts??N,this.attemptWaitMs=t?.attemptWaitMs??O,this.attemptTimeout=t?.attemptTimeout??P,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),o=await crypto.subtle.digest("SHA-256",r);return S.base64UrlEncode(o)}getState(){return this.state}getExtensionIdsForEnvironment(){const t=F;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 o=setTimeout(()=>{this.logger.debug(`[Ext2Ext/Discovery] Timeout waiting for extension ${e}`),r(new Error("Timeout"))},this.attemptTimeout);try{const s={type:p.EXT_REQUEST,requestId:crypto.randomUUID(),request:{action:"get_extension_id"}};this.logger.debug(`[Ext2Ext/Discovery] Sending message to ${e}:`,s),chrome.runtime.sendMessage(e,s,i=>{if(clearTimeout(o),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}:`,i);const n=i;n&&n.type===p.EXT_RESPONSE?(this.logger.debug(`[Ext2Ext/Discovery] ✓ Extension ${e} responded`),t(!0)):(this.logger.error(`[Ext2Ext/Discovery] Invalid response from ${e}:`,i),r(new Error("Invalid response")))})}catch(s){this.logger.error(`[Ext2Ext/Discovery] Exception pinging ${e}:`,s),clearTimeout(o),r(s)}})}sleep(e){return new Promise(t=>setTimeout(t,e))}async discoverBodhiExtension(e){const{attempts:t,attemptWaitMs:r,attemptTimeout:o}=e;this.logger.info(`[Ext2Ext/Discovery] Starting discovery: ${t} attempts per ID, ${o}ms timeout, ${r}ms between attempts`);const s=this.getExtensionIdsForEnvironment();this.logger.debug(`[Ext2Ext/Discovery] Will try ${s.length} extension(s):`,s);for(const h of s){for(let d=1;d<=t;d++){this.logger.debug(`[Ext2Ext/Discovery] Trying ${h} - attempt ${d}/${t}`);try{return await this.pingExtension(h),this.logger.info(`[Ext2Ext/Discovery] ✓ Found: ${h} on attempt ${d}`),{success:!0,extensionId:h}}catch(u){this.logger.debug(`[Ext2Ext/Discovery] Attempt ${d} failed for ${h}: ${u instanceof Error?u.message:"Unknown error"}`),d<t&&await this.sleep(r)}}this.logger.warn(`[Ext2Ext/Discovery] ✗ Not found: ${h} after ${t} attempts`)}const i=s.join(", "),n=`Extension not found. Tried ${s.length} IDs with ${t} attempts each: ${i}`;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 o=await this.handleAction(e);r(o)})(),!0)),chrome.runtime.onConnect.addListener(e=>{if(e.name!==x){this.logger.debug("[BodhiExtClient] Ignoring port with name:",e.name);return}this.logger.info("[BodhiExtClient] Streaming port connected"),e.onMessage.addListener(async t=>{if(t.type!==c.EXT2EXT_CLIENT_STREAM_REQUEST){this.logger.warn("[BodhiExtClient] Unknown stream message type:",t.type),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:t.requestId,error:{message:"Unknown stream message type",type:"extension_error"}});return}await this.handleStreamRequest(e,t)}),e.onDisconnect.addListener(()=>{this.logger.info("[BodhiExtClient] Streaming port disconnected")})}),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 s=await this._getAccessTokenRaw();if(!s)return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,error:{message:"Not authenticated. Please log in first.",type:"auth_error"}};r={...r,Authorization:`Bearer ${s}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const o=await this.sendApiRequest(e.request.method,e.request.endpoint,e.request.body,r);return{type:c.EXT2EXT_CLIENT_API_RESPONSE,requestId:t,response:o}}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:o,params:s}=r;this.logger.debug(`[BodhiExtClient] Handling action: ${o}`);try{let i={};switch(o){case _.DISCOVER_EXTENSION:{const n=s;await this.init(n);const h="production";this.logger.info("[BodhiExtClient] Discovery successful:",{extensionId:this.extensionId,environment:h}),i={extensionId:this.extensionId,environment:h};break}case _.SET_EXTENSION_ID:{const{extensionId:n}=s;this.extensionId=n,this.state="ready",this.logger.info("[BodhiExtClient] Extension ID set:",{extensionId:n}),i={success:!0};break}case _.GET_EXTENSION_ID:{const n=await this.sendExtRequestRaw(B.GET_EXTENSION_ID,s);return y(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:await this.login(),this.broadcastAuthStateChange();break;case _.LOGOUT:await this.logout(),this.broadcastAuthStateChange();break;case _.GET_AUTH_STATE:i={authState:await this.getAuthState()};break;default:return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:`Unknown action: ${o}`,type:"UNKNOWN_ACTION"}}}}return{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:i}}catch(i){return this.logger.error("[BodhiExtClient] Unexpected error:",i),{type:c.EXT2EXT_CLIENT_RESPONSE,requestId:t,response:{error:{message:i instanceof Error?i.message:`Unexpected error: ${JSON.stringify(i)}`}}}}}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(){if(!(this.isAuthenticating||(await this.getAuthState()).isLoggedIn)){this.isAuthenticating=!0;try{if(!this.extensionId)throw new Error("Extension not discovered. Please detect Bodhi extension before login.");const t=await this.requestAccess(),r=`openid profile email roles ${this.userScope} ${t}`,o=S.generateCodeVerifier(),s=await S.generateCodeChallenge(o),i=S.generateCodeVerifier();await chrome.storage.session.set({codeVerifier:o,state:i,authInProgress:!0});const n=chrome.identity.getRedirectURL("callback"),h=new URL(this.authEndpoints.authorize);return h.searchParams.set("client_id",this.authClientId),h.searchParams.set("response_type","code"),h.searchParams.set("redirect_uri",n),h.searchParams.set("scope",r),h.searchParams.set("code_challenge",s),h.searchParams.set("code_challenge_method","S256"),h.searchParams.set("state",i),new Promise((d,u)=>{chrome.identity.launchWebAuthFlow({url:h.toString(),interactive:!0},async g=>{if(await chrome.storage.session.set({authInProgress:!1}),chrome.runtime.lastError){await chrome.storage.session.remove(["codeVerifier","state"]),u(chrome.runtime.lastError);return}if(!g){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("No redirect URL received"));return}try{const l=new URL(g),m=l.searchParams.get("code"),T=l.searchParams.get("state"),{state:I}=await chrome.storage.session.get("state");if(T!==I){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("State mismatch - possible CSRF"));return}if(!m){await chrome.storage.session.remove(["codeVerifier","state"]),u(new Error("No authorization code received"));return}await this.exchangeCodeForTokens(m),await chrome.storage.session.remove(["codeVerifier","state"]),d()}catch(l){await chrome.storage.session.remove(["codeVerifier","state"]),u(l)}})})}finally{this.isAuthenticating=!1}}}async exchangeCodeForTokens(e){if((await this.getAuthState()).isLoggedIn)return;const{codeVerifier:r}=await chrome.storage.session.get("codeVerifier"),o=chrome.identity.getRedirectURL("callback"),s=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:o,client_id:this.authClientId,code_verifier:r})});if(!s.ok){const n=await s.text();throw new Error(`Token exchange failed: ${s.status} ${n}`)}const i=await s.json();await this.storeTokens({accessToken:i.access_token,refreshToken:i.refresh_token,idToken:i.id_token,expiresIn:i.expires_in})}async getAuthState(){const e=await this._getAccessTokenRaw();if(!e)return{isLoggedIn:!1};try{const t=this.parseJwt(e);return{isLoggedIn:!0,userInfo:{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}}catch(t){return this.logger.error("Failed to parse token:",t),{isLoggedIn:!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 sendExtRequest(e,t){const r=await this.sendExtRequestRaw(e,t);if(y(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,o){if(!this.extensionId)throw new Error(this.createErrorClientNotInitialized({type:"api",method:e,endpoint:t}));this.logger.debug(`[BodhiExtClient] Sending API_REQUEST: method=${e}, endpoint=${t}`,r?{body:r}:"");const s=crypto.randomUUID(),i={type:p.API_REQUEST,requestId:s,request:{method:e,endpoint:t,body:r,headers:o}};return this.logger.debug(`[BodhiExtClient] Request ID: ${s}, Extension: ${this.extensionId}`),new Promise((n,h)=>{try{chrome.runtime.sendMessage(this.extensionId,i,d=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${s}:`,chrome.runtime.lastError),h(new Error(chrome.runtime.lastError.message));return}if(this.logger.debug(`[BodhiExtClient] Response for request ${s}:`,d),!d){this.logger.error(`[BodhiExtClient] No response received for request ${s}`),h(new Error("No response from extension"));return}d.type===p.API_RESPONSE&&d.requestId===s?"error"in d?(this.logger.error(`[BodhiExtClient] API error for ${s}:`,d.error),h(new Error(d.error.message))):(this.logger.debug(`[BodhiExtClient] ✓ Valid API_RESPONSE for ${s}`),n(d.response)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${s}:`,d),h(new Error("Invalid response format")))})}catch(d){this.logger.error(`[BodhiExtClient] Exception sending message for ${s}:`,d),h(d)}})}async sendExtRequestRaw(e,t){if(!this.extensionId)throw new Error(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(),o={type:p.EXT_REQUEST,requestId:r,request:{action:e,params:t}};return this.logger.debug(`[BodhiExtClient] Request ID: ${r}, Extension: ${this.extensionId}`),new Promise((s,i)=>{try{chrome.runtime.sendMessage(this.extensionId,o,n=>{if(chrome.runtime.lastError){this.logger.error(`[BodhiExtClient] Chrome runtime error for request ${r}:`,chrome.runtime.lastError),i(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}`),i(new Error("No response from extension"));return}n.type===p.EXT_RESPONSE&&n.requestId===r?(this.logger.debug(`[BodhiExtClient] ✓ Valid EXT_RESPONSE for ${r}`),s(n)):(this.logger.error(`[BodhiExtClient] Invalid response format for ${r}:`,n),i(new Error("Invalid response format")))})}catch(n){this.logger.error(`[BodhiExtClient] Exception sending message for ${r}:`,n),i(n)}})}async handleStreamRequest(e,t){const{requestId:r,request:o}=t,{method:s,endpoint:i,body:n,headers:h,authenticated:d}=o;this.logger.debug("[BodhiExtClient] Processing stream request:",{requestId:r,method:s,endpoint:i,authenticated:d}),this.extensionId||e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:this.createErrorClientNotInitialized(t),type:"extension_error"}});try{let u={...h};if(d!==!1){const T=await this._getAccessTokenRaw();if(!T){e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Not authenticated. Please log in first.",type:"extension_error"}});return}u={...u,Authorization:`Bearer ${T}`},this.logger.debug("[BodhiExtClient] Injected auth token for authenticated request")}const g=chrome.runtime.connect(this.extensionId,{name:$});this.activeStreamPorts.set(r,g);const l=setTimeout(()=>{this.activeStreamPorts.has(r)&&(this.logger.error(`[BodhiExtClient] Stream timeout for ${r}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Stream request timed out",type:"timeout_error"}}),this.cleanupStreamPort(r))},S.STREAM_TIMEOUT);g.onMessage.addListener(T=>{if(D(T)){const I=T.response,R=I.body;I.status>=400?e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:r,response:I}):R?.done?(e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_DONE,requestId:r}),this.logger.info(`[BodhiExtClient] Stream complete for ${r}`),clearTimeout(l),this.cleanupStreamPort(r)):e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_CHUNK,requestId:r,response:I})}else U(T)?(this.logger.error(`[BodhiExtClient] Stream API error for ${r}: ${T.response.status}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_API_ERROR,requestId:r,response:T.response})):M(T)&&(this.logger.error(`[BodhiExtClient] Stream error for ${r}:`,T.error.message),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:`stream error: ${JSON.stringify(T)}`,type:"extension_error"}}),clearTimeout(l),this.cleanupStreamPort(r))}),g.onDisconnect.addListener(()=>{clearTimeout(l),this.activeStreamPorts.has(r)&&(this.logger.error(`[BodhiExtClient] Bodhi port disconnected for ${r}`),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:"Connection to Bodhi extension closed unexpectedly",type:"network_error"}}),this.activeStreamPorts.delete(r))});const m={type:p.STREAM_REQUEST,requestId:r,request:{method:s,endpoint:i,body:n,headers:u}};this.logger.debug("[BodhiExtClient] Sending stream request to bodhi port:",m),g.postMessage(m)}catch(u){const g=u;this.logger.error("[BodhiExtClient] Stream error:",JSON.stringify(g.message)),e.postMessage({type:c.EXT2EXT_CLIENT_STREAM_ERROR,requestId:r,error:{message:`uncaught error: ${JSON.stringify({error:g,message:g.message})}`,type:"extension_error"}})}}cleanupStreamPort(e){const t=this.activeStreamPorts.get(e);if(t){try{t.disconnect()}catch{}this.activeStreamPorts.delete(e)}}async requestAccess(){const e=await this.sendApiRequest("POST","/bodhi/v1/apps/request-access",{app_client_id:this.authClientId});if(!v(e))throw this.logger.error("[BodhiExtClient] Failed to get resource access scope: API error"),new Error("Failed to get resource access scope: API error");return e.body.scope}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 E.refreshAccessToken(this.authEndpoints.token,e,this.authClientId);if(t)return await this._storeRefreshedTokens(t),this.logger.info("Token refreshed successfully"),this.broadcastAuthStateChange(),t.access_token}catch(t){this.logger.warn("Token refresh failed:",t)}throw this.logger.warn("Token refresh failed, keeping tokens for manual retry"),E.createOperationError("Access token expired and unable to refresh. Try logging out and logging in again.","token_refresh_failed")}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,"/"),o=decodeURIComponent(atob(r).split("").map(s=>"%"+("00"+s.charCodeAt(0).toString(16)).slice(-2)).join(""));return JSON.parse(o)}createErrorClientNotInitialized(e){return`Client not initialized. Extension discovery not triggered nor extensionId set, cannot handle request: ${JSON.stringify(e)}`}};S.STREAM_TIMEOUT=6e4;let f=S;const z="production";exports.BodhiExtClient=f;exports.DISCOVERY_ATTEMPTS=N;exports.DISCOVERY_ATTEMPT_TIMEOUT=P;exports.DISCOVERY_ATTEMPT_WAIT_MS=O;exports.DISCOVERY_TIMEOUT_MS=w;exports.EXT2EXT_CLIENT_ACTIONS=_;exports.EXT2EXT_CLIENT_MESSAGE_TYPES=c;exports.EXT2EXT_CLIENT_STREAM_PORT=x;exports.EXT_BUILD_MODE=z;exports.ExtUIClient=V;exports.isExtClientApiError=X;
|
package/dist/bodhi-ext.esm.d.ts
CHANGED