const https = require('https'); const fs = require('fs'); const path = require('path'); const ALEXA_LOGIN_URL = 'https://www.amazon.co.jp/ap/signin?' + new URLSearchParams({ 'openid.assoc_handle': 'amzn_dp_project_dee_jp', 'openid.mode': 'checkid_setup', 'openid.ns': 'http://specs.openid.net/auth/2.0', 'openid.claimed_id': 'http://specs.openid.net/auth/2.0/identifier_select', 'openid.identity': 'http://specs.openid.net/auth/2.0/identifier_select', 'openid.return_to': 'https://alexa.amazon.co.jp/api/apps/v1/token', 'pageId': 'amzn_dp_project_dee_jp', }).toString(); const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36'; function createLogger(logger) { if (!logger) return function() {}; if (typeof logger === 'function') return logger; if (typeof logger.log === 'function') { return function(message) { logger.log(message); }; } return function() {}; } function createRequestState() { const cookieJar = {}; function setCookies(setCookieHeaders) { if (!setCookieHeaders) return; const headers = Array.isArray(setCookieHeaders) ? setCookieHeaders : [setCookieHeaders]; for (const header of headers) { const parts = header.split(';'); const kv = parts[0] || ''; const index = kv.indexOf('='); if (index <= 0) continue; const key = kv.slice(0, index).trim(); const value = kv.slice(index + 1).trim(); cookieJar[key] = value; } } function getCookieHeader() { return Object.entries(cookieJar) .map(([key, value]) => `${key}=${value}`) .join('; '); } function request(url, options) { const opts = options || {}; return new Promise((resolve, reject) => { const parsed = new URL(url); const reqOpts = { hostname: parsed.hostname, path: parsed.pathname + parsed.search, method: opts.method || 'GET', headers: { 'User-Agent': USER_AGENT, 'Accept-Language': 'ja-JP,ja;q=0.9', 'Accept': 'text/html,application/xhtml+xml,*/*;q=0.8', 'Cookie': getCookieHeader(), ...(opts.headers || {}), }, }; const req = https.request(reqOpts, (res) => { setCookies(res.headers['set-cookie']); let body = ''; res.on('data', (chunk) => { body += chunk; }); res.on('end', () => { resolve({ status: res.statusCode, headers: res.headers, body }); }); }); req.on('error', reject); if (opts.body) req.write(opts.body); req.end(); }); } return { cookieJar, getCookieHeader, request, }; } function extractHiddenFields(html) { const fields = {}; const re = /]+type=["']?hidden["']?[^>]*>/gi; let match; while ((match = re.exec(html)) !== null) { const tag = match[0]; const name = (tag.match(/name=["']([^"']+)["']/) || [])[1]; const value = (tag.match(/value=["']([^"']*)["']/) || ['', ''])[1]; if (name) fields[name] = value; } return fields; } function extractFormAction(html) { const match = html.match(/id="ap_login_form"[^>]+action="([^"]+)"/); if (match) return match[1].replace(/&/g, '&'); const match2 = html.match(/name="signIn"[^>]+action="([^"]+)"/); if (match2) return match2[1].replace(/&/g, '&'); return null; } function buildFailureMessage(current, cookieJar) { const keys = Object.keys(cookieJar).join(', ') || '(none)'; const hints = []; if (current.body && (current.body.includes('captcha') || current.body.includes('CAPTCHA'))) { hints.push('CAPTCHA が要求されています。少し待ってから再試行してください。'); } if (current.body && current.body.includes('auth-mfa-form')) { hints.push('MFA が要求されています。このスクリプトだけでは完了できません。'); } if (current.body && current.body.includes('password') && current.body.includes('error')) { hints.push('パスワードが間違っている可能性があります。'); } const message = [ '認証に失敗しました。取得できた Cookie に認証トークンが含まれていません。', `取得済み Cookie キー: ${keys}`, ...hints, ].join('\n'); const error = new Error(message); error.code = 'ALEXA_AUTH_FAILED'; error.hints = hints; return error; } async function fetchAlexaCookie(options) { const opts = options || {}; const email = opts.email; const password = opts.password; const log = createLogger(opts.logger); if (!email || !password) { const error = new Error('email と password は必須です'); error.code = 'MISSING_CREDENTIALS'; throw error; } const state = createRequestState(); const request = state.request; const cookieJar = state.cookieJar; log('[1] ログインページ取得中...'); const page1 = await request(ALEXA_LOGIN_URL); log(` Status: ${page1.status}, Cookies: ${Object.keys(cookieJar).join(', ')}`); if (page1.status !== 200) { const error = new Error(`ログインページ取得失敗: ${page1.status}`); error.code = 'LOGIN_PAGE_FETCH_FAILED'; throw error; } const action = extractFormAction(page1.body); const hidden = extractHiddenFields(page1.body); if (!action) { const error = new Error('ログインフォームが見つかりません。Amazon のログイン画面仕様が変わった可能性があります。'); error.code = 'LOGIN_FORM_NOT_FOUND'; throw error; } log(`[2] フォーム送信先: ${action}`); log(` Hidden fields: ${Object.keys(hidden).join(', ')}`); const formData = new URLSearchParams({ ...hidden, email: email, password: password, rememberMe: 'true', }).toString(); log('[3] 認証送信中...'); const page2 = await request(action, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Referer': ALEXA_LOGIN_URL, }, body: formData, }); log(` Status: ${page2.status}`); log(` Location: ${page2.headers.location || '(none)'}`); log(` Cookies after login: ${Object.keys(cookieJar).join(', ')}`); let current = page2; let redirectCount = 0; while (current.status >= 300 && current.status < 400 && current.headers.location && redirectCount < 10) { const location = current.headers.location; const nextUrl = location.startsWith('http') ? location : `https://www.amazon.co.jp${location}`; log(`[${4 + redirectCount}] Redirect -> ${nextUrl.substring(0, 80)}...`); current = await request(nextUrl); log(` Status: ${current.status}, New Cookies: ${Object.keys(cookieJar).join(', ')}`); redirectCount += 1; } const cookie = state.getCookieHeader(); const hasAlexaToken = cookie.includes('at-acbjp') || cookie.includes('session-token'); if (!hasAlexaToken) throw buildFailureMessage(current, cookieJar); return { cookie, cookieLength: cookie.length, cookieKeys: Object.keys(cookieJar), }; } function saveCookieToEnv(cookie, envPath) { const targetEnvPath = envPath || path.join(__dirname, '.env'); fs.writeFileSync(targetEnvPath, `ALEXA_COOKIE=${cookie}\n`); return targetEnvPath; } async function fetchAlexaCookieAndSave(options) { const result = await fetchAlexaCookie(options); const envPath = saveCookieToEnv(result.cookie, options && options.envPath); return { ...result, envPath, }; } module.exports = { fetchAlexaCookie, fetchAlexaCookieAndSave, saveCookieToEnv, };