"use strict"; /* * LINE WORKS Service AccountのJWT自己署名 → OAuth2アクセストークン取得。 * get-token.js (CLI、掲示板等のService Account対応APIの動作確認用) が使用する。 * Drive/共有ドライブAPIはService Account認証に非対応のため、backup.jsは * 代わりに lib/lineworksUserAuth.js (User Account認証) を使用する。 * ロジックは元々 get-token.js / lineworks-anythingllm.js にあったものを移設。 */ const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const LW_TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token"; function resolvePrivateKeyFile() { return process.env.LW_PRIVATE_KEY_FILE ? path.resolve(process.env.LW_PRIVATE_KEY_FILE) : path.join(__dirname, "..", "private_20260307184804.key"); } function resolvePrivateKey() { if (process.env.LW_PRIVATE_KEY && process.env.LW_PRIVATE_KEY.trim()) { return process.env.LW_PRIVATE_KEY; } const file = resolvePrivateKeyFile(); return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : ""; } function base64UrlEncode(value) { return Buffer.from(value) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); } function createJwtAssertion({ clientId, serviceAccount, privateKey }) { const now = Math.floor(Date.now() / 1000); const header = { alg: "RS256", typ: "JWT" }; const payload = { iss: clientId, sub: serviceAccount, aud: LW_TOKEN_URL, iat: now, exp: now + 300, }; const encodedHeader = base64UrlEncode(JSON.stringify(header)); const encodedPayload = base64UrlEncode(JSON.stringify(payload)); const signingInput = `${encodedHeader}.${encodedPayload}`; const signer = crypto.createSign("RSA-SHA256"); signer.update(signingInput); signer.end(); const signature = signer .sign(privateKey) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); return `${signingInput}.${signature}`; } // clientId/clientSecret/serviceAccount にコード側のデフォルト値は持たない。 // 必ず .env(または呼び出し元が明示的に渡すoptions)から取得し、無ければ即座にエラーとする。 // scope は呼び出し元(get-token.js / backup.js)ごとに用途が異なるため必須の引数として扱う。 async function getAccessToken(options = {}) { const clientId = options.clientId || process.env.LW_CLIENT_ID; const clientSecret = options.clientSecret || process.env.LW_CLIENT_SECRET; const serviceAccount = options.serviceAccount || process.env.LW_SERVICE_ACCOUNT; const scope = options.scope !== undefined ? options.scope : process.env.LW_SCOPE; const privateKey = options.privateKey || resolvePrivateKey(); const missing = []; if (!clientId) missing.push("LW_CLIENT_ID"); if (!clientSecret) missing.push("LW_CLIENT_SECRET"); if (!serviceAccount) missing.push("LW_SERVICE_ACCOUNT"); if (!privateKey) missing.push(`LW_PRIVATE_KEY または LW_PRIVATE_KEY_FILE (${resolvePrivateKeyFile()} が存在しません)`); if (missing.length > 0) { throw new Error(`環境変数が不足しています: ${missing.join(", ")}`); } const assertion = createJwtAssertion({ clientId, serviceAccount, privateKey }); const form = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion, client_id: clientId, client_secret: clientSecret, scope: scope || "", }); const response = await fetch(LW_TOKEN_URL, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: form, }); if (!response.ok) { const detail = await response.text(); throw new Error(`アクセストークン取得失敗: ${response.status} ${detail}`); } const data = await response.json(); if (!data.access_token) { throw new Error("アクセストークン取得失敗: access_token が返却されませんでした"); } return data.access_token; } module.exports = { getAccessToken, resolvePrivateKeyFile, resolvePrivateKey };