Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/JAVA/Openclaw/extensions/whatsapp/src/   (KI Agentensystem Version 22©)  Datei vom 26.3.2026 mit Größe 13 kB image not shown  

Quelle  auth-store.ts

  Sprache: JAVA
 

import { randomUUID } from "node:crypto";
import fsSync from "node:fs";
import fs from "node:fs/promises";
import path from "node:path";
import { formatCliCommand } from "openclaw/plugin-sdk/cli-runtime";
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing";
import { info, success } from "openclaw/plugin-sdk/runtime-env";
import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
import { defaultRuntime, type RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
import { resolveOAuthDir } from "./auth-store.runtime.js";
import { hasWebCredsSync, resolveWebCredsBackupPath, resolveWebCredsPath } from "./creds-files.js";
import {
  waitForCredsSaveQueueWithTimeout,
  type CredsQueueWaitResult,
} from "./creds-persistence.js";
import { resolveComparableIdentity, type WhatsAppSelfIdentity } from "./identity.js";
import { resolveUserPath, type WebChannel } from "./text-runtime.js";
export { hasWebCredsSync, resolveWebCredsBackupPath, resolveWebCredsPath };

export const WHATSAPP_AUTH_UNSTABLE_CODE = "whatsapp-auth-unstable";

const authStoreLogger = getChildLogger({ module: "web-auth-store" });
const emptyWebSelfId = () => ({ e164: null, jid: null, lid: null }) as const;
export type WhatsAppWebAuthState = "linked" | "not-linked" | "unstable";

export class WhatsAppAuthUnstableError extends Error {
  readonly code = WHATSAPP_AUTH_UNSTABLE_CODE;

  constructor(message = "WhatsApp auth state is still stabilizing; retry shortly.") {
    super(message);
    this.name = "WhatsAppAuthUnstableError";
  }
}

export function resolveDefaultWebAuthDir(): string {
  return path.join(resolveOAuthDir(), "whatsapp", DEFAULT_ACCOUNT_ID);
}

export const WA_WEB_AUTH_DIR = resolveDefaultWebAuthDir();

export function readCredsJsonRaw(filePath: string): string | null {
  try {
    if (!fsSync.existsSync(filePath)) {
      return null;
    }
    const stats = fsSync.statSync(filePath);
    if (!stats.isFile() || stats.size <= 1) {
      return null;
    }
    return fsSync.readFileSync(filePath, "utf-8");
  } catch {
    return null;
  }
}

async function waitForWebAuthBarrier(
  authDir: string,
  context: string,
): Promise<CredsQueueWaitResult> {
  const result = await waitForCredsSaveQueueWithTimeout(authDir);
  if (result === "timed_out") {
    authStoreLogger.warn(
      {
        authDir,
        context,
      },
      "timed out waiting for queued WhatsApp creds save before auth read",
    );
  }
  return result;
}

export async function restoreCredsFromBackupIfNeeded(authDir: string): Promise<boolean{
  const logger = getChildLogger({ module: "web-session" });
  let tempRestorePath: string | null = null;
  try {
    const credsPath = resolveWebCredsPath(authDir);
    const backupPath = resolveWebCredsBackupPath(authDir);
    const raw = readCredsJsonRaw(credsPath);
    if (raw) {
      // Validate that creds.json is parseable.
      JSON.parse(raw);
      return false;
    }

    const backupRaw = readCredsJsonRaw(backupPath);
    if (!backupRaw) {
      return false;
    }
    const backupStats = await fs.lstat(backupPath).catch(() => null);
    if (!backupStats?.isFile()) {
      return false;
    }

    // Ensure backup is parseable before restoring.
    JSON.parse(backupRaw);
    tempRestorePath = path.join(authDir, `.creds.restore-${randomUUID()}.tmp`);
    await fs.writeFile(tempRestorePath, backupRaw, {
      encoding: "utf-8",
      mode: 0o600,
      flag: "wx",
    });
    await fs.rename(tempRestorePath, credsPath);
    tempRestorePath = null;
    logger.warn({ credsPath }, "restored corrupted WhatsApp creds.json from backup");
    return true;
  } catch {
    // ignore
  } finally {
    if (tempRestorePath) {
      await fs.rm(tempRestorePath, { force: true }).catch(() => {
        // best-effort temp cleanup
      });
    }
  }
  return false;
}

export async function webAuthExists(authDir: string = resolveDefaultWebAuthDir()) {
  const resolvedAuthDir = resolveUserPath(authDir);
  const credsPath = resolveWebCredsPath(resolvedAuthDir);
  try {
    await fs.access(resolvedAuthDir);
  } catch {
    return false;
  }
  try {
    const stats = await fs.stat(credsPath);
    if (!stats.isFile() || stats.size <= 1) {
      return false;
    }
    const raw = await fs.readFile(credsPath, "utf-8");
    JSON.parse(raw);
    return true;
  } catch {
    return false;
  }
}

function resolveWebAuthState(params: {
  linked: boolean;
  barrierResult: CredsQueueWaitResult;
}): WhatsAppWebAuthState {
  if (params.barrierResult === "timed_out") {
    return "unstable";
  }
  return params.linked ? "linked" : "not-linked";
}

async function readWebAuthStateCore(
  authDir: string,
  context: string,
): Promise<{ authDir: string; linked: boolean; state: WhatsAppWebAuthState }> {
  const resolvedAuthDir = resolveUserPath(authDir);
  const barrierResult = await waitForWebAuthBarrier(resolvedAuthDir, context);
  const linked = await webAuthExists(resolvedAuthDir);
  return {
    authDir: resolvedAuthDir,
    linked,
    state: resolveWebAuthState({ linked, barrierResult }),
  };
}

export function formatWhatsAppWebAuthStatusState(state: WhatsAppWebAuthState): string {
  switch (state) {
    case "linked":
      return "linked";
    case "not-linked":
      return "not linked";
    case "unstable":
      return "auth stabilizing";
  }
  const exhaustive: never = state;
  return exhaustive;
}

export async function readWebAuthState(
  authDir: string = resolveDefaultWebAuthDir(),
): Promise<WhatsAppWebAuthState> {
  return (await readWebAuthStateCore(authDir, "readWebAuthState")).state;
}

export async function readWebAuthSnapshot(authDir: string = resolveDefaultWebAuthDir()) {
  const auth = await readWebAuthStateCore(authDir, "readWebAuthSnapshot");
  return {
    state: auth.state,
    authAgeMs: auth.state === "linked" ? getWebAuthAgeMs(auth.authDir) : null,
    selfId: auth.state === "linked" ? readWebSelfId(auth.authDir) : emptyWebSelfId(),
  } as const;
}

export async function readWebAuthExistsBestEffort(authDir: string = resolveDefaultWebAuthDir()) {
  const state = await readWebAuthState(authDir);
  return {
    exists: state === "linked",
    timedOut: state === "unstable",
  } as const;
}

export async function readWebAuthExistsForDecision(
  authDir: string = resolveDefaultWebAuthDir(),
): Promise<{ outcome: "stable"; exists: boolean } | { outcome: "unstable" }> {
  const state = await readWebAuthState(authDir);
  if (state === "unstable") {
    return { outcome: "unstable" };
  }
  return {
    outcome: "stable",
    exists: state === "linked",
  };
}

export async function readWebAuthSnapshotBestEffort(authDir: string = resolveDefaultWebAuthDir()) {
  const snapshot = await readWebAuthSnapshot(authDir);
  return {
    linked: snapshot.state === "linked",
    timedOut: snapshot.state === "unstable",
    authAgeMs: snapshot.authAgeMs,
    selfId: snapshot.selfId,
  } as const;
}

async function clearLegacyBaileysAuthState(authDir: string) {
  const entries = await fs.readdir(authDir, { withFileTypes: true });
  const shouldDelete = (name: string) => {
    if (name === "oauth.json") {
      return false;
    }
    if (name === "creds.json" || name === "creds.json.bak") {
      return true;
    }
    if (!name.endsWith(".json")) {
      return false;
    }
    return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
  };
  await Promise.all(
    entries.map(async (entry) => {
      if (!entry.isFile()) {
        return;
      }
      if (!shouldDelete(entry.name)) {
        return;
      }
      await fs.rm(path.join(authDir, entry.name), { force: true });
    }),
  );
}

async function shouldClearOnLogout(authDir: string, isLegacyAuthDir: boolean): Promise<boolean> {
  try {
    const stats = await fs.stat(authDir);
    if (!stats.isDirectory()) {
      return true;
    }
    if (isLegacyAuthDir) {
      const entries = await fs.readdir(authDir, { withFileTypes: true });
      return entries.some((entry) => {
        if (!entry.isFile()) {
          return false;
        }
        if (entry.name === "oauth.json") {
          return false;
        }
        if (entry.name === "creds.json" || entry.name === "creds.json.bak") {
          return true;
        }
        return entry.name.endsWith(".json")
          ? /^(app-state-sync|session|sender-key|pre-key)-/.test(entry.name)
          : false;
      });
    }
    const credsStats = await fs.stat(resolveWebCredsPath(authDir)).catch(() => null);
    if (credsStats?.isFile()) {
      return true;
    }
    const backupStats = await fs.stat(resolveWebCredsBackupPath(authDir)).catch(() => null);
    return backupStats?.isFile() === true;
  } catch (error) {
    const codeValue =
      error && typeof error === "object" && "code" in error
        ? (error as { code?: unknown }).code
        : undefined;
    const code = typeof codeValue === "string" ? codeValue : "";
    return code !== "ENOENT";
  }
}

export async function logoutWeb(params: {
  authDir?: string;
  isLegacyAuthDir?: boolean;
  runtime?: RuntimeEnv;
}) {
  const runtime = params.runtime ?? defaultRuntime;
  const resolvedAuthDir = resolveUserPath(params.authDir ?? resolveDefaultWebAuthDir());
  const barrierResult = await waitForWebAuthBarrier(resolvedAuthDir, "logoutWeb");
  if (barrierResult === "timed_out") {
    runtime.log(
      info("WhatsApp auth state is still stabilizing; clearing cached credentials anyway."),
    );
  }
  if (!(await shouldClearOnLogout(resolvedAuthDir, Boolean(params.isLegacyAuthDir)))) {
    runtime.log(info("No WhatsApp Web session found; nothing to delete."));
    return false;
  }
  if (params.isLegacyAuthDir) {
    await clearLegacyBaileysAuthState(resolvedAuthDir);
  } else {
    await fs.rm(resolvedAuthDir, { recursive: true, force: true });
  }
  runtime.log(success("Cleared WhatsApp Web credentials."));
  return true;
}

export function readWebSelfId(authDir: string = resolveDefaultWebAuthDir()) {
  // Read the cached WhatsApp Web identity (jid + E.164) from disk if present.
  try {
    const credsPath = resolveWebCredsPath(resolveUserPath(authDir));
    if (!fsSync.existsSync(credsPath)) {
      return emptyWebSelfId();
    }
    const raw = fsSync.readFileSync(credsPath, "utf-8");
    const parsed = JSON.parse(raw) as { me?: { id?: string; lid?: string } } | undefined;
    const identity = resolveComparableIdentity(
      {
        jid: parsed?.me?.id ?? null,
        lid: parsed?.me?.lid ?? null,
      },
      authDir,
    );
    return {
      e164: identity.e164 ?? null,
      jid: identity.jid ?? null,
      lid: identity.lid ?? null,
    } as const;
  } catch {
    return emptyWebSelfId();
  }
}

export async function readWebSelfIdentity(
  authDir: string = resolveDefaultWebAuthDir(),
  fallback?: { id?: string | null; lid?: string | null } | null,
): Promise<WhatsAppSelfIdentity> {
  const resolvedAuthDir = resolveUserPath(authDir);
  try {
    const raw = await fs.readFile(resolveWebCredsPath(resolvedAuthDir), "utf-8");
    const parsed = JSON.parse(raw) as { me?: { id?: string; lid?: string } } | undefined;
    return resolveComparableIdentity(
      {
        jid: parsed?.me?.id ?? null,
        lid: parsed?.me?.lid ?? null,
      },
      resolvedAuthDir,
    );
  } catch {
    return resolveComparableIdentity(
      {
        jid: fallback?.id ?? null,
        lid: fallback?.lid ?? null,
      },
      resolvedAuthDir,
    );
  }
}

export async function readWebSelfIdentityForDecision(
  authDir: string = resolveDefaultWebAuthDir(),
  fallback?: { id?: string | null; lid?: string | null } | null,
): Promise<{ outcome: "stable"; identity: WhatsAppSelfIdentity } | { outcome: "unstable" }> {
  const resolvedAuthDir = resolveUserPath(authDir);
  const result = await waitForWebAuthBarrier(resolvedAuthDir, "readWebSelfIdentityForDecision");
  if (result === "timed_out") {
    return { outcome: "unstable" };
  }
  return {
    outcome: "stable",
    identity: await readWebSelfIdentity(resolvedAuthDir, fallback),
  };
}

/**
 * Return the age (in milliseconds) of the cached WhatsApp web auth state, or null when missing.
 * Helpful for heartbeats/observability to spot stale credentials.
 */

export function getWebAuthAgeMs(authDir: string = resolveDefaultWebAuthDir()): number | null {
  try {
    const stats = fsSync.statSync(resolveWebCredsPath(resolveUserPath(authDir)));
    return Date.now() - stats.mtimeMs;
  } catch {
    return null;
  }
}

export function logWebSelfId(
  authDir: string = resolveDefaultWebAuthDir(),
  runtime: RuntimeEnv = defaultRuntime,
  includeChannelPrefix = false,
) {
  // Human-friendly log of the currently linked personal web session.
  const { e164, jid, lid } = readWebSelfId(authDir);
  const parts = [jid ? `jid ${jid}` : null, lid ? `lid ${lid}` : null].filter(
    (value): value is string => Boolean(value),
  );
  const details =
    e164 || parts.length > 0
      ? `${e164 ?? "unknown"}${parts.length > 0 ? ` (${parts.join(", ")})` : ""}`
      : "unknown";
  const prefix = includeChannelPrefix ? "Web Channel: " : "";
  runtime.log(info(`${prefix}${details}`));
}

export async function pickWebChannel(
  pref: WebChannel | "auto",
  authDir: string = resolveDefaultWebAuthDir(),
): Promise<WebChannel> {
  const choice: WebChannel = pref === "auto" ? "web" : pref;
  const auth = await readWebAuthExistsForDecision(authDir);
  if (auth.outcome === "unstable") {
    throw new WhatsAppAuthUnstableError();
  }
  if (!auth.exists) {
    throw new Error(
      `No WhatsApp Web session found. Run \`${formatCliCommand("openclaw channels login --channel whatsapp --verbose")}\` to link.`,
    );
  }
  return choice;
}

Messung V0.5 in Prozent
C=96 H=98 G=96

¤ Dauer der Verarbeitung: 0.1 Sekunden  (vorverarbeitet am  2026-05-26) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.