import axios from 'axios'; import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; type OlistV3Tokens = { access_token: string; refresh_token?: string; expires_in?: number; expires_at?: string; }; type OlistV3AuthorizationState = { state: string; createdAt: string; }; const OAUTH_BASE_URL = 'https://accounts.tiny.com.br/realms/tiny/protocol/openid-connect'; const TOKEN_URL = `${OAUTH_BASE_URL}/token`; const AUTHORIZE_URL = `${OAUTH_BASE_URL}/auth`; const DEFAULT_TOKEN_FILE = path.join(process.cwd(), 'data-runtime', 'olist_v3_tokens.json'); const DEFAULT_STATE_FILE = path.join(process.cwd(), 'data-runtime', 'olist_v3_oauth_state.json'); function requiredEnv(name: string) { const value = process.env[name]?.trim(); if (!value) throw new Error(`Missing ${name}.`); return value; } function getTokenFile() { return process.env.OLIST_V3_TOKEN_FILE || DEFAULT_TOKEN_FILE; } function getStateFile() { return process.env.OLIST_V3_OAUTH_STATE_FILE || DEFAULT_STATE_FILE; } function ensureParentDirectory(filePath: string) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); } function writeJson(filePath: string, value: unknown) { ensureParentDirectory(filePath); fs.writeFileSync(filePath, JSON.stringify(value, null, 2), { mode: 0o600 }); } function readJson(filePath: string): T | null { if (!fs.existsSync(filePath)) return null; return JSON.parse(fs.readFileSync(filePath, 'utf-8')) as T; } function getClientConfig() { return { clientId: requiredEnv('OLIST_V3_CLIENT_ID'), clientSecret: requiredEnv('OLIST_V3_CLIENT_SECRET'), redirectUri: requiredEnv('OLIST_V3_REDIRECT_URI') }; } function saveTokens(tokens: OlistV3Tokens) { const expiresIn = Number(tokens.expires_in || 0); const expiresAt = Number.isFinite(expiresIn) && expiresIn > 0 ? new Date(Date.now() + Math.max(0, expiresIn - 60) * 1000).toISOString() : undefined; const previous = readJson(getTokenFile()); writeJson(getTokenFile(), { ...previous, ...tokens, refresh_token: tokens.refresh_token || previous?.refresh_token, expires_at: expiresAt || previous?.expires_at }); } function tokenIsUsable(tokens: OlistV3Tokens) { if (!tokens.access_token) return false; if (!tokens.expires_at) return true; return new Date(tokens.expires_at).getTime() > Date.now(); } async function requestTokens(params: URLSearchParams) { const response = await axios.post(TOKEN_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: 30000 }); if (!response.data?.access_token) { throw new Error('Olist V3 OAuth did not return an access token.'); } saveTokens(response.data); return response.data; } export function isOlistV3Configured() { return Boolean( process.env.OLIST_V3_CLIENT_ID?.trim() && process.env.OLIST_V3_CLIENT_SECRET?.trim() && process.env.OLIST_V3_REDIRECT_URI?.trim() ); } export function getOlistV3AuthorizationUrl() { const { clientId, redirectUri } = getClientConfig(); const state = crypto.randomBytes(32).toString('hex'); writeJson(getStateFile(), { state, createdAt: new Date().toISOString() } satisfies OlistV3AuthorizationState); const params = new URLSearchParams({ client_id: clientId, redirect_uri: redirectUri, scope: 'openid', response_type: 'code', state }); return `${AUTHORIZE_URL}?${params.toString()}`; } export async function exchangeOlistV3AuthorizationCode(code: string, state: string) { const expected = readJson(getStateFile()); const expectedState = Buffer.from(expected?.state || ''); const receivedState = Buffer.from(state || ''); if (!expected?.state || expectedState.length !== receivedState.length || !crypto.timingSafeEqual(expectedState, receivedState)) { throw new Error('Invalid OAuth state. Start the authorization flow again.'); } const createdAt = new Date(expected.createdAt).getTime(); if (!Number.isFinite(createdAt) || Date.now() - createdAt > 10 * 60 * 1000) { throw new Error('OAuth state expired. Start the authorization flow again.'); } const { clientId, clientSecret, redirectUri } = getClientConfig(); const params = new URLSearchParams({ grant_type: 'authorization_code', client_id: clientId, client_secret: clientSecret, redirect_uri: redirectUri, code }); const tokens = await requestTokens(params); fs.rmSync(getStateFile(), { force: true }); return tokens; } export async function getOlistV3AccessToken(forceRefresh = false) { const current = readJson(getTokenFile()); if (current && !forceRefresh && tokenIsUsable(current)) { return current.access_token; } if (!current?.refresh_token) { throw new Error('Olist V3 is not authorized. Open /api/olist-v3/authorize first.'); } const { clientId, clientSecret } = getClientConfig(); const params = new URLSearchParams({ grant_type: 'refresh_token', client_id: clientId, client_secret: clientSecret, refresh_token: current.refresh_token }); const tokens = await requestTokens(params); return tokens.access_token; } export function getOlistV3AuthStatus() { const tokens = readJson(getTokenFile()); return { configured: isOlistV3Configured(), authorized: Boolean(tokens?.access_token || tokens?.refresh_token), accessTokenUsable: Boolean(tokens && tokenIsUsable(tokens)), tokenExpiresAt: tokens?.expires_at || null, tokenFile: getTokenFile() }; }