UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip

346 lines 14.3 kB
"use strict"; /** * Authentication service for Zalo API */ Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthService = void 0; const auth_1 = require("../types/auth"); const common_1 = require("../types/common"); const crypto_1 = require("crypto"); /** * Authentication service for handling OAuth flows and token management */ class AuthService { constructor(client, appId, appSecret) { this.client = client; this.appId = appId; this.appSecret = appSecret; // Zalo OAuth & Graph endpoints - organized by functionality this.endpoints = { auth: { oaPermission: "https://oauth.zaloapp.com/v4/oa/permission", socialPermission: "https://oauth.zaloapp.com/v4/permission", oaToken: "https://oauth.zaloapp.com/v4/oa/access_token", socialToken: "https://oauth.zaloapp.com/v4/access_token", refreshOaToken: "https://oauth.zaloapp.com/v4/oa/access_token", refreshSocialToken: "https://openapi.zalo.me/v2.0/oauth/access_token", }, social: { me: "https://graph.zalo.me/v2.0/me", }, }; } /** * Generate PKCE code verifier and challenge for Social API */ generatePKCE() { const codeVerifier = (0, crypto_1.randomBytes)(32).toString("base64url"); const codeChallenge = (0, crypto_1.createHash)("sha256") .update(codeVerifier) .digest("base64url"); return { code_verifier: codeVerifier, code_challenge: codeChallenge, code_challenge_method: "S256", }; } /** * Create OAuth authorization URL for Official Account with PKCE support * * @param redirectUri - The redirect URI after authorization * @param state - Optional state parameter for security. If not provided, auto-generates with 'zalo_oa_' prefix * @param usePkce - Whether to use PKCE for enhanced security. If true and pkce not provided, will auto-generate * @param pkce - Optional PKCE configuration for enhanced security. If usePkce=true and this is not provided, will be auto-generated * @returns Object containing the authorization URL, state, and PKCE config (if used) */ createOAAuthUrl(redirectUri, state, pkce, usePkce = false) { // Generate state with zalo_oa_ prefix if not provided const finalState = state || `zalo_oa_${(0, crypto_1.randomBytes)(16).toString("hex")}`; // Auto-generate PKCE if usePkce is true but pkce is not provided let finalPkce = pkce; if (usePkce && !pkce) { finalPkce = this.generatePKCE(); } const params = new URLSearchParams({ app_id: this.appId, redirect_uri: redirectUri, state: finalState, }); // Add PKCE parameters if PKCE is being used if (usePkce && finalPkce) { params.append("code_challenge", finalPkce.code_challenge); params.append("code_challenge_method", finalPkce.code_challenge_method); } const url = `${this.endpoints.auth.oaPermission}?${params.toString()}`; return { url, state: finalState, pkce: usePkce ? finalPkce : undefined, }; } /** * Create OAuth authorization URL for Social API */ createSocialAuthUrl(redirectUri, state, pkce) { const params = new URLSearchParams({ app_id: this.appId, redirect_uri: redirectUri, state: state || "social_auth", }); if (pkce) { params.append("code_challenge", pkce.code_challenge); params.append("code_challenge_method", pkce.code_challenge_method); } return `${this.endpoints.auth.socialPermission}?${params.toString()}`; } /** * Exchange authorization code for Official Account access token * Now supports PKCE code_verifier for enhanced security */ async getOAAccessToken(params) { try { const url = this.endpoints.auth.oaToken; // Prepare form data according to API docs const formData = new URLSearchParams(); formData.append("code", params.code); formData.append("app_id", params.app_id); formData.append("grant_type", "authorization_code"); // Add code_verifier if provided (for PKCE) if (params.code_verifier) { formData.append("code_verifier", params.code_verifier); } let result; try { result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), { "Content-Type": "application/x-www-form-urlencoded", "secret_key": params.app_secret, }); } catch (error) { // If the error contains access_token, it means the request was successful // but Zalo API returned a warning/error code along with valid tokens if (error instanceof common_1.ZaloSDKError && error.details?.access_token) { result = error.details; } else { throw error; } } // Check if we have access_token (prioritize token presence over error codes) if (result.access_token) { return { access_token: result.access_token, expires_in: parseInt(result.expires_in) || result.expires_in, refresh_token: result.refresh_token, }; } // If no access_token but we have data wrapper, check inside if (result.data?.access_token) { return { access_token: result.data.access_token, expires_in: parseInt(result.data.expires_in) || result.data.expires_in, refresh_token: result.data.refresh_token, }; } // Only check error codes if we don't have access_token if (result.error !== 0) { throw new common_1.ZaloSDKError(result.error_description || result.message || "Failed to get OA access token", result.error, result); } throw new common_1.ZaloSDKError("No access token received from Zalo API", -1, result); } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to get OA access token: ${error.message}`, -1, error); } } /** * Exchange authorization code for Social API access token */ async getSocialAccessToken(params) { try { const url = this.endpoints.auth.socialToken; const formData = new URLSearchParams(); formData.append("code", params.code); formData.append("app_id", params.app_id); formData.append("grant_type", "authorization_code"); if (params.code_verifier) { formData.append("code_verifier", params.code_verifier); } let result; try { result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), { "Content-Type": "application/x-www-form-urlencoded", secret_key: params.app_secret, }); } catch (error) { // If the error contains access_token, it means the request was successful // but Zalo API returned a warning/error code along with valid tokens if (error instanceof common_1.ZaloSDKError && error.details?.access_token) { result = error.details; } else { throw error; } } // Check if we have access_token (prioritize token presence over error codes) if (result.access_token) { return { access_token: result.access_token, expires_in: result.expires_in, refresh_token: result.refresh_token, }; } // If no access_token but we have data wrapper, check inside if (result.data?.access_token) { return { access_token: result.data.access_token, expires_in: result.data.expires_in, refresh_token: result.data.refresh_token, }; } // Only check error codes if we don't have access_token if (result.error && result.error !== 0) { throw new common_1.ZaloSDKError(result.error_description || result.message || "Failed to get Social access token", result.error, result); } throw new common_1.ZaloSDKError("No access token received from Zalo API", -1, result); } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to get Social access token: ${error.message}`, -1, error); } } /** * Refresh Official Account access token */ async refreshOAAccessToken(params) { try { const url = this.endpoints.auth.refreshOaToken; const formData = new URLSearchParams(); formData.append("refresh_token", params.refresh_token); formData.append("app_id", params.app_id); formData.append("grant_type", "refresh_token"); const result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), { "Content-Type": "application/x-www-form-urlencoded", secret_key: params.app_secret, }); if (!result.access_token) { throw new common_1.ZaloSDKError("No access token received when refreshing OA token", -1); } return { access_token: result.access_token, refresh_token: result.refresh_token, expires_in: parseInt(result.expires_in), }; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to refresh OA access token: ${error.message}`, -1, error); } } /** * Refresh Social API access token */ async refreshSocialAccessToken(params) { try { const endpoint = this.endpoints.auth.refreshSocialToken; const requestParams = { app_id: params.app_id, grant_type: "refresh_token", refresh_token: params.refresh_token, }; const result = await this.client.apiGet(endpoint, "", requestParams); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.error_description || result.message || "Failed to refresh Social access token", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No data received when refreshing Social token", -1); } return result.data; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to refresh Social access token: ${error.message}`, -1, error); } } /** * Get Social user information */ async getSocialUserInfo(accessToken, fields = "id,name,picture") { try { const url = this.endpoints.social.me; const params = { fields }; const result = await this.client.oauthRequestWithUrl("GET", url, params, { access_token: accessToken, }); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.message || "Failed to get Social user info", result.error, result); } if (!result.id) { throw new common_1.ZaloSDKError("No user data received from Social API", -1); } return { id: result.id, name: result.name, picture: result.picture, is_sensitive: result.is_sensitive, }; } catch (error) { if (error instanceof common_1.ZaloSDKError) { throw error; } throw new common_1.ZaloSDKError(`Failed to get Social user info: ${error.message}`, -1, error); } } /** * Validate access token by attempting to get user info */ async validateAccessToken(accessToken, scope = auth_1.AuthScope.SOCIAL) { try { if (scope === auth_1.AuthScope.SOCIAL) { const userInfo = await this.getSocialUserInfo(accessToken, "id"); return { valid: true, user_info: userInfo, }; } else { // For OA tokens, we could try to get OA info // This is a simplified validation return { valid: true }; } } catch (error) { return { valid: false }; } } /** * Get all authentication URLs with optional PKCE support */ getAuthUrls(redirectUri, usePkce = false, pkce) { const oaAuthResult = this.createOAAuthUrl(redirectUri, undefined, pkce, usePkce); return { oa_auth_url: oaAuthResult.url, social_auth_url: this.createSocialAuthUrl(redirectUri, undefined, pkce), token_url: this.endpoints.auth.socialToken, refresh_url: this.endpoints.auth.refreshOaToken, }; } } exports.AuthService = AuthService; //# sourceMappingURL=auth.service.js.map