mirror of
https://github.com/larksuite/cli.git
synced 2026-08-03 08:32:46 +08:00
165 lines
4.8 KiB
JavaScript
165 lines
4.8 KiB
JavaScript
#!/usr/bin/env node
|
|
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
// Fetches a live E2E tenant access token (TAT) for the shared bot identity.
|
|
//
|
|
// Invoked from the e2e-live CI job. Exchanges the bot app id/secret for a
|
|
// tenant access token, writes the token to a private file under $RUNNER_TEMP,
|
|
// and emits the file path as a step output so the test step can read it once
|
|
// and then delete it.
|
|
//
|
|
// The secret arrives via environment variables; the OAuth parameter names are
|
|
// literal because this is a source code file (.js), so the quality gate's
|
|
// benign-code-credential exemption applies to the process.env references.
|
|
|
|
const fs = require("node:fs");
|
|
const http = require("node:http");
|
|
const https = require("node:https");
|
|
const path = require("node:path");
|
|
const { URL } = require("node:url");
|
|
|
|
const ENDPOINT = process.env.E2E_TAT_ENDPOINT || "https://accounts.feishu.cn/oauth/v3/token";
|
|
const MAX_ATTEMPTS = 4;
|
|
const RETRY_BASE_MS = parseInt(process.env.E2E_TAT_RETRY_BASE_MS || "1000", 10);
|
|
|
|
function requireEnv(name) {
|
|
const value = process.env[name];
|
|
if (!value) {
|
|
console.error(`::error::Missing required environment variable: ${name}`);
|
|
process.exit(1);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function postForm(url, body) {
|
|
return new Promise((resolve, reject) => {
|
|
const parsed = new URL(url);
|
|
const transport = parsed.protocol === "http:" ? http : https;
|
|
const req = transport.request(
|
|
parsed,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
"Content-Length": Buffer.byteLength(body),
|
|
},
|
|
timeout: 20000,
|
|
},
|
|
(resp) => {
|
|
const chunks = [];
|
|
let settled = false;
|
|
const rejectOnce = (error) => {
|
|
if (!settled) {
|
|
settled = true;
|
|
reject(error);
|
|
}
|
|
};
|
|
resp.on("data", (chunk) => chunks.push(chunk));
|
|
resp.on("aborted", () => rejectOnce(new Error("response aborted before completion")));
|
|
resp.on("error", rejectOnce);
|
|
resp.on("close", () => {
|
|
if (!resp.complete) {
|
|
rejectOnce(new Error("response closed before completion"));
|
|
}
|
|
});
|
|
resp.on("end", () => {
|
|
if (!resp.complete) {
|
|
rejectOnce(new Error("response ended before completion"));
|
|
return;
|
|
}
|
|
settled = true;
|
|
resolve({
|
|
status: resp.statusCode,
|
|
body: Buffer.concat(chunks).toString("utf8"),
|
|
headers: resp.headers,
|
|
});
|
|
});
|
|
},
|
|
);
|
|
req.on("timeout", () => {
|
|
req.destroy();
|
|
reject(new Error("request timed out"));
|
|
});
|
|
req.on("error", reject);
|
|
req.write(body);
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
function encodeForm(params) {
|
|
return Object.entries(params)
|
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
.join("&");
|
|
}
|
|
|
|
function sleep(ms) {
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
}
|
|
|
|
async function fetchTenantToken() {
|
|
const appId = requireEnv("LARKSUITE_CLI_APP_ID");
|
|
const appSecret = requireEnv("TEST_BOT1_APP_SECRET");
|
|
|
|
const body = encodeForm({
|
|
grant_type: "client_credentials",
|
|
client_id: appId,
|
|
client_secret: appSecret,
|
|
});
|
|
|
|
let lastError = "";
|
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
try {
|
|
const { status, body: respBody, headers } = await postForm(ENDPOINT, body);
|
|
let payload;
|
|
try {
|
|
payload = JSON.parse(respBody);
|
|
} catch {
|
|
const logID = headers["x-tt-logid"] || headers["x-request-id"] || "unavailable";
|
|
lastError = `HTTP ${status}, log_id=${logID}, non-JSON response`;
|
|
}
|
|
if (payload) {
|
|
const token = payload.access_token;
|
|
if (status === 200 && payload.code === 0 && token) {
|
|
return token;
|
|
}
|
|
lastError = `HTTP ${status}, code=${payload.code}, error=${payload.error}, msg=${payload.msg || payload.error_description}`;
|
|
}
|
|
} catch (err) {
|
|
lastError = err.message;
|
|
}
|
|
|
|
if (attempt < MAX_ATTEMPTS) {
|
|
await sleep(2 ** (attempt - 1) * RETRY_BASE_MS);
|
|
}
|
|
}
|
|
|
|
console.error(`::error::Failed to fetch tenant access token: ${lastError}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
async function main() {
|
|
const token = await fetchTenantToken();
|
|
console.log(`::add-mask::${token}`);
|
|
|
|
const tatPath = path.join(process.env.RUNNER_TEMP, "e2e-live-tat");
|
|
fs.writeFileSync(tatPath, token, { encoding: "utf8", mode: 0o600 });
|
|
|
|
if (process.env.GITHUB_OUTPUT) {
|
|
fs.appendFileSync(process.env.GITHUB_OUTPUT, `path=${tatPath}\n`);
|
|
}
|
|
|
|
console.log("Prepared shared live E2E tenant token");
|
|
}
|
|
|
|
if (require.main === module) {
|
|
main();
|
|
}
|
|
|
|
module.exports = {
|
|
encodeForm,
|
|
fetchTenantToken,
|
|
postForm,
|
|
requireEnv,
|
|
};
|