UNPKG

@warriorteam/redai-zalo-sdk

Version:

Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account, ZNS, Consultation Service, Group Messaging, and Social APIs

312 lines 12.6 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://openapi.zalo.me/v2.0/oa/oauth/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/mehttps://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; const requestParams = { app_id: params.app_id, app_secret: params.app_secret, code: params.code, grant_type: "authorization_code", redirect_uri: params.redirect_uri, }; // Add code_verifier if provided (for PKCE) if (params.code_verifier) { requestParams.code_verifier = params.code_verifier; } await this.client.oauthRequestWithUrl("POST", url, null, { "Content-Type": "application/x-www-form-urlencoded", }); // Convert to URLSearchParams for form data const formData = new URLSearchParams(); Object.entries(requestParams).forEach(([key, value]) => { formData.append(key, value); }); const result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), { "Content-Type": "application/x-www-form-urlencoded", }); if (result.error !== 0) { throw new common_1.ZaloSDKError(result.error_description || result.message || "Failed to get OA access token", result.error, result); } if (!result.data) { throw new common_1.ZaloSDKError("No data received from Zalo API", -1); } return result.data; } 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); } const result = await this.client.oauthRequestWithUrl("POST", url, formData.toString(), { "Content-Type": "application/x-www-form-urlencoded", secret_key: params.app_secret, }); // Handle different response formats 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); } // Response might be direct or wrapped in data if (result.data) { return result.data; } else if (result.access_token) { return { access_token: result.access_token, expires_in: result.expires_in, refresh_token: result.refresh_token, }; } else { throw new common_1.ZaloSDKError("No access token received from Zalo API", -1); } } 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