Files
openclaw/extensions/msteams/src/token.ts
Peter Steinberger 538605ff44 [codex] Extract filesystem safety primitives (#77918)
* 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
2026-05-06 02:15:17 +01:00

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;
}
}