diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 1bfe1fcf2..e5f9ef5c6 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -710,11 +710,32 @@ export class BackendClient extends BaseHttpClient { ); } - private async getAuthenticatedObjectURL(path: string): Promise { + private async getAuthenticatedObjectURL( + path: string, + rewritePluginPageSdk = false, + ): Promise { const response = await this.instance.get(path, { responseType: 'blob', }); - return URL.createObjectURL(response.data); + let blob = response.data; + if (rewritePluginPageSdk && blob.type.startsWith('text/html')) { + const apiBase = + this.instance.defaults.baseURL === '/' + ? window.location.origin + : this.instance.defaults.baseURL?.replace(/\/$/, ''); + const pageSdkUrl = `${apiBase}/api/v1/plugins/_sdk/page-sdk.js`; + const html = await blob.text(); + blob = new Blob( + [ + html.replace( + /(]*\bsrc\s*=\s*)(["'])\/api\/v1\/plugins\/_sdk\/page-sdk\.js\2/gi, + `$1$2${pageSdkUrl}$2`, + ), + ], + { type: blob.type }, + ); + } + return URL.createObjectURL(blob); } public getAuthenticatedPluginAssetURL( @@ -724,6 +745,7 @@ export class BackendClient extends BaseHttpClient { ): Promise { return this.getAuthenticatedObjectURL( `/api/v1/plugins/${author}/${name}/authenticated-assets/${filepath}`, + true, ); } diff --git a/web/tests/e2e/plugin-page-auth.spec.ts b/web/tests/e2e/plugin-page-auth.spec.ts index fb90d2f18..8f8601023 100644 --- a/web/tests/e2e/plugin-page-auth.spec.ts +++ b/web/tests/e2e/plugin-page-auth.spec.ts @@ -69,7 +69,55 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({ await route.fulfill({ status: 200, contentType: 'text/html', - body: '

LangRAG Observability

', + body: ` + + +

LangRAG Observability

+ + + + + `, + }); + }, + ); + let pageSdkRequests = 0; + await page.route('**/api/v1/plugins/_sdk/page-sdk.js', async (route) => { + pageSdkRequests += 1; + await route.fulfill({ + status: 200, + contentType: 'application/javascript', + body: `window.langbot = { + api(endpoint, body, method) { + return new Promise((resolve) => { + const requestId = 'request-' + Date.now(); + const handler = (event) => { + if (event.data?.type === 'langbot:api:response' && event.data.requestId === requestId) { + window.removeEventListener('message', handler); + resolve(event.data.data); + } + }; + window.addEventListener('message', handler); + window.parent.postMessage({ type: 'langbot:api', requestId, endpoint, body, method }, '*'); + }); + }, + };`, + }); + }); + let pageApiRequests = 0; + await page.route( + '**/api/v1/plugins/langbot-team/LangRAG/page-api', + async (route) => { + pageApiRequests += 1; + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: wrapped({ saved: true }), }); }, ); @@ -78,11 +126,17 @@ test('loads a Cloud plugin page through the authenticated asset route', async ({ '/home/plugin-pages?id=langbot-team%2FLangRAG%2Fobservability', ); + const pluginFrame = page.frameLocator('iframe'); await expect( - page - .frameLocator('iframe') - .getByRole('heading', { name: 'LangRAG Observability' }), + pluginFrame.getByRole('heading', { name: 'LangRAG Observability' }), ).toBeVisible(); + await pluginFrame.getByRole('button', { name: 'Save' }).click(); + await expect(pluginFrame.locator('body')).toHaveAttribute( + 'data-saved', + 'true', + ); expect(authenticatedAssetRequests).toBeGreaterThan(0); + expect(pageSdkRequests).toBe(1); + expect(pageApiRequests).toBe(1); await expect(page.getByText('Loading...')).toHaveCount(0); });