mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(plugin-sdk): remove test-only facades (#122807)
This commit is contained in:
committed by
GitHub
parent
468dc55a39
commit
79380000e0
@@ -665,10 +665,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
[`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: bundledPluginWorkspace(),
|
[`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: bundledPluginWorkspace(),
|
||||||
[`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock`]: bundledPluginWorkspace(),
|
[`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock`]: bundledPluginWorkspace(),
|
||||||
[`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic`]: bundledPluginWorkspace([
|
[`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic`]: bundledPluginWorkspace(),
|
||||||
// The plugin-SDK anthropic-cli facade resolves this shipped artifact by basename.
|
|
||||||
"cli-api.ts!",
|
|
||||||
]),
|
|
||||||
[`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic-vertex`]: bundledPluginWorkspace(),
|
[`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic-vertex`]: bundledPluginWorkspace(),
|
||||||
[`${BUNDLED_PLUGIN_ROOT_DIR}/acpx`]: bundledPluginWorkspace([
|
[`${BUNDLED_PLUGIN_ROOT_DIR}/acpx`]: bundledPluginWorkspace([
|
||||||
// Copied as executable runtime internals by the package artifact manifest.
|
// Copied as executable runtime internals by the package artifact manifest.
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
// Lightweight Claude CLI identity artifact for core facades. The full api.js
|
|
||||||
// barrel drags the whole plugin graph through jiti on source checkouts
|
|
||||||
// (~130s per cold worker on CI); keep this surface to static facts only.
|
|
||||||
export { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js";
|
|
||||||
export { isClaudeCliProvider } from "./cli-shared.js";
|
|
||||||
@@ -84,11 +84,6 @@ vi.mock("../gateway/mcp-http.loopback-runtime.js", async (importOriginal) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../plugin-sdk/anthropic-cli.js", () => ({
|
|
||||||
CLAUDE_CLI_BACKEND_ID: "claude-cli",
|
|
||||||
isClaudeCliProvider: (providerId: string) => providerId === "claude-cli",
|
|
||||||
}));
|
|
||||||
|
|
||||||
function emitClaudeInputStarted(stdout: ((chunk: string) => void) | undefined, data: string): void {
|
function emitClaudeInputStarted(stdout: ((chunk: string) => void) | undefined, data: string): void {
|
||||||
const event = createClaudeInputStartedEvent(data);
|
const event = createClaudeInputStartedEvent(data);
|
||||||
if (event) {
|
if (event) {
|
||||||
|
|||||||
@@ -22,11 +22,6 @@ import { setCliRunnerExecuteTestDeps } from "./execute.test-support.js";
|
|||||||
import { writeCliSystemPromptFile } from "./helpers.js";
|
import { writeCliSystemPromptFile } from "./helpers.js";
|
||||||
import type { PreparedCliRunContext } from "./types.js";
|
import type { PreparedCliRunContext } from "./types.js";
|
||||||
|
|
||||||
vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({
|
|
||||||
CLAUDE_CLI_BACKEND_ID: "claude-cli",
|
|
||||||
isClaudeCliProvider: (providerId: string) => providerId === "claude-cli",
|
|
||||||
}));
|
|
||||||
|
|
||||||
type ProcessSupervisor = ReturnType<typeof getProcessSupervisor>;
|
type ProcessSupervisor = ReturnType<typeof getProcessSupervisor>;
|
||||||
type SupervisorSpawnFn = ProcessSupervisor["spawn"];
|
type SupervisorSpawnFn = ProcessSupervisor["spawn"];
|
||||||
|
|
||||||
|
|||||||
@@ -25,11 +25,6 @@ import { callGatewayTool } from "../tools/gateway.js";
|
|||||||
import { resetClaudeLiveSessionsForTest } from "./claude-live-session.test-support.js";
|
import { resetClaudeLiveSessionsForTest } from "./claude-live-session.test-support.js";
|
||||||
import { executePreparedCliRun } from "./execute.js";
|
import { executePreparedCliRun } from "./execute.js";
|
||||||
|
|
||||||
vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({
|
|
||||||
CLAUDE_CLI_BACKEND_ID: "claude-cli",
|
|
||||||
isClaudeCliProvider: (providerId: string) => providerId === "claude-cli",
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../tools/gateway.js", () => ({
|
vi.mock("../tools/gateway.js", () => ({
|
||||||
callGatewayTool: vi.fn(),
|
callGatewayTool: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ import { executePreparedCliRun } from "./execute.js";
|
|||||||
import { cliBackendLog } from "./log.js";
|
import { cliBackendLog } from "./log.js";
|
||||||
import type { PreparedCliRunContext } from "./types.js";
|
import type { PreparedCliRunContext } from "./types.js";
|
||||||
|
|
||||||
vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({
|
|
||||||
CLAUDE_CLI_BACKEND_ID: "claude-cli",
|
|
||||||
isClaudeCliProvider: (providerId: string) => providerId === "claude-cli",
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../tools/gateway.js", () => ({
|
vi.mock("../tools/gateway.js", () => ({
|
||||||
callGatewayTool: vi.fn(),
|
callGatewayTool: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -34,11 +34,6 @@ import { setCliRunnerExecuteTestDeps } from "./execute.test-support.js";
|
|||||||
import { writeCliSystemPromptFile } from "./helpers.js";
|
import { writeCliSystemPromptFile } from "./helpers.js";
|
||||||
import { cliBackendLog } from "./log.js";
|
import { cliBackendLog } from "./log.js";
|
||||||
|
|
||||||
vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({
|
|
||||||
CLAUDE_CLI_BACKEND_ID: "claude-cli",
|
|
||||||
isClaudeCliProvider: (providerId: string) => providerId === "claude-cli",
|
|
||||||
}));
|
|
||||||
|
|
||||||
type ProcessSupervisor = ReturnType<typeof getProcessSupervisor>;
|
type ProcessSupervisor = ReturnType<typeof getProcessSupervisor>;
|
||||||
type SupervisorSpawnFn = ProcessSupervisor["spawn"];
|
type SupervisorSpawnFn = ProcessSupervisor["spawn"];
|
||||||
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||||
|
|||||||
@@ -519,11 +519,6 @@ vi.mock("../plugins/manifest-registry.js", async () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
vi.mock("../plugin-sdk/matrix-deps.js", () => ({
|
|
||||||
ensureMatrixSdkInstalled: vi.fn(async () => {}),
|
|
||||||
isMatrixSdkAvailable: vi.fn(() => true),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("../channels/plugins/bundled.js", () => ({
|
vi.mock("../channels/plugins/bundled.js", () => ({
|
||||||
getBundledChannelSetupPlugin: (channel: string) =>
|
getBundledChannelSetupPlugin: (channel: string) =>
|
||||||
channel === "telegram"
|
channel === "telegram"
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
// Manual facade. Keep loader boundary explicit.
|
|
||||||
import { loadBundledPluginPublicSurfaceModuleSyncCore } from "./facade-loader.js";
|
|
||||||
|
|
||||||
type FacadeModule = {
|
|
||||||
CLAUDE_CLI_BACKEND_ID: string;
|
|
||||||
isClaudeCliProvider: (providerId: string) => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadFacadeModule(): FacadeModule {
|
|
||||||
// cli-api.js, not api.js: this facade evaluates at module scope, and the
|
|
||||||
// full barrel costs ~130s per cold jiti worker on source checkouts.
|
|
||||||
return loadBundledPluginPublicSurfaceModuleSyncCore<FacadeModule>({
|
|
||||||
dirName: "anthropic",
|
|
||||||
artifactBasename: "cli-api.js",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/** Anthropic plugin backend id for Claude CLI provider detection. */
|
|
||||||
export const CLAUDE_CLI_BACKEND_ID: FacadeModule["CLAUDE_CLI_BACKEND_ID"] =
|
|
||||||
loadFacadeModule()["CLAUDE_CLI_BACKEND_ID"];
|
|
||||||
/** Returns whether a provider id belongs to the Claude CLI backend family. */
|
|
||||||
export const isClaudeCliProvider: FacadeModule["isClaudeCliProvider"] = ((...args) =>
|
|
||||||
loadFacadeModule()["isClaudeCliProvider"](...args)) as FacadeModule["isClaudeCliProvider"];
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
/**
|
|
||||||
* Tests browser node-host facade delegation and unavailable facade behavior.
|
|
||||||
*/
|
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
||||||
|
|
||||||
const loadActivatedBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn());
|
|
||||||
|
|
||||||
vi.mock("./facade-runtime.js", () => ({
|
|
||||||
loadActivatedBundledPluginPublicSurfaceModuleSync,
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("browser node-host facade", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
loadActivatedBundledPluginPublicSurfaceModuleSync.mockReset();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("stays cold until the proxy command is called", async () => {
|
|
||||||
await import("./browser-node-host.js");
|
|
||||||
|
|
||||||
expect(loadActivatedBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("delegates the proxy command through the activated runtime facade", async () => {
|
|
||||||
const runBrowserProxyCommand = vi.fn(async () => '{"ok":true}');
|
|
||||||
loadActivatedBundledPluginPublicSurfaceModuleSync.mockReturnValue({
|
|
||||||
runBrowserProxyCommand,
|
|
||||||
});
|
|
||||||
|
|
||||||
const facade = await import("./browser-node-host.js");
|
|
||||||
|
|
||||||
await expect(facade.runBrowserProxyCommand('{"path":"/"}')).resolves.toBe('{"ok":true}');
|
|
||||||
expect(loadActivatedBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({
|
|
||||||
dirName: "browser",
|
|
||||||
artifactBasename: "runtime-api.js",
|
|
||||||
});
|
|
||||||
expect(runBrowserProxyCommand).toHaveBeenCalledWith('{"path":"/"}');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
/**
|
|
||||||
* Public SDK facade for invoking browser plugin node-host proxy commands.
|
|
||||||
*/
|
|
||||||
import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
|
|
||||||
|
|
||||||
type BrowserNodeHostFacadeModule = {
|
|
||||||
runBrowserProxyCommand(paramsJSON?: string | null): Promise<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadFacadeModule(): BrowserNodeHostFacadeModule {
|
|
||||||
return loadActivatedBundledPluginPublicSurfaceModuleSync<BrowserNodeHostFacadeModule>({
|
|
||||||
dirName: "browser",
|
|
||||||
artifactBasename: "runtime-api.js",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Runs a serialized browser proxy command through the activated browser plugin facade. */
|
|
||||||
export async function runBrowserProxyCommand(paramsJSON?: string | null): Promise<string> {
|
|
||||||
return await loadFacadeModule().runBrowserProxyCommand(paramsJSON);
|
|
||||||
}
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
// Fetch auth tests cover scoped bearer fallback retries and request header preservation.
|
|
||||||
import { describe, expect, it, vi } from "vitest";
|
|
||||||
import { fetchWithBearerAuthScopeFallback } from "./fetch-auth.js";
|
|
||||||
import { resolveRequestUrl } from "./request-url.js";
|
|
||||||
|
|
||||||
const asFetch = (fn: unknown): typeof fetch => fn as typeof fetch;
|
|
||||||
|
|
||||||
function fetchCall(fetchFn: ReturnType<typeof vi.fn>, index: number): [unknown, RequestInit?] {
|
|
||||||
const call = fetchFn.mock.calls[index];
|
|
||||||
if (!call) {
|
|
||||||
throw new Error(`expected fetch call ${index}`);
|
|
||||||
}
|
|
||||||
return call as [unknown, RequestInit?];
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("fetchWithBearerAuthScopeFallback", () => {
|
|
||||||
it("rejects non-https urls when https is required", async () => {
|
|
||||||
await expect(
|
|
||||||
fetchWithBearerAuthScopeFallback({
|
|
||||||
url: "http://example.com/file",
|
|
||||||
scopes: [],
|
|
||||||
requireHttps: true,
|
|
||||||
}),
|
|
||||||
).rejects.toThrow("URL must use HTTPS");
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
{
|
|
||||||
name: "returns immediately when the first attempt succeeds",
|
|
||||||
url: "https://example.com/file",
|
|
||||||
scopes: ["https://graph.microsoft.com"],
|
|
||||||
responses: [new Response("ok", { status: 200 })],
|
|
||||||
shouldAttachAuth: undefined,
|
|
||||||
expectedStatus: 200,
|
|
||||||
expectedFetchCalls: 1,
|
|
||||||
expectedTokenCalls: [] as string[],
|
|
||||||
expectedAuthHeader: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "retries with auth scopes after a 401 response",
|
|
||||||
url: "https://graph.microsoft.com/v1.0/me",
|
|
||||||
scopes: ["https://graph.microsoft.com", "https://api.botframework.com"],
|
|
||||||
responses: [
|
|
||||||
new Response("unauthorized", { status: 401 }),
|
|
||||||
new Response("ok", { status: 200 }),
|
|
||||||
],
|
|
||||||
shouldAttachAuth: undefined,
|
|
||||||
expectedStatus: 200,
|
|
||||||
expectedFetchCalls: 2,
|
|
||||||
expectedTokenCalls: ["https://graph.microsoft.com"],
|
|
||||||
expectedAuthHeader: "Bearer token-1",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "does not attach auth when host predicate rejects url",
|
|
||||||
url: "https://example.com/file",
|
|
||||||
scopes: ["https://graph.microsoft.com"],
|
|
||||||
responses: [new Response("unauthorized", { status: 401 })],
|
|
||||||
shouldAttachAuth: () => false,
|
|
||||||
expectedStatus: 401,
|
|
||||||
expectedFetchCalls: 1,
|
|
||||||
expectedTokenCalls: [] as string[],
|
|
||||||
expectedAuthHeader: null,
|
|
||||||
},
|
|
||||||
])(
|
|
||||||
"$name",
|
|
||||||
async ({
|
|
||||||
url,
|
|
||||||
scopes,
|
|
||||||
responses,
|
|
||||||
shouldAttachAuth,
|
|
||||||
expectedStatus,
|
|
||||||
expectedFetchCalls,
|
|
||||||
expectedTokenCalls,
|
|
||||||
expectedAuthHeader,
|
|
||||||
}) => {
|
|
||||||
const fetchFn = vi.fn();
|
|
||||||
for (const response of responses) {
|
|
||||||
fetchFn.mockResolvedValueOnce(response);
|
|
||||||
}
|
|
||||||
const tokenProvider = { getAccessToken: vi.fn(async () => "token-1") };
|
|
||||||
|
|
||||||
const response = await fetchWithBearerAuthScopeFallback({
|
|
||||||
url,
|
|
||||||
scopes,
|
|
||||||
fetchFn: asFetch(fetchFn),
|
|
||||||
tokenProvider,
|
|
||||||
shouldAttachAuth,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(expectedStatus);
|
|
||||||
expect(fetchFn).toHaveBeenCalledTimes(expectedFetchCalls);
|
|
||||||
const tokenCalls = tokenProvider.getAccessToken.mock.calls as unknown as Array<[string]>;
|
|
||||||
expect(tokenCalls.map(([scope]) => scope)).toEqual(expectedTokenCalls);
|
|
||||||
if (expectedAuthHeader === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const secondCallInit = fetchCall(fetchFn, 1)[1];
|
|
||||||
const secondHeaders = new Headers(secondCallInit?.headers);
|
|
||||||
expect(secondHeaders.get("authorization")).toBe(expectedAuthHeader);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("continues across scopes when token retrieval fails", async () => {
|
|
||||||
const fetchFn = vi
|
|
||||||
.fn()
|
|
||||||
.mockResolvedValueOnce(new Response("unauthorized", { status: 401 }))
|
|
||||||
.mockResolvedValueOnce(new Response("ok", { status: 200 }));
|
|
||||||
const tokenProvider = {
|
|
||||||
getAccessToken: vi
|
|
||||||
.fn()
|
|
||||||
.mockRejectedValueOnce(new Error("first scope failed"))
|
|
||||||
.mockResolvedValueOnce("token-2"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const response = await fetchWithBearerAuthScopeFallback({
|
|
||||||
url: "https://graph.microsoft.com/v1.0/me",
|
|
||||||
scopes: ["https://first.example", "https://second.example"],
|
|
||||||
fetchFn: asFetch(fetchFn),
|
|
||||||
tokenProvider,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(tokenProvider.getAccessToken).toHaveBeenCalledTimes(2);
|
|
||||||
expect(tokenProvider.getAccessToken).toHaveBeenNthCalledWith(1, "https://first.example");
|
|
||||||
expect(tokenProvider.getAccessToken).toHaveBeenNthCalledWith(2, "https://second.example");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("normalizes symbol-bearing request headers across unauthenticated and retry attempts", async () => {
|
|
||||||
const headers = { Accept: "application/json" } as Record<string, string> & {
|
|
||||||
[key: symbol]: unknown;
|
|
||||||
};
|
|
||||||
Object.defineProperty(headers, Symbol("sensitiveHeaders"), {
|
|
||||||
value: new Set(["accept"]),
|
|
||||||
enumerable: false,
|
|
||||||
});
|
|
||||||
const fetchFn = vi.fn(async (_url: string, init?: RequestInit) => {
|
|
||||||
const normalizedHeaders = new Headers(init?.headers);
|
|
||||||
expect(normalizedHeaders.get("accept")).toBe("application/json");
|
|
||||||
return fetchFn.mock.calls.length === 1
|
|
||||||
? new Response("unauthorized", { status: 401 })
|
|
||||||
: new Response("ok", { status: 200 });
|
|
||||||
});
|
|
||||||
const tokenProvider = { getAccessToken: vi.fn(async () => "token-1") };
|
|
||||||
|
|
||||||
const response = await fetchWithBearerAuthScopeFallback({
|
|
||||||
url: "https://graph.microsoft.com/v1.0/me",
|
|
||||||
scopes: ["https://graph.microsoft.com"],
|
|
||||||
fetchFn: asFetch(fetchFn),
|
|
||||||
tokenProvider,
|
|
||||||
requestInit: { headers },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(fetchFn).toHaveBeenCalledTimes(2);
|
|
||||||
expect(Object.getOwnPropertySymbols(fetchCall(fetchFn, 0)[1]?.headers as object)).toStrictEqual(
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
expect(new Headers(fetchCall(fetchFn, 1)[1]?.headers).get("authorization")).toBe(
|
|
||||||
"Bearer token-1",
|
|
||||||
);
|
|
||||||
expect(Object.getOwnPropertySymbols(headers)).toHaveLength(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("resolveRequestUrl", () => {
|
|
||||||
it.each([
|
|
||||||
{
|
|
||||||
name: "resolves string input",
|
|
||||||
input: "https://example.com/a",
|
|
||||||
expected: "https://example.com/a",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "resolves URL input",
|
|
||||||
input: new URL("https://example.com/b"),
|
|
||||||
expected: "https://example.com/b",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "resolves object input with url field",
|
|
||||||
input: { url: "https://example.com/c" } as unknown as RequestInfo,
|
|
||||||
expected: "https://example.com/c",
|
|
||||||
},
|
|
||||||
])("$name", ({ input, expected }) => {
|
|
||||||
expect(resolveRequestUrl(input)).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
// Fetch auth helpers provide scoped bearer-token retries for plugin HTTP requests.
|
|
||||||
import {
|
|
||||||
normalizeHeadersInitForFetch,
|
|
||||||
normalizeRequestInitHeadersForFetch,
|
|
||||||
} from "../infra/fetch-headers.js";
|
|
||||||
|
|
||||||
/** Token source used by scoped bearer-auth fetch retries. */
|
|
||||||
export type ScopeTokenProvider = {
|
|
||||||
/** Return a bearer token for the requested OAuth/API scope. */
|
|
||||||
getAccessToken: (scope: string) => Promise<string>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isAuthFailureStatus(status: number): boolean {
|
|
||||||
return status === 401 || status === 403;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Retry a fetch with bearer tokens from the provided scopes when the unauthenticated attempt fails. */
|
|
||||||
export async function fetchWithBearerAuthScopeFallback(params: {
|
|
||||||
/** Absolute URL to request. */
|
|
||||||
url: string;
|
|
||||||
/** Token scopes to try in order after the initial unauthenticated request fails. */
|
|
||||||
scopes: readonly string[];
|
|
||||||
/** Optional token source; when omitted, only the unauthenticated request is attempted. */
|
|
||||||
tokenProvider?: ScopeTokenProvider;
|
|
||||||
/** Fetch implementation override for tests or plugin runtimes. Defaults to global `fetch`. */
|
|
||||||
fetchFn?: typeof fetch;
|
|
||||||
/** Request options reused across unauthenticated and authenticated attempts. */
|
|
||||||
requestInit?: RequestInit;
|
|
||||||
/** Reject non-HTTPS URLs before any request is sent. */
|
|
||||||
requireHttps?: boolean;
|
|
||||||
/** Optional policy gate for whether this URL is allowed to receive bearer auth. */
|
|
||||||
shouldAttachAuth?: (url: string) => boolean;
|
|
||||||
/** Override which responses should trigger scoped-token retries. Defaults to 401/403. */
|
|
||||||
shouldRetry?: (response: Response) => boolean;
|
|
||||||
}): Promise<Response> {
|
|
||||||
const fetchFn = params.fetchFn ?? fetch;
|
|
||||||
let parsedUrl: URL;
|
|
||||||
try {
|
|
||||||
parsedUrl = new URL(params.url);
|
|
||||||
} catch {
|
|
||||||
throw new Error(`Invalid URL: ${params.url}`);
|
|
||||||
}
|
|
||||||
if (params.requireHttps === true && parsedUrl.protocol !== "https:") {
|
|
||||||
throw new Error(`URL must use HTTPS: ${params.url}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestInit = normalizeRequestInitHeadersForFetch(params.requestInit);
|
|
||||||
const fetchOnce = (headers?: Headers): Promise<Response> =>
|
|
||||||
fetchFn(params.url, {
|
|
||||||
...requestInit,
|
|
||||||
...(headers ? { headers } : {}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const firstAttempt = await fetchOnce();
|
|
||||||
if (firstAttempt.ok) {
|
|
||||||
return firstAttempt;
|
|
||||||
}
|
|
||||||
if (!params.tokenProvider) {
|
|
||||||
return firstAttempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldRetry =
|
|
||||||
params.shouldRetry ?? ((response: Response) => isAuthFailureStatus(response.status));
|
|
||||||
if (!shouldRetry(firstAttempt)) {
|
|
||||||
return firstAttempt;
|
|
||||||
}
|
|
||||||
if (params.shouldAttachAuth && !params.shouldAttachAuth(params.url)) {
|
|
||||||
return firstAttempt;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const scope of params.scopes) {
|
|
||||||
try {
|
|
||||||
const token = await params.tokenProvider.getAccessToken(scope);
|
|
||||||
const authHeaders = new Headers(normalizeHeadersInitForFetch(requestInit?.headers));
|
|
||||||
authHeaders.set("Authorization", `Bearer ${token}`);
|
|
||||||
const authAttempt = await fetchOnce(authHeaders);
|
|
||||||
if (authAttempt.ok) {
|
|
||||||
return authAttempt;
|
|
||||||
}
|
|
||||||
if (!shouldRetry(authAttempt)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore token/fetch errors and continue trying remaining scopes.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return firstAttempt;
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
// Manual facade. Keep loader boundary explicit.
|
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
|
||||||
import { loadBundledPluginPublicSurfaceModuleSyncCore } from "./facade-loader.js";
|
|
||||||
|
|
||||||
type FacadeModule = {
|
|
||||||
ensureMatrixSdkInstalled: (params: {
|
|
||||||
runtime: RuntimeEnv;
|
|
||||||
confirm?: (message: string) => Promise<boolean>;
|
|
||||||
}) => Promise<void>;
|
|
||||||
isMatrixSdkAvailable: () => boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadFacadeModule(): FacadeModule {
|
|
||||||
return loadBundledPluginPublicSurfaceModuleSyncCore<FacadeModule>({
|
|
||||||
dirName: "matrix",
|
|
||||||
artifactBasename: "runtime-api.js",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Ensure Matrix plugin runtime dependencies are available before Matrix setup/use. */
|
|
||||||
export const ensureMatrixSdkInstalled: FacadeModule["ensureMatrixSdkInstalled"] = ((...args) =>
|
|
||||||
loadFacadeModule().ensureMatrixSdkInstalled(...args)) as FacadeModule["ensureMatrixSdkInstalled"];
|
|
||||||
/** Returns whether Matrix SDK dependencies are currently importable. */
|
|
||||||
export const isMatrixSdkAvailable: FacadeModule["isMatrixSdkAvailable"] = ((...args) =>
|
|
||||||
loadFacadeModule().isMatrixSdkAvailable(...args)) as FacadeModule["isMatrixSdkAvailable"];
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveRequestUrl } from "./request-url.js";
|
||||||
|
|
||||||
|
describe("resolveRequestUrl", () => {
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "resolves string input",
|
||||||
|
input: "https://example.com/a",
|
||||||
|
expected: "https://example.com/a",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolves URL input",
|
||||||
|
input: new URL("https://example.com/b"),
|
||||||
|
expected: "https://example.com/b",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolves object input with url field",
|
||||||
|
input: { url: "https://example.com/c" } as unknown as RequestInfo,
|
||||||
|
expected: "https://example.com/c",
|
||||||
|
},
|
||||||
|
])("$name", ({ input, expected }) => {
|
||||||
|
expect(resolveRequestUrl(input)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user