diff --git a/docs/plugins/architecture.md b/docs/plugins/architecture.md index 19783028721..4ffdeb29125 100644 --- a/docs/plugins/architecture.md +++ b/docs/plugins/architecture.md @@ -974,6 +974,9 @@ Compatibility note: helper is only needed by a bundled extension, keep it behind the extension's local `api.js` or `runtime-api.js` seam instead of promoting it into `openclaw/plugin-sdk/`. +- Channel-branded bundled bars such as `feishu`, `googlechat`, `irc`, `line`, + `nostr`, `twitch`, and `zalo` stay private unless they are explicitly added + back to the public contract. - Capability-specific subpaths such as `image-generation`, `media-understanding`, and `speech` exist because bundled/native plugins use them today. Their presence does not by itself mean every exported helper is a diff --git a/docs/plugins/building-extensions.md b/docs/plugins/building-extensions.md index 259accaa3f0..bdbd384f192 100644 --- a/docs/plugins/building-extensions.md +++ b/docs/plugins/building-extensions.md @@ -100,6 +100,7 @@ import { createChannelPairingController } from "openclaw/plugin-sdk/channel-pair import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; import { createOptionalChannelSetupSurface } from "openclaw/plugin-sdk/channel-setup"; import { resolveChannelGroupRequireMention } from "openclaw/plugin-sdk/channel-policy"; +import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth"; // Wrong: monolithic root (lint will reject this) import { ... } from "openclaw/plugin-sdk"; @@ -120,6 +121,7 @@ Common subpaths: | `plugin-sdk/runtime-store` | Persistent plugin storage | | `plugin-sdk/allow-from` | Allowlist resolution | | `plugin-sdk/reply-payload` | Message reply types | +| `plugin-sdk/provider-oauth` | OAuth login + PKCE helpers | | `plugin-sdk/provider-onboard` | Provider onboarding config patches | | `plugin-sdk/testing` | Test utilities | diff --git a/extensions/acpx/runtime-api.ts b/extensions/acpx/runtime-api.ts index 8d1d125f226..9a019cdd0e6 100644 --- a/extensions/acpx/runtime-api.ts +++ b/extensions/acpx/runtime-api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/acpx"; +export * from "../../src/plugin-sdk/acpx.js"; diff --git a/extensions/chutes/index.ts b/extensions/chutes/index.ts index b715ad46c5a..89a2fc4a6fe 100644 --- a/extensions/chutes/index.ts +++ b/extensions/chutes/index.ts @@ -1,12 +1,12 @@ -import { definePluginEntry } from "openclaw/plugin-sdk/core"; +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; import { - buildOauthProviderAuthResult, createProviderApiKeyAuthMethod, resolveOAuthApiKeyMarker, type ProviderAuthContext, type ProviderAuthResult, } from "openclaw/plugin-sdk/provider-auth"; import { loginChutes } from "openclaw/plugin-sdk/provider-auth-login"; +import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth"; import { CHUTES_DEFAULT_MODEL_REF, applyChutesApiKeyConfig, diff --git a/extensions/device-pair/api.ts b/extensions/device-pair/api.ts index 299ad90f05d..eb4001b8a91 100644 --- a/extensions/device-pair/api.ts +++ b/extensions/device-pair/api.ts @@ -1 +1,8 @@ -export * from "openclaw/plugin-sdk/device-pair"; +export { + approveDevicePairing, + issueDeviceBootstrapToken, + listDevicePairing, +} from "openclaw/plugin-sdk/device-bootstrap"; +export { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; +export { resolveGatewayBindUrl, resolveTailnetHostWithRunner } from "openclaw/plugin-sdk/core"; +export { runPluginCommandWithTimeout } from "openclaw/plugin-sdk/sandbox"; diff --git a/extensions/feishu/runtime-api.ts b/extensions/feishu/runtime-api.ts index 1257d4a7f00..cde6bbf5569 100644 --- a/extensions/feishu/runtime-api.ts +++ b/extensions/feishu/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/feishu"; +// Private runtime barrel for the bundled Feishu extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/feishu.js"; diff --git a/extensions/google/gemini-cli-provider.ts b/extensions/google/gemini-cli-provider.ts index 412d02dd85f..77fa7077b5d 100644 --- a/extensions/google/gemini-cli-provider.ts +++ b/extensions/google/gemini-cli-provider.ts @@ -3,7 +3,7 @@ import type { ProviderAuthContext, ProviderFetchUsageSnapshotContext, } from "openclaw/plugin-sdk/plugin-entry"; -import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; +import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth"; import { fetchGeminiUsage } from "openclaw/plugin-sdk/provider-usage"; import { isModernGoogleModel, resolveGoogle31ForwardCompatModel } from "./provider-models.js"; diff --git a/extensions/google/runtime-api.ts b/extensions/google/runtime-api.ts index 7deb5b38f92..60e25c7303e 100644 --- a/extensions/google/runtime-api.ts +++ b/extensions/google/runtime-api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/google"; +export * from "../../src/plugin-sdk/google.js"; diff --git a/extensions/googlechat/runtime-api.ts b/extensions/googlechat/runtime-api.ts index 9eecea28139..cd47c0e56c7 100644 --- a/extensions/googlechat/runtime-api.ts +++ b/extensions/googlechat/runtime-api.ts @@ -1,4 +1,4 @@ // Private runtime barrel for the bundled Google Chat extension. -// Keep this barrel thin and aligned with the curated plugin-sdk/googlechat surface. +// Keep this barrel thin and aligned with the local extension surface. -export * from "openclaw/plugin-sdk/googlechat"; +export * from "../../src/plugin-sdk/googlechat.js"; diff --git a/extensions/irc/src/runtime-api.ts b/extensions/irc/src/runtime-api.ts index 93214aeda45..96e4bdbbe90 100644 --- a/extensions/irc/src/runtime-api.ts +++ b/extensions/irc/src/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/irc"; +// Private runtime barrel for the bundled IRC extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../../src/plugin-sdk/irc.js"; diff --git a/extensions/line/api.ts b/extensions/line/api.ts index 5fdc62bdfb4..35d637bcc56 100644 --- a/extensions/line/api.ts +++ b/extensions/line/api.ts @@ -1,2 +1,2 @@ -export * from "openclaw/plugin-sdk/line"; +export * from "./runtime-api.js"; export * from "./setup-api.js"; diff --git a/extensions/line/runtime-api.ts b/extensions/line/runtime-api.ts index af6082ba155..53f1be0c51c 100644 --- a/extensions/line/runtime-api.ts +++ b/extensions/line/runtime-api.ts @@ -1 +1,12 @@ -export * from "openclaw/plugin-sdk/line-core"; +// Private runtime barrel for the bundled LINE extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/line.js"; +export { resolveExactLineGroupConfigKey } from "../../src/plugin-sdk/line-core.js"; +export { + formatDocsLink, + setSetupChannelEnabled, + splitSetupEntries, + type ChannelSetupDmPolicy, + type ChannelSetupWizard, +} from "../../src/plugin-sdk/line-core.js"; diff --git a/extensions/lobster/runtime-api.ts b/extensions/lobster/runtime-api.ts index 7ab2351b77d..24898e04cf5 100644 --- a/extensions/lobster/runtime-api.ts +++ b/extensions/lobster/runtime-api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/lobster"; +export * from "../../src/plugin-sdk/lobster.js"; diff --git a/extensions/mattermost/runtime-api.ts b/extensions/mattermost/runtime-api.ts index e13fee5ad71..2bc65439262 100644 --- a/extensions/mattermost/runtime-api.ts +++ b/extensions/mattermost/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/mattermost"; +// Private runtime barrel for the bundled Mattermost extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/mattermost.js"; diff --git a/extensions/minimax/index.ts b/extensions/minimax/index.ts index e219ceec6a0..7dfd9816264 100644 --- a/extensions/minimax/index.ts +++ b/extensions/minimax/index.ts @@ -1,16 +1,16 @@ import { - buildOauthProviderAuthResult, definePluginEntry, type ProviderAuthContext, type ProviderAuthResult, type ProviderCatalogContext, -} from "openclaw/plugin-sdk/minimax-portal-auth"; +} from "openclaw/plugin-sdk/plugin-entry"; import { MINIMAX_OAUTH_MARKER, createProviderApiKeyAuthMethod, ensureAuthProfileStore, listProfilesForProvider, } from "openclaw/plugin-sdk/provider-auth"; +import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth"; import { fetchMinimaxUsage } from "openclaw/plugin-sdk/provider-usage"; import { minimaxMediaUnderstandingProvider, diff --git a/extensions/minimax/oauth.ts b/extensions/minimax/oauth.ts index fb405cd5559..20296b2a710 100644 --- a/extensions/minimax/oauth.ts +++ b/extensions/minimax/oauth.ts @@ -2,7 +2,7 @@ import { randomBytes, randomUUID } from "node:crypto"; import { generatePkceVerifierChallenge, toFormUrlEncoded, -} from "openclaw/plugin-sdk/minimax-portal-auth"; +} from "openclaw/plugin-sdk/provider-oauth"; export type MiniMaxRegion = "cn" | "global"; diff --git a/extensions/msteams/runtime-api.ts b/extensions/msteams/runtime-api.ts index 1347e49a695..e2b75780399 100644 --- a/extensions/msteams/runtime-api.ts +++ b/extensions/msteams/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/msteams"; +// Private runtime barrel for the bundled Microsoft Teams extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/msteams.js"; diff --git a/extensions/nextcloud-talk/runtime-api.ts b/extensions/nextcloud-talk/runtime-api.ts index fc9283930bd..80bc1b1dc7b 100644 --- a/extensions/nextcloud-talk/runtime-api.ts +++ b/extensions/nextcloud-talk/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/nextcloud-talk"; +// Private runtime barrel for the bundled Nextcloud Talk extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/nextcloud-talk.js"; diff --git a/extensions/nostr/api.ts b/extensions/nostr/api.ts index 3f3d64cc3bf..6606fb316b4 100644 --- a/extensions/nostr/api.ts +++ b/extensions/nostr/api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/nostr"; +export * from "./runtime-api.js"; diff --git a/extensions/nostr/runtime-api.ts b/extensions/nostr/runtime-api.ts index 3f3d64cc3bf..602b0ac81b7 100644 --- a/extensions/nostr/runtime-api.ts +++ b/extensions/nostr/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/nostr"; +// Private runtime barrel for the bundled Nostr extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/nostr.js"; diff --git a/extensions/openai/openai-codex-provider.ts b/extensions/openai/openai-codex-provider.ts index 66d182a341f..5027f486bb0 100644 --- a/extensions/openai/openai-codex-provider.ts +++ b/extensions/openai/openai-codex-provider.ts @@ -3,7 +3,6 @@ import type { ProviderResolveDynamicModelContext, ProviderRuntimeModel, } from "openclaw/plugin-sdk/plugin-entry"; -import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-auth"; import { CODEX_CLI_PROFILE_ID, ensureAuthProfileStore, @@ -17,6 +16,7 @@ import { normalizeProviderId, type ProviderPlugin, } from "openclaw/plugin-sdk/provider-models"; +import { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth"; import { createOpenAIAttributionHeadersWrapper } from "openclaw/plugin-sdk/provider-stream"; import { fetchCodexUsage } from "openclaw/plugin-sdk/provider-usage"; import { buildOpenAICodexProvider } from "./openai-codex-catalog.js"; diff --git a/extensions/phone-control/runtime-api.ts b/extensions/phone-control/runtime-api.ts index 7db40d08280..940bc8fe2ba 100644 --- a/extensions/phone-control/runtime-api.ts +++ b/extensions/phone-control/runtime-api.ts @@ -2,6 +2,6 @@ export { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; export type { OpenClawPluginApi, OpenClawPluginCommandDefinition, - OpenClawPluginService, PluginCommandContext, -} from "openclaw/plugin-sdk/core"; + OpenClawPluginService, +} from "openclaw/plugin-sdk/plugin-entry"; diff --git a/extensions/qwen-portal-auth/index.ts b/extensions/qwen-portal-auth/index.ts index e32eb8ef791..bcbc564dc33 100644 --- a/extensions/qwen-portal-auth/index.ts +++ b/extensions/qwen-portal-auth/index.ts @@ -1,9 +1,10 @@ -import { ensureAuthProfileStore, listProfilesForProvider } from "openclaw/plugin-sdk/agent-runtime"; -import { QWEN_OAUTH_MARKER } from "openclaw/plugin-sdk/agent-runtime"; import { buildQwenPortalProvider, QWEN_PORTAL_BASE_URL } from "./provider-catalog.js"; import { buildOauthProviderAuthResult, definePluginEntry, + ensureAuthProfileStore, + listProfilesForProvider, + QWEN_OAUTH_MARKER, refreshQwenPortalCredentials, type ProviderAuthContext, type ProviderCatalogContext, diff --git a/extensions/qwen-portal-auth/refresh.test.ts b/extensions/qwen-portal-auth/refresh.test.ts new file mode 100644 index 00000000000..2cbaeb65d27 --- /dev/null +++ b/extensions/qwen-portal-auth/refresh.test.ts @@ -0,0 +1,135 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { refreshQwenPortalCredentials } from "./refresh.js"; + +function expiredCredentials() { + return { + type: "oauth" as const, + provider: "qwen-portal", + access: "expired-access", + refresh: "refresh-token", + expires: Date.now() - 60_000, + }; +} + +describe("refreshQwenPortalCredentials", () => { + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + const runRefresh = async () => await refreshQwenPortalCredentials(expiredCredentials()); + + it("refreshes oauth credentials and preserves existing refresh token when absent", async () => { + globalThis.fetch = vi.fn(async () => { + return new Response( + JSON.stringify({ + access_token: "new-access", + expires_in: 3600, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + }) as typeof fetch; + + const result = await runRefresh(); + + expect(result.access).toBe("new-access"); + expect(result.refresh).toBe("refresh-token"); + expect(result.expires).toBeGreaterThan(Date.now()); + expect(globalThis.fetch).toHaveBeenCalledTimes(1); + expect(globalThis.fetch).toHaveBeenCalledWith( + "https://chat.qwen.ai/api/v1/oauth2/token", + expect.objectContaining({ + method: "POST", + body: expect.any(URLSearchParams), + }), + ); + }); + + it("replaces the refresh token when the server rotates it", async () => { + globalThis.fetch = vi.fn(async () => { + return new Response( + JSON.stringify({ + access_token: "new-access", + refresh_token: "rotated-refresh", + expires_in: 1200, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + }) as typeof fetch; + + const result = await runRefresh(); + + expect(result.refresh).toBe("rotated-refresh"); + }); + + it("rejects invalid expires_in payloads", async () => { + globalThis.fetch = vi.fn(async () => { + return new Response( + JSON.stringify({ + access_token: "new-access", + expires_in: 0, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + }) as typeof fetch; + + await expect(runRefresh()).rejects.toThrow( + "Qwen OAuth refresh response missing or invalid expires_in", + ); + }); + + it("turns 400 responses into a re-authenticate hint", async () => { + globalThis.fetch = vi.fn( + async () => new Response("bad refresh", { status: 400 }), + ) as typeof fetch; + + await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh token expired or invalid"); + }); + + it("requires a refresh token", async () => { + await expect( + refreshQwenPortalCredentials({ + type: "oauth", + provider: "qwen-portal", + access: "expired-access", + refresh: "", + expires: Date.now() - 60_000, + }), + ).rejects.toThrow("Qwen OAuth refresh token missing"); + }); + + it("rejects missing access tokens", async () => { + globalThis.fetch = vi.fn(async () => { + return new Response( + JSON.stringify({ + expires_in: 3600, + }), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); + }) as typeof fetch; + + await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh response missing access token"); + }); + + it("surfaces non-400 refresh failures", async () => { + globalThis.fetch = vi.fn( + async () => new Response("gateway down", { status: 502 }), + ) as typeof fetch; + + await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh failed: gateway down"); + }); +}); diff --git a/src/providers/qwen-portal-oauth.ts b/extensions/qwen-portal-auth/refresh.ts similarity index 96% rename from src/providers/qwen-portal-oauth.ts rename to extensions/qwen-portal-auth/refresh.ts index 159942ef2a9..eee8421e011 100644 --- a/src/providers/qwen-portal-oauth.ts +++ b/extensions/qwen-portal-auth/refresh.ts @@ -1,5 +1,5 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai"; -import { formatCliCommand } from "../cli/command-format.js"; +import { formatCliCommand } from "openclaw/plugin-sdk/setup-tools"; const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai"; const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`; @@ -54,9 +54,9 @@ export async function refreshQwenPortalCredentials( return { ...credentials, - access: accessToken, // RFC 6749 section 6: new refresh token is optional; if present, replace old. refresh: newRefreshToken || refreshToken, + access: accessToken, expires: Date.now() + expiresIn * 1000, }; } diff --git a/extensions/qwen-portal-auth/runtime-api.ts b/extensions/qwen-portal-auth/runtime-api.ts index 232a2886110..52ad77bf6f0 100644 --- a/extensions/qwen-portal-auth/runtime-api.ts +++ b/extensions/qwen-portal-auth/runtime-api.ts @@ -1 +1,10 @@ -export * from "openclaw/plugin-sdk/qwen-portal-auth"; +export { buildOauthProviderAuthResult } from "openclaw/plugin-sdk/provider-oauth"; +export { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +export type { ProviderAuthContext, ProviderCatalogContext } from "openclaw/plugin-sdk/plugin-entry"; +export { ensureAuthProfileStore, listProfilesForProvider } from "openclaw/plugin-sdk/provider-auth"; +export { QWEN_OAUTH_MARKER } from "openclaw/plugin-sdk/agent-runtime"; +export { + generatePkceVerifierChallenge, + toFormUrlEncoded, +} from "openclaw/plugin-sdk/provider-oauth"; +export { refreshQwenPortalCredentials } from "./refresh.js"; diff --git a/extensions/signal/src/accounts.ts b/extensions/signal/src/accounts.ts index 272b4612dc1..51bd1f7e96d 100644 --- a/extensions/signal/src/accounts.ts +++ b/extensions/signal/src/accounts.ts @@ -4,7 +4,7 @@ import { resolveAccountEntry, type OpenClawConfig, } from "openclaw/plugin-sdk/account-resolution"; -import type { SignalAccountConfig } from "openclaw/plugin-sdk/signal-core"; +import type { SignalAccountConfig } from "./runtime-api.js"; export type ResolvedSignalAccount = { accountId: string; diff --git a/extensions/signal/src/config-schema.ts b/extensions/signal/src/config-schema.ts index a4f2d054ffd..e67469e1499 100644 --- a/extensions/signal/src/config-schema.ts +++ b/extensions/signal/src/config-schema.ts @@ -1,3 +1,3 @@ -import { buildChannelConfigSchema, SignalConfigSchema } from "openclaw/plugin-sdk/signal-core"; +import { buildChannelConfigSchema, SignalConfigSchema } from "./runtime-api.js"; export const SignalChannelConfigSchema = buildChannelConfigSchema(SignalConfigSchema); diff --git a/extensions/signal/src/runtime-api.ts b/extensions/signal/src/runtime-api.ts index 93bce482026..172943641f8 100644 --- a/extensions/signal/src/runtime-api.ts +++ b/extensions/signal/src/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/signal"; +// Private runtime barrel for the bundled Signal extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../../src/plugin-sdk/signal.js"; diff --git a/extensions/synology-chat/api.ts b/extensions/synology-chat/api.ts deleted file mode 100644 index 4ff5241bd49..00000000000 --- a/extensions/synology-chat/api.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "openclaw/plugin-sdk/synology-chat"; -export * from "./setup-api.js"; diff --git a/extensions/synology-chat/src/channel.test-mocks.ts b/extensions/synology-chat/src/channel.test-mocks.ts index 21859ba90e9..77c4a6d223f 100644 --- a/extensions/synology-chat/src/channel.test-mocks.ts +++ b/extensions/synology-chat/src/channel.test-mocks.ts @@ -27,20 +27,37 @@ async function readRequestBodyWithLimitForTest(req: IncomingMessage): Promise ({ - DEFAULT_ACCOUNT_ID: "default", - setAccountEnabledInConfigSection: vi.fn((_opts: unknown) => ({})), - registerPluginHttpRoute: registerPluginHttpRouteMock, - buildChannelConfigSchema: vi.fn((schema: unknown) => ({ schema })), - readRequestBodyWithLimit: vi.fn(readRequestBodyWithLimitForTest), - isRequestBodyLimitError: vi.fn(() => false), - requestBodyErrorToText: vi.fn(() => "Request body too large"), - createFixedWindowRateLimiter: vi.fn(() => ({ - isRateLimited: vi.fn(() => false), - size: vi.fn(() => 0), - clear: vi.fn(), - })), -})); +vi.mock("openclaw/plugin-sdk/setup", async () => { + const actual = await vi.importActual("openclaw/plugin-sdk/setup"); + return { + ...actual, + DEFAULT_ACCOUNT_ID: "default", + }; +}); + +vi.mock("openclaw/plugin-sdk/channel-config-schema", async () => { + const actual = await vi.importActual("openclaw/plugin-sdk/channel-config-schema"); + return { + ...actual, + buildChannelConfigSchema: vi.fn((schema: unknown) => ({ schema })), + }; +}); + +vi.mock("openclaw/plugin-sdk/webhook-ingress", async () => { + const actual = await vi.importActual("openclaw/plugin-sdk/webhook-ingress"); + return { + ...actual, + registerPluginHttpRoute: registerPluginHttpRouteMock, + readRequestBodyWithLimit: vi.fn(readRequestBodyWithLimitForTest), + isRequestBodyLimitError: vi.fn(() => false), + requestBodyErrorToText: vi.fn(() => "Request body too large"), + createFixedWindowRateLimiter: vi.fn(() => ({ + isRateLimited: vi.fn(() => false), + size: vi.fn(() => 0), + clear: vi.fn(), + })), + }; +}); vi.mock("./client.js", () => ({ sendMessage: vi.fn().mockResolvedValue(true), diff --git a/extensions/synology-chat/src/channel.ts b/extensions/synology-chat/src/channel.ts index 9617dc129ae..ef01c240e10 100644 --- a/extensions/synology-chat/src/channel.ts +++ b/extensions/synology-chat/src/channel.ts @@ -8,6 +8,7 @@ import { createHybridChannelConfigAdapter, createScopedDmSecurityResolver, } from "openclaw/plugin-sdk/channel-config-helpers"; +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema"; import { createConditionalWarningCollector, projectWarningCollector, @@ -17,8 +18,9 @@ import { createEmptyChannelDirectoryAdapter, createTextPairingAdapter, } from "openclaw/plugin-sdk/channel-runtime"; +import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup"; +import { registerPluginHttpRoute } from "openclaw/plugin-sdk/webhook-ingress"; import { z } from "zod"; -import { DEFAULT_ACCOUNT_ID, registerPluginHttpRoute, buildChannelConfigSchema } from "../api.js"; import { listAccountIds, resolveAccount } from "./accounts.js"; import { sendMessage, sendFileUrl } from "./client.js"; import { getSynologyRuntime } from "./runtime.js"; diff --git a/extensions/synology-chat/src/config-schema.ts b/extensions/synology-chat/src/config-schema.ts index cfdc3fb7a81..4a9f868a87f 100644 --- a/extensions/synology-chat/src/config-schema.ts +++ b/extensions/synology-chat/src/config-schema.ts @@ -1,4 +1,4 @@ +import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema"; import { z } from "zod"; -import { buildChannelConfigSchema } from "../api.js"; export const SynologyChatChannelConfigSchema = buildChannelConfigSchema(z.object({}).passthrough()); diff --git a/extensions/synology-chat/src/runtime.ts b/extensions/synology-chat/src/runtime.ts index e1288f74468..3e0234029ac 100644 --- a/extensions/synology-chat/src/runtime.ts +++ b/extensions/synology-chat/src/runtime.ts @@ -1,5 +1,5 @@ +import type { PluginRuntime } from "openclaw/plugin-sdk/core"; import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store"; -import type { PluginRuntime } from "../api.js"; const { setRuntime: setSynologyRuntime, getRuntime: getSynologyRuntime } = createPluginRuntimeStore( diff --git a/extensions/synology-chat/src/security.ts b/extensions/synology-chat/src/security.ts index 8ac50016a12..c6a10560efb 100644 --- a/extensions/synology-chat/src/security.ts +++ b/extensions/synology-chat/src/security.ts @@ -3,7 +3,10 @@ */ import * as crypto from "node:crypto"; -import { createFixedWindowRateLimiter, type FixedWindowRateLimiter } from "../api.js"; +import { + createFixedWindowRateLimiter, + type FixedWindowRateLimiter, +} from "openclaw/plugin-sdk/webhook-ingress"; export type DmAuthorizationResult = | { allowed: true } diff --git a/extensions/synology-chat/src/webhook-handler.ts b/extensions/synology-chat/src/webhook-handler.ts index 4f38136e9a5..9382b78e54f 100644 --- a/extensions/synology-chat/src/webhook-handler.ts +++ b/extensions/synology-chat/src/webhook-handler.ts @@ -9,7 +9,7 @@ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, -} from "../api.js"; +} from "openclaw/plugin-sdk/webhook-ingress"; import { sendMessage, resolveChatUserId } from "./client.js"; import { validateToken, authorizeUserForDm, sanitizeInput, RateLimiter } from "./security.js"; import type { SynologyWebhookPayload, ResolvedSynologyChatAccount } from "./types.js"; diff --git a/extensions/tlon/api.ts b/extensions/tlon/api.ts index 5364c68f07d..6606fb316b4 100644 --- a/extensions/tlon/api.ts +++ b/extensions/tlon/api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/tlon"; +export * from "./runtime-api.js"; diff --git a/extensions/tlon/runtime-api.ts b/extensions/tlon/runtime-api.ts new file mode 100644 index 00000000000..3ba9718868f --- /dev/null +++ b/extensions/tlon/runtime-api.ts @@ -0,0 +1,4 @@ +// Private runtime barrel for the bundled Tlon extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/tlon.js"; diff --git a/extensions/twitch/api.ts b/extensions/twitch/api.ts index 68033283423..6606fb316b4 100644 --- a/extensions/twitch/api.ts +++ b/extensions/twitch/api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/twitch"; +export * from "./runtime-api.js"; diff --git a/extensions/twitch/runtime-api.ts b/extensions/twitch/runtime-api.ts index 68033283423..9d055202a39 100644 --- a/extensions/twitch/runtime-api.ts +++ b/extensions/twitch/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/twitch"; +// Private runtime barrel for the bundled Twitch extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/twitch.js"; diff --git a/extensions/voice-call/api.ts b/extensions/voice-call/api.ts index ef9f7d7a3c0..6606fb316b4 100644 --- a/extensions/voice-call/api.ts +++ b/extensions/voice-call/api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/voice-call"; +export * from "./runtime-api.js"; diff --git a/extensions/voice-call/runtime-api.ts b/extensions/voice-call/runtime-api.ts new file mode 100644 index 00000000000..f0b32548645 --- /dev/null +++ b/extensions/voice-call/runtime-api.ts @@ -0,0 +1,4 @@ +// Private runtime barrel for the bundled Voice Call extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/voice-call.js"; diff --git a/extensions/zai/runtime-api.ts b/extensions/zai/runtime-api.ts index 27c34abce5a..16d46dd4362 100644 --- a/extensions/zai/runtime-api.ts +++ b/extensions/zai/runtime-api.ts @@ -1 +1 @@ -export * from "openclaw/plugin-sdk/zai"; +export * from "../../src/plugin-sdk/zai.js"; diff --git a/extensions/zalo/runtime-api.ts b/extensions/zalo/runtime-api.ts index 666b1c2a59d..082f65d43b8 100644 --- a/extensions/zalo/runtime-api.ts +++ b/extensions/zalo/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/zalo"; +// Private runtime barrel for the bundled Zalo extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/zalo.js"; diff --git a/extensions/zalouser/runtime-api.ts b/extensions/zalouser/runtime-api.ts index ef062d07887..1b63edaea42 100644 --- a/extensions/zalouser/runtime-api.ts +++ b/extensions/zalouser/runtime-api.ts @@ -1 +1,4 @@ -export * from "openclaw/plugin-sdk/zalouser"; +// Private runtime barrel for the bundled Zalo Personal extension. +// Keep this barrel thin and aligned with the local extension surface. + +export * from "../../src/plugin-sdk/zalouser.js"; diff --git a/package.json b/package.json index 594ec0dfc78..7c5e36ce537 100644 --- a/package.json +++ b/package.json @@ -173,10 +173,6 @@ "types": "./dist/plugin-sdk/acp-runtime.d.ts", "default": "./dist/plugin-sdk/acp-runtime.js" }, - "./plugin-sdk/acpx": { - "types": "./dist/plugin-sdk/acpx.d.ts", - "default": "./dist/plugin-sdk/acpx.js" - }, "./plugin-sdk/telegram": { "types": "./dist/plugin-sdk/telegram.d.ts", "default": "./dist/plugin-sdk/telegram.js" @@ -193,46 +189,10 @@ "types": "./dist/plugin-sdk/discord-core.d.ts", "default": "./dist/plugin-sdk/discord-core.js" }, - "./plugin-sdk/feishu": { - "types": "./dist/plugin-sdk/feishu.d.ts", - "default": "./dist/plugin-sdk/feishu.js" - }, - "./plugin-sdk/google": { - "types": "./dist/plugin-sdk/google.d.ts", - "default": "./dist/plugin-sdk/google.js" - }, - "./plugin-sdk/googlechat": { - "types": "./dist/plugin-sdk/googlechat.d.ts", - "default": "./dist/plugin-sdk/googlechat.js" - }, - "./plugin-sdk/irc": { - "types": "./dist/plugin-sdk/irc.d.ts", - "default": "./dist/plugin-sdk/irc.js" - }, - "./plugin-sdk/line-core": { - "types": "./dist/plugin-sdk/line-core.d.ts", - "default": "./dist/plugin-sdk/line-core.js" - }, - "./plugin-sdk/lobster": { - "types": "./dist/plugin-sdk/lobster.d.ts", - "default": "./dist/plugin-sdk/lobster.js" - }, "./plugin-sdk/matrix": { "types": "./dist/plugin-sdk/matrix.d.ts", "default": "./dist/plugin-sdk/matrix.js" }, - "./plugin-sdk/mattermost": { - "types": "./dist/plugin-sdk/mattermost.d.ts", - "default": "./dist/plugin-sdk/mattermost.js" - }, - "./plugin-sdk/msteams": { - "types": "./dist/plugin-sdk/msteams.d.ts", - "default": "./dist/plugin-sdk/msteams.js" - }, - "./plugin-sdk/nextcloud-talk": { - "types": "./dist/plugin-sdk/nextcloud-talk.d.ts", - "default": "./dist/plugin-sdk/nextcloud-talk.js" - }, "./plugin-sdk/slack": { "types": "./dist/plugin-sdk/slack.d.ts", "default": "./dist/plugin-sdk/slack.js" @@ -249,10 +209,6 @@ "types": "./dist/plugin-sdk/imessage-core.d.ts", "default": "./dist/plugin-sdk/imessage-core.js" }, - "./plugin-sdk/signal": { - "types": "./dist/plugin-sdk/signal.d.ts", - "default": "./dist/plugin-sdk/signal.js" - }, "./plugin-sdk/whatsapp": { "types": "./dist/plugin-sdk/whatsapp.d.ts", "default": "./dist/plugin-sdk/whatsapp.js" @@ -313,9 +269,9 @@ "types": "./dist/plugin-sdk/boolean-param.d.ts", "default": "./dist/plugin-sdk/boolean-param.js" }, - "./plugin-sdk/device-pair": { - "types": "./dist/plugin-sdk/device-pair.d.ts", - "default": "./dist/plugin-sdk/device-pair.js" + "./plugin-sdk/device-bootstrap": { + "types": "./dist/plugin-sdk/device-bootstrap.d.ts", + "default": "./dist/plugin-sdk/device-bootstrap.js" }, "./plugin-sdk/diagnostics-otel": { "types": "./dist/plugin-sdk/diagnostics-otel.d.ts", @@ -369,10 +325,6 @@ "types": "./dist/plugin-sdk/keyed-async-queue.d.ts", "default": "./dist/plugin-sdk/keyed-async-queue.js" }, - "./plugin-sdk/line": { - "types": "./dist/plugin-sdk/line.d.ts", - "default": "./dist/plugin-sdk/line.js" - }, "./plugin-sdk/llm-task": { "types": "./dist/plugin-sdk/llm-task.d.ts", "default": "./dist/plugin-sdk/llm-task.js" @@ -381,14 +333,14 @@ "types": "./dist/plugin-sdk/memory-lancedb.d.ts", "default": "./dist/plugin-sdk/memory-lancedb.js" }, - "./plugin-sdk/minimax-portal-auth": { - "types": "./dist/plugin-sdk/minimax-portal-auth.d.ts", - "default": "./dist/plugin-sdk/minimax-portal-auth.js" - }, "./plugin-sdk/provider-auth": { "types": "./dist/plugin-sdk/provider-auth.d.ts", "default": "./dist/plugin-sdk/provider-auth.js" }, + "./plugin-sdk/provider-oauth": { + "types": "./dist/plugin-sdk/provider-oauth.d.ts", + "default": "./dist/plugin-sdk/provider-oauth.js" + }, "./plugin-sdk/provider-auth-api-key": { "types": "./dist/plugin-sdk/provider-auth-api-key.d.ts", "default": "./dist/plugin-sdk/provider-auth-api-key.js" @@ -429,10 +381,6 @@ "types": "./dist/plugin-sdk/image-generation.d.ts", "default": "./dist/plugin-sdk/image-generation.js" }, - "./plugin-sdk/nostr": { - "types": "./dist/plugin-sdk/nostr.d.ts", - "default": "./dist/plugin-sdk/nostr.js" - }, "./plugin-sdk/reply-history": { "types": "./dist/plugin-sdk/reply-history.d.ts", "default": "./dist/plugin-sdk/reply-history.js" @@ -453,10 +401,6 @@ "types": "./dist/plugin-sdk/request-url.d.ts", "default": "./dist/plugin-sdk/request-url.js" }, - "./plugin-sdk/qwen-portal-auth": { - "types": "./dist/plugin-sdk/qwen-portal-auth.d.ts", - "default": "./dist/plugin-sdk/qwen-portal-auth.js" - }, "./plugin-sdk/webhook-ingress": { "types": "./dist/plugin-sdk/webhook-ingress.d.ts", "default": "./dist/plugin-sdk/webhook-ingress.js" @@ -473,46 +417,14 @@ "types": "./dist/plugin-sdk/secret-input.d.ts", "default": "./dist/plugin-sdk/secret-input.js" }, - "./plugin-sdk/signal-core": { - "types": "./dist/plugin-sdk/signal-core.d.ts", - "default": "./dist/plugin-sdk/signal-core.js" - }, - "./plugin-sdk/synology-chat": { - "types": "./dist/plugin-sdk/synology-chat.d.ts", - "default": "./dist/plugin-sdk/synology-chat.js" - }, "./plugin-sdk/thread-ownership": { "types": "./dist/plugin-sdk/thread-ownership.d.ts", "default": "./dist/plugin-sdk/thread-ownership.js" }, - "./plugin-sdk/tlon": { - "types": "./dist/plugin-sdk/tlon.d.ts", - "default": "./dist/plugin-sdk/tlon.js" - }, - "./plugin-sdk/twitch": { - "types": "./dist/plugin-sdk/twitch.d.ts", - "default": "./dist/plugin-sdk/twitch.js" - }, - "./plugin-sdk/voice-call": { - "types": "./dist/plugin-sdk/voice-call.d.ts", - "default": "./dist/plugin-sdk/voice-call.js" - }, "./plugin-sdk/web-media": { "types": "./dist/plugin-sdk/web-media.d.ts", "default": "./dist/plugin-sdk/web-media.js" }, - "./plugin-sdk/zai": { - "types": "./dist/plugin-sdk/zai.d.ts", - "default": "./dist/plugin-sdk/zai.js" - }, - "./plugin-sdk/zalo": { - "types": "./dist/plugin-sdk/zalo.d.ts", - "default": "./dist/plugin-sdk/zalo.js" - }, - "./plugin-sdk/zalouser": { - "types": "./dist/plugin-sdk/zalouser.d.ts", - "default": "./dist/plugin-sdk/zalouser.js" - }, "./plugin-sdk/speech": { "types": "./dist/plugin-sdk/speech.d.ts", "default": "./dist/plugin-sdk/speech.js" diff --git a/scripts/lib/plugin-sdk-entrypoints.json b/scripts/lib/plugin-sdk-entrypoints.json index f9c20590e4b..e1991f4ab76 100644 --- a/scripts/lib/plugin-sdk-entrypoints.json +++ b/scripts/lib/plugin-sdk-entrypoints.json @@ -33,26 +33,15 @@ "hook-runtime", "process-runtime", "acp-runtime", - "acpx", "telegram", "telegram-core", "discord", "discord-core", - "feishu", - "google", - "googlechat", - "irc", - "line-core", - "lobster", "matrix", - "mattermost", - "msteams", - "nextcloud-talk", "slack", "slack-core", "imessage", "imessage-core", - "signal", "whatsapp", "whatsapp-shared", "whatsapp-action-runtime", @@ -68,7 +57,7 @@ "allowlist-resolution", "allowlist-config-edit", "boolean-param", - "device-pair", + "device-bootstrap", "diagnostics-otel", "diffs", "extension-shared", @@ -82,11 +71,10 @@ "directory-runtime", "json-store", "keyed-async-queue", - "line", "llm-task", "memory-lancedb", - "minimax-portal-auth", "provider-auth", + "provider-oauth", "provider-auth-api-key", "provider-auth-login", "plugin-entry", @@ -97,27 +85,17 @@ "provider-usage", "provider-web-search", "image-generation", - "nostr", "reply-history", "media-understanding", "secret-input-runtime", "secret-input-schema", "request-url", - "qwen-portal-auth", "webhook-ingress", "webhook-path", "runtime-store", "secret-input", - "signal-core", - "synology-chat", "thread-ownership", - "tlon", - "twitch", - "voice-call", "web-media", - "zai", - "zalo", - "zalouser", "speech", "state-paths", "tool-send" diff --git a/src/plugin-sdk/acpx.ts b/src/plugin-sdk/acpx.ts index 9d634ec8fb5..1e131f0dfd3 100644 --- a/src/plugin-sdk/acpx.ts +++ b/src/plugin-sdk/acpx.ts @@ -1,4 +1,4 @@ -// Public ACPX runtime backend helpers. +// Private ACPX runtime backend helpers for bundled extensions. // Keep this surface narrow and limited to the ACP runtime/backend contract. export type { AcpRuntimeErrorCode } from "../acp/runtime/errors.js"; diff --git a/src/plugin-sdk/core.ts b/src/plugin-sdk/core.ts index 3c588f5a06e..38509cef4ab 100644 --- a/src/plugin-sdk/core.ts +++ b/src/plugin-sdk/core.ts @@ -91,7 +91,6 @@ export { parseOptionalDelimitedEntries, } from "../channels/plugins/helpers.js"; export { getChatChannelMeta } from "../channels/registry.js"; -export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; export { channelTargetSchema, channelTargetsSchema, diff --git a/src/plugin-sdk/device-bootstrap.ts b/src/plugin-sdk/device-bootstrap.ts new file mode 100644 index 00000000000..c3ecf15ab51 --- /dev/null +++ b/src/plugin-sdk/device-bootstrap.ts @@ -0,0 +1,4 @@ +// Shared bootstrap/pairing helpers for plugins that provision remote devices. + +export { approveDevicePairing, listDevicePairing } from "../infra/device-pairing.js"; +export { issueDeviceBootstrapToken } from "../infra/device-bootstrap.js"; diff --git a/src/plugin-sdk/device-pair.ts b/src/plugin-sdk/device-pair.ts deleted file mode 100644 index a87e1eea8f1..00000000000 --- a/src/plugin-sdk/device-pair.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Narrow plugin-sdk surface for the bundled device-pair plugin. -// Keep this list additive and scoped to symbols used under extensions/device-pair. - -export { definePluginEntry } from "./core.js"; -export { approveDevicePairing, listDevicePairing } from "../infra/device-pairing.js"; -export { issueDeviceBootstrapToken } from "../infra/device-bootstrap.js"; -export type { OpenClawPluginApi } from "../plugins/types.js"; -export { resolveGatewayBindUrl } from "../shared/gateway-bind-url.js"; -export { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js"; -export { runPluginCommandWithTimeout } from "./run-command.js"; diff --git a/src/plugin-sdk/feishu.ts b/src/plugin-sdk/feishu.ts index 70a55d58474..b616d16fdd0 100644 --- a/src/plugin-sdk/feishu.ts +++ b/src/plugin-sdk/feishu.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled feishu plugin. +// Private helper surface for the bundled feishu plugin. // Keep this list additive and scoped to symbols used under extensions/feishu. export type { HistoryEntry } from "../auto-reply/reply/history.js"; diff --git a/src/plugin-sdk/google.ts b/src/plugin-sdk/google.ts index b39d4aa4ced..79ca16d674d 100644 --- a/src/plugin-sdk/google.ts +++ b/src/plugin-sdk/google.ts @@ -1,4 +1,4 @@ -// Public Google-specific helpers used by bundled Google plugins. +// Private Google-specific helpers used by bundled Google plugins. export { normalizeGoogleModelId } from "../agents/model-id-normalization.js"; export { parseGeminiAuth } from "../infra/gemini-auth.js"; diff --git a/src/plugin-sdk/googlechat.ts b/src/plugin-sdk/googlechat.ts index 35f07014e86..026a5d157f8 100644 --- a/src/plugin-sdk/googlechat.ts +++ b/src/plugin-sdk/googlechat.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled googlechat plugin. +// Private helper surface for the bundled googlechat plugin. // Keep this list additive and scoped to symbols used under extensions/googlechat. import { resolveChannelGroupRequireMention } from "./channel-policy.js"; diff --git a/src/plugin-sdk/irc.ts b/src/plugin-sdk/irc.ts index 29df9fb5748..01e9b8557b9 100644 --- a/src/plugin-sdk/irc.ts +++ b/src/plugin-sdk/irc.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled irc plugin. +// Private helper surface for the bundled irc plugin. // Keep this list additive and scoped to symbols used under extensions/irc. export { resolveControlCommandGate } from "../channels/command-gating.js"; diff --git a/src/plugin-sdk/lobster.ts b/src/plugin-sdk/lobster.ts index c6a2a413acc..2434e1be70e 100644 --- a/src/plugin-sdk/lobster.ts +++ b/src/plugin-sdk/lobster.ts @@ -1,4 +1,4 @@ -// Public Lobster plugin helpers. +// Private Lobster plugin helpers for bundled extensions. // Keep this surface narrow and limited to the Lobster workflow/tool contract. export { definePluginEntry } from "./core.js"; diff --git a/src/plugin-sdk/mattermost.ts b/src/plugin-sdk/mattermost.ts index 8ab28d2a4ea..25856195bd2 100644 --- a/src/plugin-sdk/mattermost.ts +++ b/src/plugin-sdk/mattermost.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled mattermost plugin. +// Private helper surface for the bundled mattermost plugin. // Keep this list additive and scoped to symbols used under extensions/mattermost. export { formatInboundFromLabel } from "../auto-reply/envelope.js"; diff --git a/src/plugin-sdk/minimax-portal-auth.ts b/src/plugin-sdk/minimax-portal-auth.ts deleted file mode 100644 index a8dad415488..00000000000 --- a/src/plugin-sdk/minimax-portal-auth.ts +++ /dev/null @@ -1,12 +0,0 @@ -// Narrow plugin-sdk surface for MiniMax OAuth helpers used by the bundled minimax plugin. -// Keep this list additive and scoped to MiniMax OAuth support code. - -export { definePluginEntry } from "./core.js"; -export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; -export type { - OpenClawPluginApi, - ProviderAuthContext, - ProviderCatalogContext, - ProviderAuthResult, -} from "../plugins/types.js"; -export { generatePkceVerifierChallenge, toFormUrlEncoded } from "./oauth-utils.js"; diff --git a/src/plugin-sdk/msteams.ts b/src/plugin-sdk/msteams.ts index 1c72c82ea53..9937d1d9c3d 100644 --- a/src/plugin-sdk/msteams.ts +++ b/src/plugin-sdk/msteams.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled msteams plugin. +// Private helper surface for the bundled msteams plugin. // Keep this list additive and scoped to symbols used under extensions/msteams. import { createOptionalChannelSetupSurface } from "./channel-setup.js"; diff --git a/src/plugin-sdk/nextcloud-talk.ts b/src/plugin-sdk/nextcloud-talk.ts index 229ff806db0..c231cf49564 100644 --- a/src/plugin-sdk/nextcloud-talk.ts +++ b/src/plugin-sdk/nextcloud-talk.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled nextcloud-talk plugin. +// Private helper surface for the bundled nextcloud-talk plugin. // Keep this list additive and scoped to symbols used under extensions/nextcloud-talk. export { logInboundDrop } from "../channels/logging.js"; diff --git a/src/plugin-sdk/nostr.ts b/src/plugin-sdk/nostr.ts index 640642dcd46..95647cc1dcc 100644 --- a/src/plugin-sdk/nostr.ts +++ b/src/plugin-sdk/nostr.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled nostr plugin. +// Private helper surface for the bundled nostr plugin. // Keep this list additive and scoped to symbols used under extensions/nostr. import { createOptionalChannelSetupSurface } from "./channel-setup.js"; diff --git a/src/plugin-sdk/plugin-entry.ts b/src/plugin-sdk/plugin-entry.ts index 9d0cb1eceba..e411cb51e89 100644 --- a/src/plugin-sdk/plugin-entry.ts +++ b/src/plugin-sdk/plugin-entry.ts @@ -11,6 +11,7 @@ export type { AnyAgentTool, MediaUnderstandingProviderPlugin, OpenClawPluginApi, + PluginCommandContext, OpenClawPluginConfigSchema, ProviderDiscoveryContext, ProviderCatalogContext, diff --git a/src/plugin-sdk/provider-auth.ts b/src/plugin-sdk/provider-auth.ts index 645073a4d02..13125b7704c 100644 --- a/src/plugin-sdk/provider-auth.ts +++ b/src/plugin-sdk/provider-auth.ts @@ -5,7 +5,6 @@ export type { SecretInput } from "../config/types.secrets.js"; export type { ProviderAuthResult } from "../plugins/types.js"; export type { ProviderAuthContext } from "../plugins/types.js"; export type { AuthProfileStore, OAuthCredential } from "../agents/auth-profiles/types.js"; -export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; export { CLAUDE_CLI_PROFILE_ID, diff --git a/src/plugin-sdk/provider-oauth.ts b/src/plugin-sdk/provider-oauth.ts new file mode 100644 index 00000000000..8e183c55954 --- /dev/null +++ b/src/plugin-sdk/provider-oauth.ts @@ -0,0 +1,4 @@ +// Focused OAuth helpers for provider plugins. + +export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; +export { generatePkceVerifierChallenge, toFormUrlEncoded } from "./oauth-utils.js"; diff --git a/src/plugin-sdk/qwen-portal-auth.ts b/src/plugin-sdk/qwen-portal-auth.ts deleted file mode 100644 index adc61259a09..00000000000 --- a/src/plugin-sdk/qwen-portal-auth.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Narrow plugin-sdk surface for the bundled qwen-portal-auth plugin. -// Keep this list additive and scoped to symbols used under extensions/qwen-portal-auth. - -export { definePluginEntry } from "./core.js"; -export { buildOauthProviderAuthResult } from "./provider-auth-result.js"; -export type { - OpenClawPluginApi, - ProviderAuthContext, - ProviderCatalogContext, -} from "../plugins/types.js"; -export { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js"; -export { QWEN_OAUTH_MARKER } from "../agents/model-auth-markers.js"; -export { refreshQwenPortalCredentials } from "../providers/qwen-portal-oauth.js"; -export { generatePkceVerifierChallenge, toFormUrlEncoded } from "./oauth-utils.js"; diff --git a/src/plugin-sdk/runtime-api-guardrails.test.ts b/src/plugin-sdk/runtime-api-guardrails.test.ts index a8a7f4cd769..78a39d7ccb3 100644 --- a/src/plugin-sdk/runtime-api-guardrails.test.ts +++ b/src/plugin-sdk/runtime-api-guardrails.test.ts @@ -34,13 +34,13 @@ const RUNTIME_API_EXPORT_GUARDS: Record = { 'export { probeIMessage } from "./src/probe.js";', 'export { sendMessageIMessage } from "./src/send.js";', ], - "extensions/googlechat/runtime-api.ts": ['export * from "openclaw/plugin-sdk/googlechat";'], + "extensions/googlechat/runtime-api.ts": ['export * from "../../src/plugin-sdk/googlechat.js";'], "extensions/matrix/runtime-api.ts": [ 'export * from "./src/auth-precedence.js";', 'export * from "./helper-api.js";', ], "extensions/nextcloud-talk/runtime-api.ts": [ - 'export * from "openclaw/plugin-sdk/nextcloud-talk";', + 'export * from "../../src/plugin-sdk/nextcloud-talk.js";', ], "extensions/signal/runtime-api.ts": ['export * from "./src/runtime-api.js";'], "extensions/slack/runtime-api.ts": [ diff --git a/src/plugin-sdk/signal-core.ts b/src/plugin-sdk/signal-core.ts index 89b0dde05af..d7e5277d1ab 100644 --- a/src/plugin-sdk/signal-core.ts +++ b/src/plugin-sdk/signal-core.ts @@ -1,3 +1,6 @@ +// Private helper surface for the bundled signal plugin. +// Keep this list additive and scoped to symbols used under extensions/signal. + export type { SignalAccountConfig } from "../config/types.js"; export type { ChannelPlugin } from "./channel-plugin-common.js"; export { diff --git a/src/plugin-sdk/signal.ts b/src/plugin-sdk/signal.ts index b3a7d0147b5..def847ccd33 100644 --- a/src/plugin-sdk/signal.ts +++ b/src/plugin-sdk/signal.ts @@ -1,3 +1,6 @@ +// Private helper surface for the bundled signal plugin. +// Keep this list additive and scoped to symbols used under extensions/signal. + export type { ChannelMessageActionAdapter } from "../channels/plugins/types.js"; export type { OpenClawConfig } from "../config/config.js"; export type { SignalAccountConfig } from "../config/types.js"; diff --git a/src/plugin-sdk/subpaths.test.ts b/src/plugin-sdk/subpaths.test.ts index 069a0be8067..ab8c16d71f7 100644 --- a/src/plugin-sdk/subpaths.test.ts +++ b/src/plugin-sdk/subpaths.test.ts @@ -16,7 +16,9 @@ import * as imessageSdk from "openclaw/plugin-sdk/imessage"; import * as imessageCoreSdk from "openclaw/plugin-sdk/imessage-core"; import * as lazyRuntimeSdk from "openclaw/plugin-sdk/lazy-runtime"; import * as ollamaSetupSdk from "openclaw/plugin-sdk/ollama-setup"; +import * as providerAuthSdk from "openclaw/plugin-sdk/provider-auth"; import * as providerModelsSdk from "openclaw/plugin-sdk/provider-models"; +import * as providerOauthSdk from "openclaw/plugin-sdk/provider-oauth"; import * as providerSetupSdk from "openclaw/plugin-sdk/provider-setup"; import * as replyPayloadSdk from "openclaw/plugin-sdk/reply-payload"; import * as routingSdk from "openclaw/plugin-sdk/routing"; @@ -56,10 +58,33 @@ const allowlistEditSdk = await import("openclaw/plugin-sdk/allowlist-config-edit describe("plugin-sdk subpath exports", () => { it("keeps the curated public list free of internal implementation subpaths", () => { + expect(pluginSdkSubpaths).not.toContain("acpx"); expect(pluginSdkSubpaths).not.toContain("compat"); + expect(pluginSdkSubpaths).not.toContain("device-pair"); + expect(pluginSdkSubpaths).not.toContain("feishu"); + expect(pluginSdkSubpaths).not.toContain("google"); + expect(pluginSdkSubpaths).not.toContain("googlechat"); + expect(pluginSdkSubpaths).not.toContain("irc"); + expect(pluginSdkSubpaths).not.toContain("line"); + expect(pluginSdkSubpaths).not.toContain("line-core"); + expect(pluginSdkSubpaths).not.toContain("lobster"); + expect(pluginSdkSubpaths).not.toContain("mattermost"); + expect(pluginSdkSubpaths).not.toContain("msteams"); + expect(pluginSdkSubpaths).not.toContain("nextcloud-talk"); + expect(pluginSdkSubpaths).not.toContain("nostr"); expect(pluginSdkSubpaths).not.toContain("pairing-access"); + expect(pluginSdkSubpaths).not.toContain("qwen-portal-auth"); expect(pluginSdkSubpaths).not.toContain("reply-prefix"); + expect(pluginSdkSubpaths).not.toContain("signal"); + expect(pluginSdkSubpaths).not.toContain("signal-core"); + expect(pluginSdkSubpaths).not.toContain("synology-chat"); + expect(pluginSdkSubpaths).not.toContain("tlon"); + expect(pluginSdkSubpaths).not.toContain("twitch"); expect(pluginSdkSubpaths).not.toContain("typing"); + expect(pluginSdkSubpaths).not.toContain("voice-call"); + expect(pluginSdkSubpaths).not.toContain("zalo"); + expect(pluginSdkSubpaths).not.toContain("zai"); + expect(pluginSdkSubpaths).not.toContain("zalouser"); expect(pluginSdkSubpaths).not.toContain("provider-model-definitions"); }); @@ -91,6 +116,13 @@ describe("plugin-sdk subpath exports", () => { expect(typeof accountHelpersSdk.createAccountListHelpers).toBe("function"); }); + it("exports device bootstrap helpers from the dedicated subpath", async () => { + const deviceBootstrapSdk = await import("openclaw/plugin-sdk/device-bootstrap"); + expect(typeof deviceBootstrapSdk.approveDevicePairing).toBe("function"); + expect(typeof deviceBootstrapSdk.issueDeviceBootstrapToken).toBe("function"); + expect(typeof deviceBootstrapSdk.listDevicePairing).toBe("function"); + }); + it("exports allowlist edit helpers from the dedicated subpath", () => { expect(typeof allowlistEditSdk.buildDmGroupAccountAllowlistAdapter).toBe("function"); expect(typeof allowlistEditSdk.createNestedAllowlistOverrideResolver).toBe("function"); @@ -139,6 +171,14 @@ describe("plugin-sdk subpath exports", () => { expect(typeof providerSetupSdk.discoverOpenAICompatibleSelfHostedProvider).toBe("function"); }); + it("exports oauth helpers from the dedicated provider oauth subpath", () => { + expect(typeof providerOauthSdk.buildOauthProviderAuthResult).toBe("function"); + expect(typeof providerOauthSdk.generatePkceVerifierChallenge).toBe("function"); + expect(typeof providerOauthSdk.toFormUrlEncoded).toBe("function"); + expect("buildOauthProviderAuthResult" in asExports(coreSdk)).toBe(false); + expect("buildOauthProviderAuthResult" in asExports(providerAuthSdk)).toBe(false); + }); + it("keeps provider models focused on shared provider primitives", () => { expect(typeof providerModelsSdk.applyOpenAIConfig).toBe("function"); expect(typeof providerModelsSdk.buildKilocodeModelDefinition).toBe("function"); @@ -187,8 +227,11 @@ describe("plugin-sdk subpath exports", () => { }); it("exports webhook ingress helpers from the dedicated subpath", () => { + expect(typeof webhookIngressSdk.registerPluginHttpRoute).toBe("function"); expect(typeof webhookIngressSdk.resolveWebhookPath).toBe("function"); + expect(typeof webhookIngressSdk.readRequestBodyWithLimit).toBe("function"); expect(typeof webhookIngressSdk.readJsonWebhookBodyOrReject).toBe("function"); + expect(typeof webhookIngressSdk.requestBodyErrorToText).toBe("function"); expect(typeof webhookIngressSdk.withResolvedWebhookRequestPipeline).toBe("function"); }); diff --git a/src/plugin-sdk/synology-chat.ts b/src/plugin-sdk/synology-chat.ts deleted file mode 100644 index 1b10e475f67..00000000000 --- a/src/plugin-sdk/synology-chat.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Narrow plugin-sdk surface for the bundled synology-chat plugin. -// Keep this list additive and scoped to symbols used under extensions/synology-chat. - -export { setAccountEnabledInConfigSection } from "../channels/plugins/config-helpers.js"; -export { buildChannelConfigSchema } from "../channels/plugins/config-schema.js"; -export type { ChannelSetupAdapter } from "../channels/plugins/types.adapters.js"; -export { - isRequestBodyLimitError, - readRequestBodyWithLimit, - requestBodyErrorToText, -} from "../infra/http-body.js"; -export { emptyPluginConfigSchema } from "../plugins/config-schema.js"; -export { registerPluginHttpRoute } from "../plugins/http-registry.js"; -export type { OpenClawConfig } from "../config/config.js"; -export type { PluginRuntime } from "../plugins/runtime/types.js"; -export type { OpenClawPluginApi } from "../plugins/types.js"; -export { DEFAULT_ACCOUNT_ID } from "../routing/session-key.js"; -export type { FixedWindowRateLimiter } from "./webhook-memory-guards.js"; -export { createFixedWindowRateLimiter } from "./webhook-memory-guards.js"; -export { - synologyChatSetupAdapter, - synologyChatSetupWizard, -} from "../../extensions/synology-chat/setup-api.js"; diff --git a/src/plugin-sdk/tlon.ts b/src/plugin-sdk/tlon.ts index da3803e612f..953a87ced2f 100644 --- a/src/plugin-sdk/tlon.ts +++ b/src/plugin-sdk/tlon.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled tlon plugin. +// Private helper surface for the bundled tlon plugin. // Keep this list additive and scoped to symbols used under extensions/tlon. import { createOptionalChannelSetupSurface } from "./channel-setup.js"; diff --git a/src/plugin-sdk/twitch.ts b/src/plugin-sdk/twitch.ts index 1194e9c55f5..440f33d15dc 100644 --- a/src/plugin-sdk/twitch.ts +++ b/src/plugin-sdk/twitch.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled twitch plugin. +// Private helper surface for the bundled twitch plugin. // Keep this list additive and scoped to symbols used under extensions/twitch. import { createOptionalChannelSetupSurface } from "./channel-setup.js"; diff --git a/src/plugin-sdk/voice-call.ts b/src/plugin-sdk/voice-call.ts index 8e61959187f..a278d645127 100644 --- a/src/plugin-sdk/voice-call.ts +++ b/src/plugin-sdk/voice-call.ts @@ -1,4 +1,4 @@ -// Public Voice Call plugin helpers. +// Private helper surface for the bundled voice-call plugin. // Keep this surface narrow and limited to the voice-call feature contract. export { definePluginEntry } from "./core.js"; diff --git a/src/plugin-sdk/webhook-ingress.ts b/src/plugin-sdk/webhook-ingress.ts index c76e986c050..88d71b18248 100644 --- a/src/plugin-sdk/webhook-ingress.ts +++ b/src/plugin-sdk/webhook-ingress.ts @@ -14,14 +14,18 @@ export { beginWebhookRequestPipelineOrReject, createWebhookInFlightLimiter, isJsonContentType, + isRequestBodyLimitError, + readRequestBodyWithLimit, readJsonWebhookBodyOrReject, readWebhookBodyOrReject, + requestBodyErrorToText, WEBHOOK_BODY_READ_DEFAULTS, WEBHOOK_IN_FLIGHT_DEFAULTS, type WebhookBodyReadProfile, type WebhookInFlightLimiter, } from "./webhook-request-guards.js"; export { + registerPluginHttpRoute, registerWebhookTarget, registerWebhookTargetWithPluginRoute, resolveSingleWebhookTarget, diff --git a/src/plugin-sdk/webhook-request-guards.ts b/src/plugin-sdk/webhook-request-guards.ts index f181859bc84..670e5b34565 100644 --- a/src/plugin-sdk/webhook-request-guards.ts +++ b/src/plugin-sdk/webhook-request-guards.ts @@ -10,6 +10,12 @@ import type { FixedWindowRateLimiter } from "./webhook-memory-guards.js"; export type WebhookBodyReadProfile = "pre-auth" | "post-auth"; +export { + isRequestBodyLimitError, + readRequestBodyWithLimit, + requestBodyErrorToText, +} from "../infra/http-body.js"; + export const WEBHOOK_BODY_READ_DEFAULTS = Object.freeze({ preAuth: { maxBytes: 64 * 1024, diff --git a/src/plugin-sdk/webhook-targets.ts b/src/plugin-sdk/webhook-targets.ts index e3dd9eda01d..43d67a93e27 100644 --- a/src/plugin-sdk/webhook-targets.ts +++ b/src/plugin-sdk/webhook-targets.ts @@ -19,6 +19,8 @@ export type RegisterWebhookTargetOptions = { type RegisterPluginHttpRouteParams = Parameters[0]; +export { registerPluginHttpRoute }; + export type RegisterWebhookPluginRouteOptions = Omit< RegisterPluginHttpRouteParams, "path" | "fallbackPath" diff --git a/src/plugin-sdk/zai.ts b/src/plugin-sdk/zai.ts index 87a745ee7d0..e52dcbb5b9b 100644 --- a/src/plugin-sdk/zai.ts +++ b/src/plugin-sdk/zai.ts @@ -1,4 +1,4 @@ -// Public Z.ai helpers for provider plugins that need endpoint detection. +// Private Z.ai helpers for bundled provider plugins that need endpoint detection. export { detectZaiEndpoint, diff --git a/src/plugin-sdk/zalo.ts b/src/plugin-sdk/zalo.ts index 0e1ff28cff0..6441ba0da81 100644 --- a/src/plugin-sdk/zalo.ts +++ b/src/plugin-sdk/zalo.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled zalo plugin. +// Private helper surface for the bundled zalo plugin. // Keep this list additive and scoped to symbols used under extensions/zalo. export { jsonResult, readStringParam } from "../agents/tools/common.js"; diff --git a/src/plugin-sdk/zalouser.ts b/src/plugin-sdk/zalouser.ts index e037c0b69ab..bb435627355 100644 --- a/src/plugin-sdk/zalouser.ts +++ b/src/plugin-sdk/zalouser.ts @@ -1,4 +1,4 @@ -// Narrow plugin-sdk surface for the bundled zalouser plugin. +// Private helper surface for the bundled zalouser plugin. // Keep this list additive and scoped to symbols used under extensions/zalouser. import { createOptionalChannelSetupSurface } from "./channel-setup.js"; diff --git a/src/plugins/contracts/runtime.contract.test.ts b/src/plugins/contracts/runtime.contract.test.ts index 1e614150cb3..551361d1bdd 100644 --- a/src/plugins/contracts/runtime.contract.test.ts +++ b/src/plugins/contracts/runtime.contract.test.ts @@ -23,8 +23,8 @@ vi.mock("@mariozechner/pi-ai/oauth", async () => { }; }); -vi.mock("../../plugin-sdk/qwen-portal-auth.js", async () => { - const actual = await vi.importActual("../../plugin-sdk/qwen-portal-auth.js"); +vi.mock("../../../extensions/qwen-portal-auth/refresh.js", async () => { + const actual = await vi.importActual("../../../extensions/qwen-portal-auth/refresh.js"); return { ...actual, refreshQwenPortalCredentials: refreshQwenPortalCredentialsMock, diff --git a/src/providers/qwen-portal-oauth.test.ts b/src/providers/qwen-portal-oauth.test.ts deleted file mode 100644 index 4e73062d8fe..00000000000 --- a/src/providers/qwen-portal-oauth.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { describe, expect, it, vi, afterEach } from "vitest"; -import { refreshQwenPortalCredentials } from "./qwen-portal-oauth.js"; - -const originalFetch = globalThis.fetch; - -afterEach(() => { - vi.unstubAllGlobals(); - globalThis.fetch = originalFetch; -}); - -describe("refreshQwenPortalCredentials", () => { - const expiredCredentials = () => ({ - access: "old-access", - refresh: "old-refresh", - expires: Date.now() - 1000, - }); - - const runRefresh = async () => await refreshQwenPortalCredentials(expiredCredentials()); - - const stubFetchResponse = (response: unknown) => { - const fetchSpy = vi.fn().mockResolvedValue(response); - vi.stubGlobal("fetch", fetchSpy); - return fetchSpy; - }; - - it("refreshes tokens with a new access token", async () => { - const fetchSpy = stubFetchResponse({ - ok: true, - status: 200, - json: async () => ({ - access_token: "new-access", - refresh_token: "new-refresh", - expires_in: 3600, - }), - }); - - const result = await runRefresh(); - - expect(fetchSpy).toHaveBeenCalledWith( - "https://chat.qwen.ai/api/v1/oauth2/token", - expect.objectContaining({ - method: "POST", - }), - ); - expect(result.access).toBe("new-access"); - expect(result.refresh).toBe("new-refresh"); - expect(result.expires).toBeGreaterThan(Date.now()); - }); - - it("keeps refresh token when refresh response omits it", async () => { - stubFetchResponse({ - ok: true, - status: 200, - json: async () => ({ - access_token: "new-access", - expires_in: 1800, - }), - }); - - const result = await runRefresh(); - - expect(result.refresh).toBe("old-refresh"); - }); - - it("keeps refresh token when response sends an empty refresh token", async () => { - stubFetchResponse({ - ok: true, - status: 200, - json: async () => ({ - access_token: "new-access", - refresh_token: "", - expires_in: 1800, - }), - }); - - const result = await runRefresh(); - - expect(result.refresh).toBe("old-refresh"); - }); - - it("errors when refresh response has invalid expires_in", async () => { - stubFetchResponse({ - ok: true, - status: 200, - json: async () => ({ - access_token: "new-access", - refresh_token: "new-refresh", - expires_in: 0, - }), - }); - - await expect(runRefresh()).rejects.toThrow( - "Qwen OAuth refresh response missing or invalid expires_in", - ); - }); - - it("errors when refresh token is invalid", async () => { - stubFetchResponse({ - ok: false, - status: 400, - text: async () => "invalid_grant", - }); - - await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh token expired or invalid"); - }); - - it("errors when refresh token is missing before any request", async () => { - await expect( - refreshQwenPortalCredentials({ - access: "old-access", - refresh: " ", - expires: Date.now() - 1000, - }), - ).rejects.toThrow("Qwen OAuth refresh token missing"); - }); - - it("errors when refresh response omits access token", async () => { - stubFetchResponse({ - ok: true, - status: 200, - json: async () => ({ - refresh_token: "new-refresh", - expires_in: 1800, - }), - }); - - await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh response missing access token"); - }); - - it("errors with server payload text for non-400 status", async () => { - stubFetchResponse({ - ok: false, - status: 500, - statusText: "Server Error", - text: async () => "gateway down", - }); - - await expect(runRefresh()).rejects.toThrow("Qwen OAuth refresh failed: gateway down"); - }); -});