diff --git a/extensions/googlechat/src/accounts.ts b/extensions/googlechat/src/accounts.ts index dd7e78110f3..4bf9112cc32 100644 --- a/extensions/googlechat/src/accounts.ts +++ b/extensions/googlechat/src/accounts.ts @@ -8,6 +8,7 @@ import { } from "openclaw/plugin-sdk/account-resolution"; import { safeParseJsonWithSchema, safeParseWithSchema } from "openclaw/plugin-sdk/extension-shared"; import { isSecretRef } from "openclaw/plugin-sdk/secret-input"; +import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { z } from "zod"; import type { GoogleChatAccountConfig } from "./types.config.js"; @@ -27,14 +28,6 @@ const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT"; const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"; const JsonRecordSchema = z.record(z.string(), z.unknown()); -function normalizeOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - const { listAccountIds: listGoogleChatAccountIds, resolveDefaultAccountId: resolveDefaultGoogleChatAccountId, diff --git a/extensions/googlechat/src/auth.ts b/extensions/googlechat/src/auth.ts index 6454e88bda1..1f1f6968069 100644 --- a/extensions/googlechat/src/auth.ts +++ b/extensions/googlechat/src/auth.ts @@ -1,4 +1,5 @@ import { GoogleAuth, OAuth2Client } from "google-auth-library"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import { fetchWithSsrFGuard } from "../runtime-api.js"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; @@ -16,10 +17,6 @@ const verifyClient = new OAuth2Client(); let cachedCerts: { fetchedAt: number; certs: Record } | null = null; -function normalizeLowercaseStringOrEmpty(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} - function buildAuthKey(account: ResolvedGoogleChatAccount): string { if (account.credentialsFile) { return `file:${account.credentialsFile}`; diff --git a/extensions/googlechat/src/monitor-webhook.ts b/extensions/googlechat/src/monitor-webhook.ts index b5020d8a27e..cf382d29672 100644 --- a/extensions/googlechat/src/monitor-webhook.ts +++ b/extensions/googlechat/src/monitor-webhook.ts @@ -1,4 +1,5 @@ import type { IncomingMessage, ServerResponse } from "node:http"; +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { WebhookInFlightLimiter } from "openclaw/plugin-sdk/webhook-request-guards"; import { readJsonWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards"; import { @@ -14,10 +15,6 @@ import type { GoogleChatUser, } from "./types.js"; -function normalizeLowercaseStringOrEmpty(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} - function extractBearerToken(header: unknown): string { const authHeader = Array.isArray(header) ? typeof header[0] === "string" @@ -104,6 +101,19 @@ function parseGoogleChatInboundPayload( return { ok: true, event, addOnBearerToken }; } +async function isAuthorizedGoogleChatTarget( + target: WebhookTarget, + bearer: string, +): Promise { + const verification = await verifyGoogleChatRequest({ + bearer, + audienceType: target.audienceType, + audience: target.audience, + expectedAddOnPrincipal: target.account.config.appPrincipal, + }); + return verification.ok; +} + export function createGoogleChatWebhookRequestHandler(params: { webhookTargets: Map; webhookInFlightLimiter: WebhookInFlightLimiter; @@ -149,15 +159,7 @@ export function createGoogleChatWebhookRequestHandler(params: { selectedTarget = await resolveWebhookTargetWithAuthOrReject({ targets, res, - isMatch: async (target) => { - const verification = await verifyGoogleChatRequest({ - bearer: headerBearer, - audienceType: target.audienceType, - audience: target.audience, - expectedAddOnPrincipal: target.account.config.appPrincipal, - }); - return verification.ok; - }, + isMatch: (target) => isAuthorizedGoogleChatTarget(target, headerBearer), }); if (!selectedTarget) { return true; @@ -184,15 +186,7 @@ export function createGoogleChatWebhookRequestHandler(params: { selectedTarget = await resolveWebhookTargetWithAuthOrReject({ targets, res, - isMatch: async (target) => { - const verification = await verifyGoogleChatRequest({ - bearer: parsed.addOnBearerToken, - audienceType: target.audienceType, - audience: target.audience, - expectedAddOnPrincipal: target.account.config.appPrincipal, - }); - return verification.ok; - }, + isMatch: (target) => isAuthorizedGoogleChatTarget(target, parsed.addOnBearerToken), }); if (!selectedTarget) { return true; diff --git a/extensions/googlechat/src/sender-allow.ts b/extensions/googlechat/src/sender-allow.ts index 037527010a9..5aa09dd6f8d 100644 --- a/extensions/googlechat/src/sender-allow.ts +++ b/extensions/googlechat/src/sender-allow.ts @@ -1,6 +1,4 @@ -function normalizeLowercaseStringOrEmpty(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; function normalizeUserId(raw?: string | null): string { const trimmed = typeof raw === "string" ? raw.trim() : ""; diff --git a/extensions/googlechat/src/setup-surface.ts b/extensions/googlechat/src/setup-surface.ts index 0681211a48e..5161efda599 100644 --- a/extensions/googlechat/src/setup-surface.ts +++ b/extensions/googlechat/src/setup-surface.ts @@ -11,6 +11,10 @@ import { type ChannelSetupDmPolicy, type ChannelSetupWizard, } from "openclaw/plugin-sdk/setup"; +import { + normalizeOptionalString, + normalizeStringifiedOptionalString, +} from "openclaw/plugin-sdk/text-runtime"; import { resolveDefaultGoogleChatAccountId, resolveGoogleChatAccount } from "./accounts.js"; const channel = "googlechat" as const; @@ -19,23 +23,8 @@ const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE"; const USE_ENV_FLAG = "__googlechatUseEnv"; const AUTH_METHOD_FLAG = "__googlechatAuthMethod"; -function normalizeOptionalString(value: unknown): string | undefined { - if (typeof value !== "string") { - return undefined; - } - const trimmed = value.trim(); - return trimmed ? trimmed : undefined; -} - -function normalizeStringifiedOptionalString(value: unknown): string | undefined { - if (typeof value === "string") { - return normalizeOptionalString(value); - } - if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") { - return normalizeOptionalString(String(value)); - } - return undefined; -} +type GoogleChatTextInput = NonNullable[number]; +type GoogleChatTextInputKey = GoogleChatTextInput["inputKey"]; const promptAllowFrom = createPromptParsedAllowFromForAccount({ defaultAccountId: resolveDefaultGoogleChatAccountId, @@ -104,6 +93,32 @@ const googlechatDmPolicy: ChannelSetupDmPolicy = { export { googlechatSetupAdapter } from "./setup-core.js"; +function createServiceAccountTextInput(params: { + inputKey: GoogleChatTextInputKey; + message: string; + placeholder: string; + authMethod: "file" | "inline"; + patchKey: "serviceAccountFile" | "serviceAccount"; +}): GoogleChatTextInput { + return { + inputKey: params.inputKey, + message: params.message, + placeholder: params.placeholder, + shouldPrompt: ({ credentialValues }) => + credentialValues[USE_ENV_FLAG] !== "1" && + credentialValues[AUTH_METHOD_FLAG] === params.authMethod, + validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), + normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", + applySet: async ({ cfg, accountId, value }) => + applySetupAccountConfigPatch({ + cfg, + channelKey: channel, + accountId, + patch: { [params.patchKey]: value }, + }), + }; +} + export const googlechatSetupWizard: ChannelSetupWizard = { channel, status: createStandardChannelSetupStatus({ @@ -169,38 +184,20 @@ export const googlechatSetupWizard: ChannelSetupWizard = { }, credentials: [], textInputs: [ - { + createServiceAccountTextInput({ inputKey: "tokenFile", message: "Service account JSON path", placeholder: "/path/to/service-account.json", - shouldPrompt: ({ credentialValues }) => - credentialValues[USE_ENV_FLAG] !== "1" && credentialValues[AUTH_METHOD_FLAG] === "file", - validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), - normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", - applySet: async ({ cfg, accountId, value }) => - applySetupAccountConfigPatch({ - cfg, - channelKey: channel, - accountId, - patch: { serviceAccountFile: value }, - }), - }, - { + authMethod: "file", + patchKey: "serviceAccountFile", + }), + createServiceAccountTextInput({ inputKey: "token", message: "Service account JSON (single line)", placeholder: '{"type":"service_account", ... }', - shouldPrompt: ({ credentialValues }) => - credentialValues[USE_ENV_FLAG] !== "1" && credentialValues[AUTH_METHOD_FLAG] === "inline", - validate: ({ value }) => (normalizeStringifiedOptionalString(value) ? undefined : "Required"), - normalizeValue: ({ value }) => normalizeStringifiedOptionalString(value) ?? "", - applySet: async ({ cfg, accountId, value }) => - applySetupAccountConfigPatch({ - cfg, - channelKey: channel, - accountId, - patch: { serviceAccount: value }, - }), - }, + authMethod: "inline", + patchKey: "serviceAccount", + }), ], finalize: async ({ cfg, accountId, prompter }) => { const account = resolveGoogleChatAccount({ diff --git a/extensions/googlechat/src/targets.ts b/extensions/googlechat/src/targets.ts index 64b4b599a7e..cf092cfab4d 100644 --- a/extensions/googlechat/src/targets.ts +++ b/extensions/googlechat/src/targets.ts @@ -1,10 +1,7 @@ +import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; import { findGoogleChatDirectMessage } from "./api.js"; -function normalizeLowercaseStringOrEmpty(value: unknown): string { - return typeof value === "string" ? value.trim().toLowerCase() : ""; -} - export function normalizeGoogleChatTarget(raw?: string | null): string | undefined { const trimmed = raw?.trim(); if (!trimmed) {