// @vitest-environment node
|
|
import type { Server as HttpServer, IncomingHttpHeaders } from 'node:http';
|
import type { ViteDevServer } from 'vite';
|
|
import { Buffer } from 'node:buffer';
|
import { createHash } from 'node:crypto';
|
import { readFileSync } from 'node:fs';
|
import { createServer as createHttpServer, request } from 'node:http';
|
import { resolve } from 'node:path';
|
import { fileURLToPath } from 'node:url';
|
import { gzipSync } from 'node:zlib';
|
|
import vue from '@vitejs/plugin-vue';
|
import { createServer } from 'vite';
|
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
import { createOnlyOfficeSdkProxy, onlyOfficeDevelopmentPlugin, resolveOnlyOfficeDevOptions } from '../../vite/onlyoffice-plugin';
|
|
const root = fileURLToPath(new URL('../..', import.meta.url));
|
const manifestPath = resolve(root, 'config.json');
|
const iconNames = ['icon.png', 'icon@2x.png'] as const;
|
const proxyErrorText = 'ONLYOFFICE SDK 代理失败\n';
|
const maxSdkResponseBytes = 8 * 1024 * 1024;
|
|
async function listen(server: HttpServer): Promise<number> {
|
await new Promise<void>((resolveListen, reject) => {
|
server.once('error', reject);
|
server.listen(0, '127.0.0.1', () => {
|
server.off('error', reject);
|
resolveListen();
|
});
|
});
|
const address = server.address();
|
if (!address || typeof address === 'string') throw new Error('HTTP 测试服务没有可用监听端口');
|
return address.port;
|
}
|
|
async function close(server: HttpServer): Promise<void> {
|
await new Promise<void>((resolveClose, reject) => server.close((error) => (error ? reject(error) : resolveClose())));
|
}
|
|
async function forceClose(server: HttpServer): Promise<void> {
|
server.closeAllConnections();
|
await close(server);
|
}
|
|
async function createProxyOrigin(docsUrl: string, timeoutMs = 5_000): Promise<{ close: () => Promise<void>; origin: string }> {
|
const handler = createOnlyOfficeSdkProxy({ docsUrl, timeoutMs });
|
const proxyServer = createHttpServer((incomingRequest, response) => {
|
void handler(incomingRequest, response, () => {
|
response.statusCode = 404;
|
response.end('Not Found');
|
});
|
});
|
const proxyPort = await listen(proxyServer);
|
return {
|
close: () => close(proxyServer),
|
origin: `http://127.0.0.1:${proxyPort}`,
|
};
|
}
|
|
function hashSourceManifest(): string {
|
return createHash('sha256').update(readFileSync(manifestPath)).digest('hex');
|
}
|
|
function expectNoStore(response: Response): void {
|
expect(response.headers.get('cache-control')).toBe('no-store, no-cache, must-revalidate');
|
expect(response.headers.get('pragma')).toBe('no-cache');
|
expect(response.headers.get('expires')).toBe('0');
|
}
|
|
describe('onlyoffice Vite 开发服务', () => {
|
const sourceManifestHash = hashSourceManifest();
|
let options = resolveOnlyOfficeDevOptions({}, new Date('2026-08-11T01:02:03Z'));
|
let origin: string;
|
let port: number;
|
let server: ViteDevServer;
|
let upstreamHeaders: IncomingHttpHeaders;
|
let upstreamMethod: string | undefined;
|
let upstreamStatus = 200;
|
let upstreamUrl: string | undefined;
|
let upstreamServer: HttpServer;
|
|
beforeAll(async () => {
|
upstreamServer = createHttpServer((incomingRequest, response) => {
|
upstreamHeaders = incomingRequest.headers;
|
upstreamMethod = incomingRequest.method;
|
upstreamUrl = incomingRequest.url;
|
response.statusCode = upstreamStatus;
|
const file = incomingRequest.url?.endsWith('plugins.css') ? 'plugins.css' : 'plugins.js';
|
response.setHeader('Content-Type', file === 'plugins.css' ? 'text/css; charset=utf-8' : 'application/javascript; charset=utf-8');
|
response.setHeader('ETag', `"fixture-${file}"`);
|
response.setHeader('Last-Modified', 'Mon, 10 Aug 2026 12:00:00 GMT');
|
response.end(incomingRequest.method === 'HEAD' ? undefined : `fixture:${file}`);
|
});
|
const upstreamPort = await listen(upstreamServer);
|
options = resolveOnlyOfficeDevOptions({ ONLYOFFICE_DOCS_URL: `http://127.0.0.1:${upstreamPort}` }, new Date('2026-08-11T01:02:03Z'));
|
server = await createServer({
|
configFile: false,
|
logLevel: 'silent',
|
plugins: [vue(), onlyOfficeDevelopmentPlugin(options)],
|
root,
|
server: {
|
host: '127.0.0.1',
|
port: 0,
|
},
|
});
|
await server.listen();
|
|
const address = server.httpServer?.address();
|
if (!address || typeof address === 'string') throw new Error('Vite 测试服务没有可用监听端口');
|
port = address.port;
|
origin = `http://127.0.0.1:${port}`;
|
});
|
|
afterAll(async () => {
|
await server?.close();
|
await close(upstreamServer);
|
});
|
|
it('在精确开发路径返回动态 manifest 和 no-store 响应头', async () => {
|
const response = await fetch(`${origin}${options.basePath}/config.json?v=${options.startupId}`);
|
|
expect(response.status).toBe(200);
|
expectNoStore(response);
|
await expect(response.json()).resolves.toMatchObject({
|
guid: 'asc.{7F4E8F35-5D66-47F8-A5B4-6AB3FAACF565}',
|
variations: [{ url: `index.html?v=${options.startupId}` }],
|
version: '0.1.0',
|
});
|
});
|
|
it('返回空翻译语言清单,避免 SDK 继续请求不存在的语言文件', async () => {
|
const response = await fetch(`${origin}${options.basePath}/translations/langs.json`);
|
|
expect(response.status).toBe(200);
|
expectNoStore(response);
|
await expect(response.json()).resolves.toEqual([]);
|
});
|
|
it.each(['POST', 'PUT'])('manifest 拒绝 %s 并声明允许的方法', async (method) => {
|
const response = await fetch(`${origin}${options.basePath}/config.json?v=${options.startupId}`, { method });
|
|
expect(response.status).toBe(405);
|
expect(response.headers.get('allow')).toBe('GET, HEAD');
|
expectNoStore(response);
|
});
|
|
it('将嵌套开发入口重写到 Vite HTML 并改写 SDK 资源', async () => {
|
const response = await fetch(`${origin}${options.basePath}/index.html?v=${options.startupId}`);
|
const html = await response.text();
|
|
expect(response.status).toBe(200);
|
expectNoStore(response);
|
expect(html).toContain('href="/onlyoffice-sdk/v1/plugins.css"');
|
expect(html).toContain('src="/onlyoffice-sdk/v1/plugins.js"');
|
expect(html).not.toContain('../v1/plugins.');
|
});
|
|
it.each(iconNames)('在开发路径返回与源码一致的白名单图标 %s', async (iconName) => {
|
const source = readFileSync(resolve(root, 'resources', iconName));
|
const response = await fetch(`${origin}${options.basePath}/resources/${iconName}`);
|
const content = Buffer.from(await response.arrayBuffer());
|
|
expect(response.status).toBe(200);
|
expectNoStore(response);
|
expect(content.byteLength).toBeGreaterThan(0);
|
expect(content).toEqual(source);
|
});
|
|
it.each(iconNames)('白名单图标 %s 支持 HEAD 且不返回 body', async (iconName) => {
|
const response = await fetch(`${origin}${options.basePath}/resources/${iconName}`, { method: 'HEAD' });
|
const content = await response.arrayBuffer();
|
|
expect(response.status).toBe(200);
|
expectNoStore(response);
|
expect(content.byteLength).toBe(0);
|
});
|
|
it.each(['icon-v2.png', 'icon.png.bak', 'missing.png'])('拒绝非白名单图标资源 %s', async (iconName) => {
|
const response = await fetch(`${origin}${options.basePath}/resources/${iconName}`);
|
|
expect(response.status).toBe(404);
|
});
|
|
it.each(['/@vite/client', '/src/main.ts', '/src/styles.css'])('为开发资源 %s 设置 no-store 响应头', async (pathname) => {
|
const response = await fetch(`${origin}${pathname}`);
|
|
expect(response.status).toBe(200);
|
expectNoStore(response);
|
});
|
|
it.each([
|
['plugins.js', 'application/javascript; charset=utf-8'],
|
['plugins.css', 'text/css; charset=utf-8'],
|
])('精确代理 SDK 资源 %s 并只转发白名单请求头', async (file, contentType) => {
|
const response = await fetch(`${origin}/onlyoffice-sdk/v1/${file}?browser-secret=query-secret`, {
|
headers: {
|
accept: `${contentType}, */*`,
|
authorization: 'Bearer authorization-secret',
|
cookie: 'session=cookie-secret',
|
'if-modified-since': 'Sun, 09 Aug 2026 12:00:00 GMT',
|
'if-none-match': 'browser-etag',
|
'x-browser-secret': 'private-header-value',
|
},
|
});
|
|
expect(response.status).toBe(200);
|
expect(await response.text()).toBe(`fixture:${file}`);
|
expect(response.headers.get('content-type')).toBe(contentType);
|
expect(response.headers.get('etag')).toBe(`"fixture-${file}"`);
|
expect(response.headers.get('last-modified')).toBe('Mon, 10 Aug 2026 12:00:00 GMT');
|
expectNoStore(response);
|
expect(upstreamUrl).toBe(`/sdkjs-plugins/v1/${file}`);
|
expect(upstreamHeaders.accept).toBe(`${contentType}, */*`);
|
expect(upstreamHeaders['if-none-match']).toBe('browser-etag');
|
expect(upstreamHeaders['if-modified-since']).toBe('Sun, 09 Aug 2026 12:00:00 GMT');
|
expect(upstreamHeaders.authorization).toBeUndefined();
|
expect(upstreamHeaders.cookie).toBeUndefined();
|
expect(upstreamHeaders['x-browser-secret']).toBeUndefined();
|
});
|
|
it('使用 HEAD 请求 SDK 上游且不返回 body', async () => {
|
const response = await fetch(`${origin}/onlyoffice-sdk/v1/plugins.js`, { method: 'HEAD' });
|
const body = await response.arrayBuffer();
|
|
expect(response.status).toBe(200);
|
expect(body.byteLength).toBe(0);
|
expect(upstreamMethod).toBe('HEAD');
|
expectNoStore(response);
|
});
|
|
it('sdk 精确资源拒绝非 GET/HEAD 方法', async () => {
|
const response = await fetch(`${origin}/onlyoffice-sdk/v1/plugins.js`, { method: 'POST' });
|
|
expect(response.status).toBe(405);
|
expect(response.headers.get('allow')).toBe('GET, HEAD');
|
expectNoStore(response);
|
});
|
|
it('正式插件日志只记录固定失败类别', async () => {
|
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
upstreamStatus = 500;
|
|
try {
|
const response = await fetch(`${origin}/onlyoffice-sdk/v1/plugins.js?secret=query`, {
|
headers: { authorization: 'Bearer secret', cookie: 'private=cookie' },
|
});
|
|
expect(response.status).toBe(502);
|
expect(consoleError.mock.calls).toEqual([['[onlyoffice-sdk-proxy] status']]);
|
expect(JSON.stringify(consoleError.mock.calls)).not.toMatch(/127\.0\.0\.1|secret|private|fixture/u);
|
} finally {
|
upstreamStatus = 200;
|
consoleError.mockRestore();
|
}
|
});
|
|
it.each(['/onlyoffice-sdk/v1/unknown.js', '/onlyoffice-sdk/v1/plugins.js.map', '/onlyoffice-sdk/v2/plugins.js'])(
|
'未知 SDK 路径返回 404:%s',
|
async (pathname) => {
|
const response = await fetch(`${origin}${pathname}`);
|
|
expect(response.status).toBe(404);
|
expectNoStore(response);
|
},
|
);
|
|
it.each([
|
'/onlyoffice-plugins/0.1.1-dev/eln-template-filler/config.json',
|
`${options.basePath}-similar/index.html`,
|
`${options.basePath}/index.html-similar`,
|
])('拒绝未知 release 或相似前缀 %s', async (pathname) => {
|
const response = await fetch(`${origin}${pathname}`);
|
|
expect(response.status).toBe(404);
|
});
|
|
it('拒绝开发 release 路径中的编码路径穿越', async () => {
|
const status = await new Promise<number | undefined>((resolveStatus, reject) => {
|
const clientRequest = request(
|
{
|
host: '127.0.0.1',
|
path: `${options.basePath}/%2e%2e/index.html`,
|
port,
|
},
|
(response) => {
|
response.resume();
|
response.on('end', () => resolveStatus(response.statusCode));
|
},
|
);
|
clientRequest.on('error', reject);
|
clientRequest.end();
|
});
|
|
expect(status).toBe(404);
|
});
|
|
it.each([`${options.basePath}/resources/../index.html`, `${options.basePath}/resources/%2e%2e/index.html`, `${options.basePath}/resources%2ficon.png`])(
|
'拒绝图标路径穿越或编码斜杠 %s',
|
async (path) => {
|
const status = await new Promise<number | undefined>((resolveStatus, reject) => {
|
const clientRequest = request({ host: '127.0.0.1', path, port }, (response) => {
|
response.resume();
|
response.on('end', () => resolveStatus(response.statusCode));
|
});
|
clientRequest.on('error', reject);
|
clientRequest.end();
|
});
|
|
expect(status).toBe(404);
|
},
|
);
|
|
it('开发请求不写回正式 manifest', () => {
|
expect(hashSourceManifest()).toBe(sourceManifestHash);
|
});
|
});
|
|
describe('onlyoffice SDK 代理失败语义', () => {
|
async function expectFixedProxyFailure(response: Response): Promise<void> {
|
expect(response.status).toBe(502);
|
expect(response.headers.get('content-type')).toBe('text/plain; charset=utf-8');
|
expectNoStore(response);
|
const body = await response.text();
|
expect(body).toBe(proxyErrorText);
|
expect(body).not.toMatch(/127\.0\.0\.1|secret|private|upstream/u);
|
}
|
|
it.each([404, 500])('上游 %s 统一返回固定 502,且不泄露上游响应', async (status) => {
|
const upstream = createHttpServer((_request, response) => {
|
response.statusCode = status;
|
response.end('upstream-private-body');
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js?secret=query`, {
|
headers: { authorization: 'Bearer secret', cookie: 'private=cookie' },
|
});
|
await expectFixedProxyFailure(response);
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('非 2xx 响应会主动取消未结束的上游 body', async () => {
|
let upstreamClosed = false;
|
const upstream = createHttpServer((_request, response) => {
|
response.statusCode = 500;
|
response.on('close', () => {
|
upstreamClosed = true;
|
});
|
response.write('upstream-private-body');
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
|
await vi.waitFor(() => expect(upstreamClosed).toBe(true));
|
} finally {
|
await proxy.close();
|
await forceClose(upstream);
|
}
|
});
|
|
it('拒绝跨 origin 重定向且不向重定向目标泄露条件请求头', async () => {
|
let redirectedRequestCount = 0;
|
let redirectedHeaders: IncomingHttpHeaders = {};
|
const redirected = createHttpServer((incomingRequest, response) => {
|
redirectedRequestCount += 1;
|
redirectedHeaders = incomingRequest.headers;
|
response.end('redirect-private-body');
|
});
|
const redirectedPort = await listen(redirected);
|
const upstream = createHttpServer((_request, response) => {
|
response.statusCode = 302;
|
response.setHeader('Location', `http://127.0.0.1:${redirectedPort}/private-target`);
|
response.end();
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`, {
|
headers: { 'if-modified-since': 'validator-secret', 'if-none-match': 'etag-secret' },
|
});
|
await expectFixedProxyFailure(response);
|
expect(redirectedRequestCount).toBe(0);
|
expect(redirectedHeaders['if-none-match']).toBeUndefined();
|
expect(redirectedHeaders['if-modified-since']).toBeUndefined();
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
await close(redirected);
|
}
|
});
|
|
it.each(['GET', 'HEAD'])('透传上游 304 及 validator,%s 响应无 body', async (method) => {
|
const upstream = createHttpServer((_request, response) => {
|
response.statusCode = 304;
|
response.setHeader('ETag', 'upstream-etag');
|
response.setHeader('Last-Modified', 'Mon, 10 Aug 2026 12:00:00 GMT');
|
response.end('must-not-be-forwarded');
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`, { method });
|
const body = await response.arrayBuffer();
|
|
expect(response.status).toBe(304);
|
expect(response.headers.get('etag')).toBe('upstream-etag');
|
expect(response.headers.get('last-modified')).toBe('Mon, 10 Aug 2026 12:00:00 GMT');
|
expectNoStore(response);
|
expect(body.byteLength).toBe(0);
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('上游忽略 identity 并返回 gzip 时按解码 body 代理', async () => {
|
const source = 'window.Asc = { plugin: true };';
|
const compressed = gzipSync(source);
|
let upstreamAcceptEncoding: string | undefined;
|
const upstream = createHttpServer((incomingRequest, response) => {
|
upstreamAcceptEncoding = incomingRequest.headers['accept-encoding'];
|
response.setHeader('Content-Encoding', 'gzip');
|
response.setHeader('Content-Length', String(compressed.byteLength));
|
response.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
response.end(compressed);
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
const response = await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`, {
|
headers: { 'accept-encoding': 'browser-secret-encoding' },
|
});
|
|
expect(response.status).toBe(200);
|
expect(await response.text()).toBe(source);
|
expect(response.headers.get('content-encoding')).toBeNull();
|
expect(upstreamAcceptEncoding).toBe('identity');
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('gzip 解码 body 超过 8 MiB 时返回固定 502', async () => {
|
const compressed = gzipSync(Buffer.alloc(maxSdkResponseBytes + 1, 1));
|
const upstream = createHttpServer((_request, response) => {
|
response.setHeader('Content-Encoding', 'gzip');
|
response.setHeader('Content-Length', String(compressed.byteLength));
|
response.end(compressed);
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('content-length 超过 8 MiB 时在读取 body 前返回固定 502', async () => {
|
let upstreamClosed = false;
|
const upstream = createHttpServer((_request, response) => {
|
response.setHeader('Content-Length', String(maxSdkResponseBytes + 1));
|
response.on('close', () => {
|
upstreamClosed = true;
|
});
|
response.write('not-forwarded');
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
|
await vi.waitFor(() => expect(upstreamClosed).toBe(true));
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('无 content-length 的响应流超过 8 MiB 时返回固定 502', async () => {
|
const chunk = Buffer.alloc(1024 * 1024, 1);
|
const upstream = createHttpServer((_request, response) => {
|
for (let index = 0; index < 9; index += 1) response.write(chunk);
|
response.end();
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.css`));
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('上游声明长度后截断响应流时返回固定 502', async () => {
|
const upstream = createHttpServer((_request, response) => {
|
response.setHeader('Content-Length', '1024');
|
response.end('truncated-private-body');
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`));
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('上游响应流中途错误时返回固定 502', async () => {
|
const upstream = createHttpServer((_request, response) => {
|
response.write('stream-private-body');
|
response.destroy();
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.css`));
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
|
it('客户端提前关闭连接时立即取消上游请求', async () => {
|
let resolveUpstreamStarted = (): void => undefined;
|
const upstreamStarted = new Promise<void>((resolveStarted) => {
|
resolveUpstreamStarted = resolveStarted;
|
});
|
let upstreamClosed = false;
|
const upstream = createHttpServer((_request, response) => {
|
resolveUpstreamStarted();
|
response.on('close', () => {
|
upstreamClosed = true;
|
});
|
response.write('partial');
|
});
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`, 5_000);
|
|
try {
|
const clientRequest = request(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js`);
|
clientRequest.on('error', () => undefined);
|
clientRequest.end();
|
await upstreamStarted;
|
clientRequest.destroy();
|
await vi.waitFor(() => expect(upstreamClosed).toBe(true));
|
} finally {
|
await forceClose(upstream);
|
await proxy.close();
|
}
|
});
|
|
it('上游连接拒绝统一返回固定 502', async () => {
|
const unavailable = createHttpServer();
|
const unavailablePort = await listen(unavailable);
|
await close(unavailable);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${unavailablePort}`);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.css?secret=query`));
|
} finally {
|
await proxy.close();
|
}
|
});
|
|
it('上游超时统一返回固定 502', async () => {
|
const upstream = createHttpServer(() => undefined);
|
const upstreamPort = await listen(upstream);
|
const proxy = await createProxyOrigin(`http://127.0.0.1:${upstreamPort}`, 20);
|
|
try {
|
await expectFixedProxyFailure(await fetch(`${proxy.origin}/onlyoffice-sdk/v1/plugins.js?secret=query`));
|
} finally {
|
await proxy.close();
|
await close(upstream);
|
}
|
});
|
});
|