/* 仕様書 https://developers.worksmobile.com/jp/docs/group-create 使い方: node createLINEWORKSGroup.js "新規グループ名" 環境変数: LINEWORKS_DOMAIN_ID 任意: domainId (未指定時は DEFAULT_SETTINGS.domainId) LINEWORKS_DEFAULT_ADMIN_USER_ID 任意: 管理者 userId の初期値 LINEWORKS_DEFAULT_MEMBER_ID 任意: メンバー id の初期値 LINEWORKS_DEFAULT_GROUP_DOMAIN 任意: groupEmail 生成時のデフォルトドメイン LW_CLIENT_ID 任意: LINE WORKS Client ID LW_CLIENT_SECRET 任意: LINE WORKS Client Secret LW_SERVICE_ACCOUNT 任意: サービスアカウント LW_PRIVATE_KEY 任意: 秘密鍵文字列 LW_PRIVATE_KEY_FILE 任意: 秘密鍵ファイルパス LW_SCOPE 任意: トークン取得スコープ (既定: group) */ "use strict"; const fs = require("fs"); const path = require("path"); const crypto = require("crypto"); const LW_TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token"; const API_URL = "https://www.worksapis.com/v1.0/groups"; // 認証設定(importCal.js / importBoard.js と同系統) const PRIVATE_KEY_FILE = process.env.LW_PRIVATE_KEY_FILE ? path.resolve(process.env.LW_PRIVATE_KEY_FILE) : path.join(__dirname, "private_20260307184804.key"); const LW_CLIENT_ID = process.env.LW_CLIENT_ID || "tre8J_Tk8RblfsMSyZRh"; const LW_CLIENT_SECRET = process.env.LW_CLIENT_SECRET || "O5_R5gcHxg"; const LW_SERVICE_ACCOUNT = process.env.LW_SERVICE_ACCOUNT || "wh4k8.serviceaccount@next-hd.co.jp"; const LW_SCOPE = process.env.LW_SCOPE || "group"; const LW_PRIVATE_KEY = (process.env.LW_PRIVATE_KEY && process.env.LW_PRIVATE_KEY.trim()) ? process.env.LW_PRIVATE_KEY : (fs.existsSync(PRIVATE_KEY_FILE) ? fs.readFileSync(PRIVATE_KEY_FILE, "utf8") : ""); // groupName 以外はここでデフォルト値を調整可能 const DEFAULT_SETTINGS = { // 必須項目 domainId: 401101389, administrators: [ { userId: "b4e31b40-ea40-4b53-199c-04469159be6d" || "", }, ], members: [ { id: "b4e31b40-ea40-4b53-199c-04469159be6d" || "", type: "USER", }, ], groupEmailDomain: "next-hd.co.jp", // 任意項目 description: "共用アドレス用グループ", visible: false, useServiceNotification: false, serviceManageable: false, groupExternalKey: null, useMessage: false, useNote: false, useCalendar: false, useTask: false, useFolder: false, useMail: true, groupEmail: null, aliasEmails: [], canReceiveExternalMail: true, toExternalEmails: [], membersAllowedToUseGroupEmailAsRecipient: [], membersAllowedToUseGroupEmailAsSender: [], }; function parseArgs() { const groupName = process.argv.slice(2).join(" ").trim(); if (!groupName) { throw new Error('グループ名を引数で指定してください。例: node createLINEWORKSGroup.js "営業本部"'); } return { groupName }; } function ensureRequiredEnvForAuth() { const missing = []; if (!LW_CLIENT_ID) missing.push("LW_CLIENT_ID"); if (!LW_CLIENT_SECRET) missing.push("LW_CLIENT_SECRET"); if (!LW_SERVICE_ACCOUNT) missing.push("LW_SERVICE_ACCOUNT"); if (!LW_PRIVATE_KEY) missing.push("LW_PRIVATE_KEY"); if (missing.length > 0) { if (missing.includes("LW_PRIVATE_KEY") && !fs.existsSync(PRIVATE_KEY_FILE)) { throw new Error(`秘密鍵が見つかりません: ${PRIVATE_KEY_FILE}`); } throw new Error(`環境変数が不足しています: ${missing.join(", ")}`); } } function base64UrlEncode(value) { return Buffer.from(value) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); } function createJwtAssertion() { const now = Math.floor(Date.now() / 1000); const header = { alg: "RS256", typ: "JWT" }; const payload = { iss: LW_CLIENT_ID, sub: LW_SERVICE_ACCOUNT, 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(LW_PRIVATE_KEY) .toString("base64") .replace(/=/g, "") .replace(/\+/g, "-") .replace(/\//g, "_"); return `${signingInput}.${signature}`; } async function getAccessToken(scope = LW_SCOPE) { ensureRequiredEnvForAuth(); const assertion = createJwtAssertion(); const form = new URLSearchParams({ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", assertion, client_id: LW_CLIENT_ID, client_secret: LW_CLIENT_SECRET, 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; } function buildRequestBody(groupName) { const domainId = Number(process.env.LINEWORKS_DOMAIN_ID || DEFAULT_SETTINGS.domainId); const groupEmailDomain = String(DEFAULT_SETTINGS.groupEmailDomain || "").trim(); const groupEmailLocalPart = String(groupName).split("@")[0] || groupName; const generatedGroupEmail = `${groupEmailLocalPart}@${groupEmailDomain}`; const finalGroupName = "【ML】" + groupName; // グループ名の先頭に識別用のプレフィックスを付与(任意) const body = { domainId: domainId, groupName: finalGroupName, administrators: DEFAULT_SETTINGS.administrators, members: DEFAULT_SETTINGS.members, description: DEFAULT_SETTINGS.description, visible: DEFAULT_SETTINGS.visible, useServiceNotification: DEFAULT_SETTINGS.useServiceNotification, serviceManageable: DEFAULT_SETTINGS.serviceManageable, groupExternalKey: DEFAULT_SETTINGS.groupExternalKey, useMessage: DEFAULT_SETTINGS.useMessage, useNote: DEFAULT_SETTINGS.useNote, useCalendar: DEFAULT_SETTINGS.useCalendar, useTask: DEFAULT_SETTINGS.useTask, useFolder: DEFAULT_SETTINGS.useFolder, useMail: DEFAULT_SETTINGS.useMail, groupEmail: generatedGroupEmail, aliasEmails: DEFAULT_SETTINGS.aliasEmails, canReceiveExternalMail: DEFAULT_SETTINGS.canReceiveExternalMail, toExternalEmails: DEFAULT_SETTINGS.toExternalEmails, membersAllowedToUseGroupEmailAsRecipient: DEFAULT_SETTINGS.membersAllowedToUseGroupEmailAsRecipient, membersAllowedToUseGroupEmailAsSender: DEFAULT_SETTINGS.membersAllowedToUseGroupEmailAsSender, }; // null/undefined は送信しない Object.keys(body).forEach((key) => { if (body[key] == null) { delete body[key]; } }); return body; } function validateBody(body) { if (!Number.isInteger(body.domainId) || body.domainId <= 0) { throw new Error("domainId が未設定です。DEFAULT_SETTINGS.domainId または LINEWORKS_DOMAIN_ID を設定してください。"); } if (!Array.isArray(body.administrators) || body.administrators.length === 0) { throw new Error("administrators は 1 件以上必要です。"); } if (body.administrators.some((admin) => !admin || !admin.userId)) { throw new Error("administrators[].userId が未設定です。DEFAULT_SETTINGS を設定してください。"); } if (!Array.isArray(body.members) || body.members.length === 0) { throw new Error("members は 1 件以上必要です。"); } if (body.members.some((member) => !member || !member.id || !member.type)) { throw new Error("members[].id / members[].type が未設定です。DEFAULT_SETTINGS を設定してください。"); } if (body.useMail === true && !body.groupEmail) { throw new Error("useMail=true の場合は groupEmail が必須です。"); } } async function createGroup(body, accessToken) { if (typeof fetch !== "function") { throw new Error("この Node.js 実行環境は fetch に未対応です。Node.js 18 以上を使用してください。"); } const response = await fetch(API_URL, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json", }, body: JSON.stringify(body), }); const raw = await response.text(); let data = null; try { data = raw ? JSON.parse(raw) : null; } catch (_err) { data = raw; } if (!response.ok) { const detail = typeof data === "string" ? data : JSON.stringify(data, null, 2); throw new Error(`API error ${response.status}: ${detail}`); } return data; } async function main() { const accessToken = await getAccessToken(); const { groupName } = parseArgs(); const body = buildRequestBody(groupName); validateBody(body); const created = await createGroup(body, accessToken); console.log("グループ作成に成功しました。"); console.log(JSON.stringify(created, null, 2)); } main().catch((err) => { console.error("グループ作成に失敗しました。"); console.error(err.message); process.exit(1); });