Files
windmill_workflow/alexa-api/auth4-core.js
2026-04-04 09:54:20 +09:00

240 lines
7.4 KiB
JavaScript

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 = /<input[^>]+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(/&amp;/g, '&');
const match2 = html.match(/name="signIn"[^>]+action="([^"]+)"/);
if (match2) return match2[1].replace(/&amp;/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,
};