Skill: autogame-17/feishu-common (version 1.0.1)

# feishu-common Skill ## Description Shared Feishu authentication and API helper for OpenClaw Feishu skills. Provides: - Tenant token acquisition and cache - Retry and timeout handling - Authenticated request wrapper with token refresh ## Install Requirement Install this skill before installing or running dependent Feishu skills. ## Usage Dependent skills should import from `feishu-common`: ```javascript const { getToken, fetchWithRetry, fetchWithAuth } = require("../feishu-common/index.js"); ``` Compatibility alias is also available: ```javascript const { getToken, fetchWithAuth } = require("../feishu-common/feishu-client.js"); ``` ## Files - `index.js`: Main implementation. - `feishu-client.js`: Compatibility alias to `index.js`.

path: _meta.json
content: ownerId: kn7apafdj4thknczrgxdzfd2v1808svf
slug: feishu-common
version: 1.0.0
publishedAt: 1770892359246

path: feishu-client.js
content: module.exports = require("./index.js");


path: index.js
content: const fs = require('fs');
const path = require('path');
// const https = require('https'); // Unused
require('dotenv').config({ path: path.resolve(__dirname, '../../.env'), quiet: true });

const APP_ID = process.env.FEISHU_APP_ID;
const APP_SECRET = process.env.FEISHU_APP_SECRET;
const TOKEN_CACHE_FILE = path.resolve(__dirname, '../../memory/feishu_token.json');

// --- Upstream Logic Injection (Simplified) ---
// Upstream uses @larksuiteoapi/node-sdk but we are lightweight.
// We replicate the robustness, not the dependency.

/**
 * Robust Fetch with Retry (Exponential Backoff)
 */
async function fetchWithRetry(url, options = {}, retries = 3) {
    const timeoutMs = options.timeout || 15000;
    
    for (let i = 0; i < retries; i++) {
        let timeoutId;
        try {
            const controller = new AbortController();
            timeoutId = setTimeout(() => controller.abort(), timeoutMs);
            const fetchOptions = { ...options, signal: controller.signal };
            delete fetchOptions.timeout; 

            const res = await fetch(url, fetchOptions);
            clearTimeout(timeoutId);

            if (!res.ok) {
                // Rate Limiting (429)
                if (res.status === 429) {
                    const retryAfter = res.headers.get('Retry-After');
                    let waitMs = 1000 * Math.pow(2, i);
                    if (retryAfter) waitMs = parseInt(retryAfter, 10) * 1000;
                    console.warn(`[FeishuClient] Rate limited. Waiting ${waitMs}ms...`);
                    await new Promise(r => setTimeout(r, waitMs));
                    continue; 
                }
                
                // Do not retry 4xx errors (except 429), usually auth or param errors
                if (res.status >= 400 && res.status < 500) {
                    const errBody = await res.text();
                    throw new Error(`HTTP ${res.status} [${url}]: ${errBody}`);
                }
                throw new Error(`HTTP ${res.status} ${res.statusText} [${url}]`);
            }
            return res;
        } catch (e) {
            if (timeoutId) clearTimeout(timeoutId);
            if (e.name === 'AbortError') e.message = `Timeout (${timeoutMs}ms) [${url}]`;
            
            // Don't retry if it's a permanent error
            if (e.message.includes('HTTP 4') && !e.message.includes('429')) throw e;
            
            if (i === retries - 1) throw e;
            const delay = 1000 * Math.pow(2, i);
            console.warn(`[FeishuClient] Fetch failed (${e.message}) [${url}]. Retrying in ${delay}ms...`);
            await new Promise(r => setTimeout(r, delay));
        }
    }
}

/**
 * Get Tenant Access Token (Cached)
 */
async function getToken(forceRefresh = false) {
    const now = Math.floor(Date.now() / 1000);

    if (!forceRefresh && fs.existsSync(TOKEN_CACHE_FILE)) {
        try {
            const cached = JSON.parse(fs.readFileSync(TOKEN_CACHE_FILE, 'utf8'));
            if (cached.token && cached.expire > now + 60) return cached.token;
        } catch (e) {}
    }

    try {
        const res = await fetchWithRetry('https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ app_id: APP_ID, app_secret: APP_SECRET })
        });
        const data = await res.json();

        if (data.code !== 0) throw new Error(`API Error: ${data.msg}`);
        
        try {
            const cacheData = { [REDACTED_SECRET], expire: now + data.expire };
            const cacheDir = path.dirname(TOKEN_CACHE_FILE);
            if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
            fs.writeFileSync(TOKEN_CACHE_FILE, JSON.stringify(cacheData, null, 2));
        } catch (e) {}

        return data.tenant_access_token;
    } catch (e) {
        console.error('[FeishuClient] Failed to get token:', e.message);
        throw e;
    }
}

/**
 * Authenticated Fetch with Auto-Refresh
 */
