mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-14 22:10:45 +00:00
* refactor: extract filesystem safety primitives * refactor: use fs-safe for file access helpers * refactor: reuse fs-safe for media reads * refactor: use fs-safe for image reads * refactor: reuse fs-safe in qqbot media opener * refactor: reuse fs-safe for local media checks * refactor: consume cleaner fs-safe api * refactor: align fs-safe json option names * fix: preserve fs-safe migration contracts * refactor: use fs-safe primitive subpaths * refactor: use grouped fs-safe subpaths * refactor: align fs-safe api usage * refactor: adapt private state store api * chore: refresh proof gate * refactor: follow fs-safe json api split * refactor: follow reduced fs-safe surface * build: default fs-safe python helper off * fix: preserve fs-safe plugin sdk aliases * refactor: consolidate fs-safe usage * refactor: unify fs-safe store usage * refactor: trim fs-safe temp workspace usage * refactor: hide low-level fs-safe primitives * build: use published fs-safe package * fix: preserve outbound recovery durability after rebase * chore: refresh pr checks
195 lines
6.2 KiB
TypeScript
195 lines
6.2 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { basename, dirname } from "node:path";
|
|
import { privateFileStoreSync } from "openclaw/plugin-sdk/security-runtime";
|
|
import type { MSTeamsConfig } from "../runtime-api.js";
|
|
import type { MSTeamsDelegatedTokens } from "./oauth.shared.js";
|
|
import { refreshMSTeamsDelegatedTokens } from "./oauth.token.js";
|
|
import {
|
|
hasConfiguredSecretInput,
|
|
normalizeResolvedSecretInputString,
|
|
normalizeSecretInputString,
|
|
} from "./secret-input.js";
|
|
import { resolveMSTeamsStorePath } from "./storage.js";
|
|
|
|
// ── Credential types ───────────────────────────────────────────────────────
|
|
|
|
export type MSTeamsSecretCredentials = {
|
|
type: "secret";
|
|
appId: string;
|
|
appPassword: string;
|
|
tenantId: string;
|
|
};
|
|
|
|
export type MSTeamsFederatedCredentials = {
|
|
type: "federated";
|
|
appId: string;
|
|
tenantId: string;
|
|
certificatePath?: string;
|
|
certificateThumbprint?: string;
|
|
useManagedIdentity?: boolean;
|
|
managedIdentityClientId?: string;
|
|
};
|
|
|
|
export type MSTeamsCredentials = MSTeamsSecretCredentials | MSTeamsFederatedCredentials;
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function resolveAuthType(cfg?: MSTeamsConfig): "secret" | "federated" {
|
|
const fromCfg = cfg?.authType;
|
|
if (fromCfg === "secret" || fromCfg === "federated") {
|
|
return fromCfg;
|
|
}
|
|
|
|
const fromEnv = process.env.MSTEAMS_AUTH_TYPE;
|
|
if (fromEnv === "federated") {
|
|
return "federated";
|
|
}
|
|
|
|
return "secret";
|
|
}
|
|
|
|
// ── hasConfiguredMSTeamsCredentials ────────────────────────────────────────
|
|
|
|
export function hasConfiguredMSTeamsCredentials(cfg?: MSTeamsConfig): boolean {
|
|
const authType = resolveAuthType(cfg);
|
|
|
|
const hasAppId = Boolean(
|
|
normalizeSecretInputString(cfg?.appId) ||
|
|
normalizeSecretInputString(process.env.MSTEAMS_APP_ID),
|
|
);
|
|
const hasTenantId = Boolean(
|
|
normalizeSecretInputString(cfg?.tenantId) ||
|
|
normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID),
|
|
);
|
|
|
|
if (authType === "federated") {
|
|
const hasCert = Boolean(cfg?.certificatePath || process.env.MSTEAMS_CERTIFICATE_PATH);
|
|
const hasManagedIdentity =
|
|
cfg?.useManagedIdentity ?? process.env.MSTEAMS_USE_MANAGED_IDENTITY === "true";
|
|
|
|
return hasAppId && hasTenantId && (hasCert || hasManagedIdentity);
|
|
}
|
|
|
|
// "secret" (default) — original logic
|
|
return Boolean(
|
|
normalizeSecretInputString(cfg?.appId) &&
|
|
hasConfiguredSecretInput(cfg?.appPassword) &&
|
|
normalizeSecretInputString(cfg?.tenantId),
|
|
);
|
|
}
|
|
|
|
// ── resolveMSTeamsCredentials ─────────────────────────────────────────────
|
|
|
|
export function resolveMSTeamsCredentials(cfg?: MSTeamsConfig): MSTeamsCredentials | undefined {
|
|
const authType = resolveAuthType(cfg);
|
|
|
|
const appId =
|
|
normalizeSecretInputString(cfg?.appId) ||
|
|
normalizeSecretInputString(process.env.MSTEAMS_APP_ID);
|
|
|
|
const tenantId =
|
|
normalizeSecretInputString(cfg?.tenantId) ||
|
|
normalizeSecretInputString(process.env.MSTEAMS_TENANT_ID);
|
|
|
|
if (!appId || !tenantId) {
|
|
return undefined;
|
|
}
|
|
|
|
if (authType === "federated") {
|
|
const certificatePath =
|
|
cfg?.certificatePath || process.env.MSTEAMS_CERTIFICATE_PATH || undefined;
|
|
|
|
const certificateThumbprint =
|
|
cfg?.certificateThumbprint || process.env.MSTEAMS_CERTIFICATE_THUMBPRINT || undefined;
|
|
|
|
const useManagedIdentity =
|
|
cfg?.useManagedIdentity ?? process.env.MSTEAMS_USE_MANAGED_IDENTITY === "true";
|
|
|
|
const managedIdentityClientId =
|
|
cfg?.managedIdentityClientId || process.env.MSTEAMS_MANAGED_IDENTITY_CLIENT_ID || undefined;
|
|
|
|
// At least one federated mechanism must be configured.
|
|
if (!certificatePath && !useManagedIdentity) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
type: "federated",
|
|
appId,
|
|
tenantId,
|
|
certificatePath,
|
|
certificateThumbprint,
|
|
useManagedIdentity: useManagedIdentity || undefined,
|
|
managedIdentityClientId,
|
|
};
|
|
}
|
|
|
|
// "secret" (default) — original logic
|
|
const appPassword =
|
|
normalizeResolvedSecretInputString({
|
|
value: cfg?.appPassword,
|
|
path: "channels.msteams.appPassword",
|
|
}) || normalizeSecretInputString(process.env.MSTEAMS_APP_PASSWORD);
|
|
|
|
if (!appPassword) {
|
|
return undefined;
|
|
}
|
|
|
|
return { type: "secret", appId, appPassword, tenantId };
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Delegated token storage / resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const DELEGATED_TOKEN_FILENAME = "msteams-delegated.json";
|
|
|
|
function resolveDelegatedTokenPath(): string {
|
|
return resolveMSTeamsStorePath({ filename: DELEGATED_TOKEN_FILENAME });
|
|
}
|
|
|
|
export function loadDelegatedTokens(): MSTeamsDelegatedTokens | undefined {
|
|
try {
|
|
const content = readFileSync(resolveDelegatedTokenPath(), "utf8");
|
|
return JSON.parse(content) as MSTeamsDelegatedTokens;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function saveDelegatedTokens(tokens: MSTeamsDelegatedTokens): void {
|
|
const tokenPath = resolveDelegatedTokenPath();
|
|
privateFileStoreSync(dirname(tokenPath)).writeJson(basename(tokenPath), tokens);
|
|
}
|
|
|
|
export async function resolveDelegatedAccessToken(params: {
|
|
tenantId: string;
|
|
clientId: string;
|
|
clientSecret: string;
|
|
}): Promise<string | undefined> {
|
|
const tokens = loadDelegatedTokens();
|
|
if (!tokens) {
|
|
return undefined;
|
|
}
|
|
|
|
// Token still valid (5-min buffer already baked into expiresAt)
|
|
if (tokens.expiresAt > Date.now()) {
|
|
return tokens.accessToken;
|
|
}
|
|
|
|
// Attempt refresh
|
|
try {
|
|
const refreshed = await refreshMSTeamsDelegatedTokens({
|
|
tenantId: params.tenantId,
|
|
clientId: params.clientId,
|
|
clientSecret: params.clientSecret,
|
|
refreshToken: tokens.refreshToken,
|
|
scopes: tokens.scopes,
|
|
});
|
|
saveDelegatedTokens(refreshed);
|
|
return refreshed.accessToken;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|