Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from bundle
Free Telegram /setup command for OpenClaw Codex auth: OAuth via pasted redirect/code, token fallback, status, and safe config patching.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
assets/tg-auth-setup-plugin/index.js
1import fs from "node:fs/promises";2import os from "node:os";3import path from "node:path";4import { pathToFileURL } from "node:url";5import { execFileSync } from "node:child_process";67const PROFILE_ID = "openai-codex:default";8const PROVIDER_ID = "openai-codex";9const DEFAULT_MODEL = "openai-codex/gpt-5.5";10const OAUTH_TIMEOUT_MS = 10 * 60 * 1000;11const QUICK_WAIT_MS = 25 * 1000;1213const oauthBySender = new Map();1415function normalizeText(value) {16return String(value || "").trim();17}1819function sleep(ms) {20return new Promise((resolve) => setTimeout(resolve, ms));21}2223function senderKey(ctx) {24const channel = String(ctx.channelId || ctx.channel || "unknown");25const sender = String(ctx.senderId || ctx.from || ctx.to || "unknown");26return `${channel}:${sender}`;27}2829function maskToken(token) {30const clean = String(token || "").trim();31if (clean.length <= 8) {32return "********";33}34return `${clean.slice(0, 4)}...${clean.slice(-4)}`;35}3637function sanitizeToken(raw) {38return String(raw || "").replace(/\s+/g, "").trim();39}4041function resolveAuthPath() {42const explicit = process.env.OPENCLAW_AGENT_DIR;43if (explicit && explicit.trim()) {44return path.join(explicit, "auth-profiles.json");45}46return path.join(os.homedir(), ".openclaw", "agents", "main", "agent", "auth-profiles.json");47}4849async function loadStore(authPath) {50try {51const raw = await fs.readFile(authPath, "utf8");52const parsed = JSON.parse(raw);53if (!parsed || typeof parsed !== "object") {54return { version: 1, profiles: {} };55}56const version =57Number.isFinite(parsed.version) && Number(parsed.version) > 0 ? Number(parsed.version) : 1;58const profiles =59parsed.profiles && typeof parsed.profiles === "object" ? { ...parsed.profiles } : {};60return {61...parsed,62version,63profiles,64};65} catch {66return { version: 1, profiles: {} };67}68}6970async function writeStore(authPath, store) {71const dir = path.dirname(authPath);72await fs.mkdir(dir, { recursive: true, mode: 0o700 });73await fs.chmod(dir, 0o700).catch(() => {});74const tmp = `${authPath}.tmp-${process.pid}-${Date.now()}`;75const body = `${JSON.stringify(store, null, 2)}\n`;76await fs.writeFile(tmp, body, { mode: 0o600 });77await fs.rename(tmp, authPath);78await fs.chmod(authPath, 0o600).catch(() => {});79}8081async function saveCodexToken(token) {82const authPath = resolveAuthPath();83const store = await loadStore(authPath);84store.version = store.version || 1;85store.profiles = store.profiles || {};86store.profiles[PROFILE_ID] = {87type: "token",88provider: PROVIDER_ID,89token,90};91await writeStore(authPath, store);92return authPath;93}9495async function saveCodexOauth(creds) {96const authPath = resolveAuthPath();97const store = await loadStore(authPath);98store.version = store.version || 1;99store.profiles = store.profiles || {};100store.profiles[PROFILE_ID] = {101type: "oauth",102provider: PROVIDER_ID,103access: creds.access,104refresh: creds.refresh,105expires: creds.expires,106...(creds.accountId ? { accountId: creds.accountId } : {}),107};108await writeStore(authPath, store);109return authPath;110}111112async function patchConfig(api, mode) {113const cfg = api.runtime.config.loadConfig();114const next = { ...(cfg || {}) };115next.auth = next.auth || {};116next.auth.profiles = next.auth.profiles || {};117next.auth.profiles[PROFILE_ID] = {118provider: PROVIDER_ID,119mode,120};121122next.agents = next.agents || {};123next.agents.defaults = next.agents.defaults || {};124next.agents.defaults.model = next.agents.defaults.model || {};125if (!next.agents.defaults.model.primary) {126next.agents.defaults.model.primary = DEFAULT_MODEL;127}128129await api.runtime.config.writeConfigFile(next);130}131132async function loadLoginOpenAICodex() {133const prefixes = new Set();134const execPrefix = path.dirname(path.dirname(process.execPath || ""));135if (execPrefix) {136prefixes.add(execPrefix);137}138prefixes.add("/usr");139prefixes.add("/usr/local");140const home = os.homedir();141if (home && process.version) {142prefixes.add(path.join(home, ".nvm", "versions", "node", process.version));143}144if (process.env.npm_config_prefix) {145prefixes.add(process.env.npm_config_prefix);146}147148const packageJsonCandidates = new Set();149const addOpenClawPackageCandidate = (candidate) => {150if (candidate && candidate.trim()) {151packageJsonCandidates.add(candidate);152}153};154155const addOpenClawCandidateFromBinary = (binaryPath) => {156if (!binaryPath) {157return;158}159const rootDir = path.resolve(binaryPath, "..", "..", "lib", "node_modules", "openclaw");160addOpenClawPackageCandidate(path.join(rootDir, "package.json"));161};162163for (const prefix of prefixes) {164addOpenClawPackageCandidate(path.join(prefix, "lib", "node_modules", "openclaw", "package.json"));165}166167const pathDirs = String(process.env.PATH || "")168.split(path.delimiter)169.map((value) => value.trim())170.filter(Boolean);171for (const dir of pathDirs) {172addOpenClawCandidateFromBinary(path.join(dir, "openclaw"));173addOpenClawCandidateFromBinary(path.join(dir, "openclaw-gateway"));174}175176try {177const npmRoot = execFileSync("npm", ["root", "-g"], {178encoding: "utf8",179stdio: ["ignore", "pipe", "ignore"],180}).trim();181if (npmRoot) {182addOpenClawPackageCandidate(path.join(npmRoot, "openclaw", "package.json"));183}184} catch {185// ignore npm lookup failures and fall back to static candidates186}187188for (const openclawPackageJson of packageJsonCandidates) {189try {190await fs.access(openclawPackageJson);191const openclawRoot = path.dirname(openclawPackageJson);192const directCandidates = [193path.join(openclawRoot, "node_modules", "@mariozechner", "pi-ai", "dist", "oauth.js"),194path.join(openclawRoot, "node_modules", "@mariozechner", "pi-ai", "dist", "index.js"),195];196for (const candidate of directCandidates) {197try {198await fs.access(candidate);199const mod = await import(pathToFileURL(candidate).toString());200if (mod && typeof mod.loginOpenAICodex === "function") {201return mod.loginOpenAICodex;202}203} catch {204// try next candidate205}206}207} catch {208// try next candidate209}210}211212const candidates = ["@mariozechner/pi-ai"];213for (const prefix of prefixes) {214candidates.push(215path.join(prefix, "lib", "node_modules", "@mariozechner", "pi-ai", "dist", "oauth.js"),216path.join(217prefix,218"lib",219"node_modules",220"@mariozechner",221"pi-ai",222"dist",223"utils",224"oauth",225"index.js",226),227path.join(prefix, "lib", "node_modules", "@mariozechner", "pi-ai", "dist", "index.js"),228path.join(229prefix,230"lib",231"node_modules",232"openclaw",233"node_modules",234"@mariozechner",235"pi-ai",236"dist",237"oauth.js",238),239path.join(240prefix,241"lib",242"node_modules",243"openclaw",244"node_modules",245"@mariozechner",246"pi-ai",247"dist",248"utils",249"oauth",250"index.js",251),252path.join(253prefix,254"lib",255"node_modules",256"openclaw",257"node_modules",258"@mariozechner",259"pi-ai",260"dist",261"index.js",262),263);264}265candidates.push(266"/usr/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/dist/oauth.js",267"/usr/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/dist/utils/oauth/index.js",268"/usr/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/dist/index.js",269"/usr/local/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/dist/oauth.js",270"/usr/local/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/dist/utils/oauth/index.js",271"/usr/local/lib/node_modules/openclaw/node_modules/@mariozechner/pi-ai/dist/index.js",272);273274for (const candidate of candidates) {275try {276const spec =277candidate.startsWith("/") ? pathToFileURL(candidate).toString() : candidate;278const mod = await import(spec);279if (mod && typeof mod.loginOpenAICodex === "function") {280return mod.loginOpenAICodex;281}282} catch {283// try next candidate284}285}286throw new Error("Cannot load loginOpenAICodex from pi-ai package.");287}288289function usage() {290return [291"Usage:",292"/setup",293"/setup status",294"/setup oauth",295"/setup code <redirect_url_or_code>",296"/setup cancel",297"/cancel (while OAuth is waiting)",298"/setup codex <token>",299"/setup <token>",300"",301"OAuth flow from Telegram:",302"1) /setup",303"2) Open URL in browser, sign in",304"3) Copy full redirect URL (or code) and send it as a normal message",305"4) Optional: /setup code <value>",306"5) Cancel anytime with /cancel",307].join("\n");308}309310function buildOauthPromptText(session) {311return [312"Відкрий це посилання у локальному браузері:",313session.authUrl,314"",315"Після логіну просто надішли сюди ПОВНИЙ redirect URL або code звичайним повідомленням.",316"Щоб скасувати, надішли /cancel",317].join("\n");318}319320function getActiveOauthSession(key) {321const session = oauthBySender.get(key);322if (!session) {323return null;324}325if (session.status === "starting" || session.status === "waiting") {326return session;327}328return null;329}330331function cancelOauthSession(key) {332const session = oauthBySender.get(key);333if (!session) {334return { text: "Немає активної OAuth-сесії." };335}336session.status = "cancelled";337if (session.rejectInput) {338session.rejectInput(new Error("Cancelled by user."));339}340oauthBySender.delete(key);341return { text: "OAuth-сесію скасовано." };342}343344async function finishOauthInput(key, rawValue) {345const value = normalizeText(rawValue);346if (!value) {347return { text: "Надішли повний redirect URL або code." };348}349350const session = oauthBySender.get(key);351if (!session) {352return { text: "Немає активної OAuth-сесії. Почни з /setup" };353}354355if (session.resolveInput) {356session.resolveInput(value);357} else {358session.pendingInput = value;359}360361await Promise.race([session.runner, sleep(QUICK_WAIT_MS)]);362if (session.status === "done") {363const text = session.resultText || "OAuth completed.";364oauthBySender.delete(key);365return { text };366}367if (session.status === "failed") {368const error = session.lastError || "unknown error";369oauthBySender.delete(key);370return { text: `OAuth failed: ${error}` };371}372return { text: "Код отримала. Доробляю OAuth, перевір /setup status за кілька секунд." };373}374375async function startOauthFlow(api, key) {376const existing = getActiveOauthSession(key);377if (existing) {378if (existing.authUrl) {379return { text: buildOauthPromptText(existing) };380}381return {382text: "OAuth уже запускається. Якщо хочеш зупинити, надішли /cancel",383};384}385386const session = {387status: "starting",388authUrl: null,389instructions: null,390waitingPrompt: null,391resolveInput: null,392rejectInput: null,393pendingInput: null,394lastError: null,395runner: null,396};397oauthBySender.set(key, session);398399const loginOpenAICodex = await loadLoginOpenAICodex();400401let authReadyResolve;402const authReady = new Promise((resolve) => {403authReadyResolve = resolve;404});405406session.runner = (async () => {407try {408const creds = await loginOpenAICodex({409onAuth: (info) => {410session.authUrl = info?.url || null;411session.instructions = info?.instructions || null;412session.status = "waiting";413authReadyResolve?.();414},415onPrompt: async (prompt) => {416session.status = "waiting";417return await waitForCodeInput(session, prompt?.message || null);418},419onManualCodeInput: async () => {420session.status = "waiting";421return await waitForCodeInput(session, null);422},423onProgress: () => {},424});425426const authPath = await saveCodexOauth(creds);427await patchConfig(api, "oauth");428session.status = "done";429session.lastError = null;430session.resultText =431"OAuth completed.\n" +432`Profile: ${PROFILE_ID}\n` +433`Path: ${authPath}`;434} catch (err) {435session.status = "failed";436session.lastError = String(err?.message || err || "unknown error");437}438})();439440await Promise.race([authReady, sleep(5000)]);441if (!session.authUrl) {442if (session.status === "failed") {443const error = session.lastError || "unknown error";444oauthBySender.delete(key);445return { text: `Не вдалося запустити OAuth: ${error}` };446}447return {448text: "OAuth ще запускається. Якщо зависне, спробуй /setup ще раз або надішли /cancel",449};450}451return { text: buildOauthPromptText(session) };452}453454function extractCodeFromMessage(rawText) {455const text = normalizeText(rawText);456if (!text) {457return { kind: "empty" };458}459if (text === "/cancel" || text === "cancel") {460return { kind: "cancel" };461}462if (/^\/setup\s+cancel$/i.test(text)) {463return { kind: "cancel" };464}465const codeMatch = text.match(/^\/setup\s+code\s+([\s\S]+)$/i);466if (codeMatch) {467return { kind: "value", value: normalizeText(codeMatch[1]) };468}469if (/^\/setup(?:\s+oauth|\s+login)?$/i.test(text)) {470return { kind: "repeat-setup" };471}472if (text.startsWith("/")) {473return { kind: "other-command" };474}475return { kind: "value", value: text };476}477478async function handleCapturedOauthMessage(event, ctx) {479const key = senderKey({ channelId: ctx.channelId || event.channel, senderId: ctx.senderId });480const session = getActiveOauthSession(key);481if (!session) {482return null;483}484485const parsed = extractCodeFromMessage(event.body || event.content);486if (parsed.kind === "cancel") {487return { handled: true, text: cancelOauthSession(key).text };488}489if (parsed.kind === "repeat-setup") {490return {491handled: true,492text: session.authUrl493? buildOauthPromptText(session)494: "OAuth уже запускається. Якщо хочеш скасувати, надішли /cancel",495};496}497if (parsed.kind === "other-command") {498return {499handled: true,500text:501"Зараз чекаю redirect URL/code для OAuth. Просто надішли його сюди звичайним повідомленням або надішли /cancel",502};503}504if (parsed.kind === "empty") {505return {506handled: true,507text: "Надішли повний redirect URL або code. Щоб скасувати, надішли /cancel",508};509}510511const result = await finishOauthInput(key, parsed.value);512return { handled: true, text: result.text };513}514515function waitForCodeInput(session, promptText) {516if (session.pendingInput) {517const value = session.pendingInput;518session.pendingInput = null;519return Promise.resolve(value);520}521return new Promise((resolve, reject) => {522session.waitingPrompt = promptText || "Paste redirect URL/code via /setup code <value>";523session.resolveInput = (value) => {524session.resolveInput = null;525session.rejectInput = null;526session.waitingPrompt = null;527resolve(value);528};529session.rejectInput = (err) => {530session.resolveInput = null;531session.rejectInput = null;532session.waitingPrompt = null;533reject(err);534};535setTimeout(() => {536if (session.rejectInput) {537session.rejectInput(new Error("Timed out waiting for /setup code."));538}539}, OAUTH_TIMEOUT_MS);540});541}542543export default function register(api) {544api.on("before_dispatch", async (event, ctx) => {545return await handleCapturedOauthMessage(event, ctx);546});547548api.registerCommand({549name: "setup",550description: "Owner-only setup helpers for OpenAI Codex auth.",551acceptsArgs: true,552requireAuth: true,553handler: a