async function fetchWithAuth(url, options = {}) {
    let token = await getToken();
    let headers = { ...options.headers, 'Authorization': `Bearer ${token}` };
    
    try {
        let res = await fetchWithRetry(url, { ...options, headers });
        
        // Handle JSON Logic Errors (200 OK but code != 0)
        const clone = res.clone();
        try {
            const data = await clone.json();
            // Codes for invalid [REDACTED_SECRET], 99991664, 99991661, 99991668
            if ([99991663, 99991664, 99991661, 99991668].includes(data.code)) {
                throw new Error('TokenExpired');
            }
        } catch (jsonErr) {
            // If response isn't JSON or TokenExpired, ignore here
            if (jsonErr.message === 'TokenExpired') throw jsonErr;
        }
        
        return res;

    } catch (e) {
        if (e.message.includes('HTTP 401') || e.message === 'TokenExpired') {
            console.warn(`[FeishuClient] Token expired. Refreshing...`);
            token = await getToken(true);
            headers = { ...options.headers, 'Authorization': `Bearer ${token}` };
            return await fetchWithRetry(url, { ...options, headers });
        }
        throw e;
    }
}

module.exports = { getToken, fetchWithRetry, fetchWithAuth };


path: package-lock.json
content: {
  "name": "feishu-common",
  "version": "1.0.0",
  "lockfileVersion": 3,
  "requires": true,
  "packages": {
    "": {
      "name": "feishu-common",
      "version": "1.0.0",
      "license": "MIT",
      "dependencies": {
        "axios": "^1.6.0",
        "dotenv": "^16.3.1"
      }
    },
    "node_modules/asynckit": {
      "version": "0.4.0",
      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    },
    "node_modules/axios": {
      "version": "1.13.5",
      "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "follow-redirects": "^1.15.11",
        "form-data": "^4.0.5",
        "proxy-from-env": "^1.1.0"
      }
    },
    "node_modules/call-bind-apply-helpers": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/combined-stream": {
      "version": "1.0.8",
      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "delayed-stream": "~1.0.0"
      },
      "engines": {
        "node": ">= 0.8"
      }
    },
    "node_modules/delayed-stream": {
      "version": "1.0.0",
      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">=0.4.0"
      }
    },
    "node_modules/dotenv": {
      "version": "16.6.1",
      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "BSD-2-Clause",
      "engines": {
        "node": ">=12"
      },
      "funding": {
        "url": "https://dotenvx.com"
      }
    },
    "node_modules/dunder-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.1",
        "es-errors": "^1.3.0",
        "gopd": "^1.2.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-define-property": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-errors": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-object-atoms": {
      "version": "1.1.1",
      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/es-set-tostringtag": {
      "version": "2.1.0",
      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "es-errors": "^1.3.0",
        "get-intrinsic": "^1.2.6",
        "has-tostringtag": "^1.0.2",
        "hasown": "^2.0.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/follow-redirects": {
      "version": "1.15.11",
      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
      "integrity": [REDACTED_SECRET],
      "funding": [
        {
          "type": "individual",
          "url": "https://github.com/sponsors/RubenVerborgh"
        }
      ],
      "license": "MIT",
      "engines": {
        "node": ">=4.0"
      },
      "peerDependenciesMeta": {
        "debug": {
          "optional": true
        }
      }
    },
    "node_modules/form-data": {
      "version": "4.0.5",
      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "asynckit": "^0.4.0",
        "combined-stream": "^1.0.8",
        "es-set-tostringtag": "^2.1.0",
        "hasown": "^2.0.2",
        "mime-types": "^2.1.12"
      },
      "engines": {
        "node": ">= 6"
      }
    },
    "node_modules/function-bind": {
      "version": "1.1.2",
      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-intrinsic": {
      "version": "1.3.0",
      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "call-bind-apply-helpers": "^1.0.2",
        "es-define-property": "^1.0.1",
        "es-errors": "^1.3.0",
        "es-object-atoms": "^1.1.1",
        "function-bind": "^1.1.2",
        "get-proto": "^1.0.1",
        "gopd": "^1.2.0",
        "has-symbols": "^1.1.0",
        "hasown": "^2.0.2",
        "math-intrinsics": "^1.1.0"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/get-proto": {
      "version": "1.0.1",
      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "dunder-proto": "^1.0.1",
        "es-object-atoms": "^1.0.0"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/gopd": {
      "version": "1.2.0",
      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-symbols": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/has-tostringtag": {
      "version": "1.0.2",
      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "has-symbols": "^1.0.3"
      },
      "engines": {
        "node": ">= 0.4"
      },
      "funding": {
        "url": "https://github.com/sponsors/ljharb"
      }
    },
    "node_modules/hasown": {
      "version": "2.0.2",
      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "function-bind": "^1.1.2"
      },
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/math-intrinsics": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.4"
      }
    },
    "node_modules/mime-db": {
      "version": "1.52.0",
      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/mime-types": {
      "version": "2.1.35",
      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT",
      "dependencies": {
        "mime-db": "1.52.0"
      },
      "engines": {
        "node": ">= 0.6"
      }
    },
    "node_modules/proxy-from-env": {
      "version": "1.1.0",
      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
      "integrity": [REDACTED_SECRET],
      "license": "MIT"
    }
  }
}


path: package.json
content: {
  "name": "feishu-common",
  "version": "1.0.0",
  "description": "Common Feishu API client and authentication utilities for OpenClaw skills.",
  "main": "index.js",
  "scripts": {
    "test": "echo \"No tests specified\" && exit 0"
  },
  "dependencies": {
    "axios": "^1.6.0",
    "dotenv": "^16.3.1"
  },
  "author": "OpenClaw Evolution",
  "license": "MIT"
}


Security scan verdicts — ClawScan: clean | static analysis:  | VirusTotal: clean | SkillSpector: suspicious (MEDIUM)