@spinzaf/freefire-api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ const FreeFireAPI = require('./lib/api');
2
+
3
+ module.exports = FreeFireAPI;
package/lib/api.js ADDED
@@ -0,0 +1,229 @@
1
+ const axios = require('axios');
2
+ const protoHandler = require('./protobuf');
3
+ const { AE, HEADERS, URLS, GARENA_CLIENT, DEFAULT_CREDENTIALS } = require('./constants');
4
+ const { processPlayerItems } = require('./utils');
5
+
6
+ class FreeFireAPI {
7
+ constructor() {
8
+ this.session = {
9
+ token: null,
10
+ serverUrl: null,
11
+ openId: null,
12
+ accountId: null
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Authenticate with Garena using UID and Password (Guest/Account)
18
+ * @param {string} [uid] - (Optional) User ID
19
+ * @param {string} [password] - (Optional) Password
20
+ */
21
+ async login(uid = null, password = null) {
22
+ // Use default credentials if not provided
23
+ if (!uid || !password) {
24
+ console.log("[i] No credentials provided, loading from config/credentials.yaml.");
25
+ uid = DEFAULT_CREDENTIALS.UID;
26
+ password = DEFAULT_CREDENTIALS.PASSWORD;
27
+ }
28
+
29
+ if (!uid || !password) {
30
+ throw new Error("Missing credentials. Set UID and PASSWORD in config/credentials.yaml or pass them to login(uid, password).");
31
+ }
32
+
33
+ // Step 1: Get Garena Token
34
+ const garenaData = await this._getGarenaToken(uid, password);
35
+ if (!garenaData || !garenaData.access_token) {
36
+ throw new Error("Garena authentication failed: Invalid credentials or response");
37
+ }
38
+
39
+ // Step 2: Major Login
40
+ const loginData = await this._majorLogin(garenaData.access_token, garenaData.open_id);
41
+ if (!loginData || !loginData.token) {
42
+ throw new Error("Major login failed: Empty token received");
43
+ }
44
+
45
+ this.session.token = loginData.token;
46
+ this.session.serverUrl = loginData.serverUrl;
47
+ this.session.openId = garenaData.open_id;
48
+ this.session.accountId = loginData.accountid;
49
+
50
+ return this.session;
51
+ }
52
+
53
+ async _getGarenaToken(uid, password) {
54
+ const params = new URLSearchParams();
55
+ params.append('uid', uid);
56
+ params.append('password', password);
57
+ params.append('response_type', 'token');
58
+ params.append('client_type', '2');
59
+ params.append('client_secret', GARENA_CLIENT.CLIENT_SECRET);
60
+ params.append('client_id', GARENA_CLIENT.CLIENT_ID);
61
+
62
+ try {
63
+ const response = await axios.post(URLS.GARENA_TOKEN, params, {
64
+ headers: HEADERS.GARENA_AUTH
65
+ });
66
+ return response.data;
67
+ } catch (error) {
68
+ throw new Error(`Garena Auth Request Failed: ${error.message}`);
69
+ }
70
+ }
71
+
72
+ async _majorLogin(accessToken, openId) {
73
+ const payload = {
74
+ openid: openId,
75
+ logintoken: accessToken,
76
+ platform: "4"
77
+ };
78
+
79
+ const encryptedBody = await protoHandler.encode('MajorLogin.proto', 'request', payload, true);
80
+
81
+ try {
82
+ const response = await axios.post(URLS.MAJOR_LOGIN, encryptedBody, {
83
+ headers: {
84
+ ...HEADERS.COMMON,
85
+ 'Authorization': 'Bearer', // Specific to MajorLogin
86
+ 'Content-Type': 'application/octet-stream'
87
+ },
88
+ responseType: 'arraybuffer'
89
+ });
90
+
91
+ return await protoHandler.decode('MajorLogin.proto', 'response', response.data);
92
+ } catch (error) {
93
+ throw new Error(`Major Login Request Failed: ${error.message}`);
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Search for accounts by name (fuzzy search)
99
+ * @param {string} keyword
100
+ * @returns {Promise<Array>} List of matching accounts
101
+ */
102
+ async searchAccount(keyword) {
103
+ await this._checkSession();
104
+
105
+ if (keyword.length < 3) {
106
+ throw new Error("Search keyword must be at least 3 characters long.");
107
+ }
108
+
109
+ const payload = { keyword: String(keyword) };
110
+ const encryptedBody = await protoHandler.encode('SearchAccountByName.proto', 'SearchAccountByName.request', payload, true);
111
+
112
+ const url = URLS.SEARCH(this.session.serverUrl);
113
+
114
+ try {
115
+ const response = await axios.post(url, encryptedBody, {
116
+ headers: {
117
+ ...HEADERS.COMMON,
118
+ 'Authorization': `Bearer ${this.session.token}`,
119
+ 'Content-Type': 'application/x-www-form-urlencoded'
120
+ },
121
+ responseType: 'arraybuffer'
122
+ });
123
+
124
+ const data = await protoHandler.decode('SearchAccountByName.proto', 'SearchAccountByName.response', response.data);
125
+ return data.infos; // Field name is 'infos' in proto
126
+ } catch (error) {
127
+ throw new Error(`Search Failed: ${error.message}`);
128
+ }
129
+ }
130
+
131
+ /**
132
+ * Get detailed player profile (Personal Show)
133
+ * @param {number|string} uid
134
+ * @returns {Promise<Object>} Player data including profile, guild, etc.
135
+ */
136
+ async getPlayerProfile(uid) {
137
+ await this._checkSession();
138
+
139
+ const payload = {
140
+ accountId: Number(uid),
141
+ callSignSrc: 7,
142
+ needGalleryInfo: true
143
+ };
144
+
145
+ const encryptedBody = await protoHandler.encode('PlayerPersonalShow.proto', 'request', payload, true);
146
+ const url = URLS.PERSONAL_SHOW(this.session.serverUrl);
147
+
148
+ try {
149
+ const response = await axios.post(url, encryptedBody, {
150
+ headers: {
151
+ ...HEADERS.COMMON,
152
+ 'Authorization': `Bearer ${this.session.token}`
153
+ },
154
+ responseType: 'arraybuffer'
155
+ });
156
+
157
+ return await protoHandler.decode('PlayerPersonalShow.proto', 'response', response.data);
158
+ } catch (error) {
159
+ throw new Error(`Get Profile Failed: ${error.message}`);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get player items (outfit, weapons, skills, pet)
165
+ * @param {number|string} uid
166
+ */
167
+ async getPlayerItems(uid) {
168
+ const profile = await this.getPlayerProfile(uid);
169
+ if (!profile) return null;
170
+ return processPlayerItems(profile);
171
+ }
172
+
173
+ /**
174
+ * Get Player Stats
175
+ * @param {number|string} uid
176
+ * @param {'br'|'cs'} mode - Battle Royale or Clash Squad
177
+ * @param {'career'|'ranked'|'normal'} matchType
178
+ */
179
+ async getPlayerStats(uid, mode = 'br', matchType = 'career') {
180
+ await this._checkSession();
181
+
182
+ const modeLower = mode.toLowerCase();
183
+ const typeUpper = matchType.toUpperCase();
184
+
185
+ let matchMode = 0;
186
+ let url = '';
187
+ let protoFile = '';
188
+ let payload = { accountid: Number(uid) };
189
+
190
+ if (modeLower === 'br') {
191
+ const types = { 'CAREER': 0, 'NORMAL': 1, 'RANKED': 2 };
192
+ matchMode = types[typeUpper] !== undefined ? types[typeUpper] : 0;
193
+ url = URLS.PLAYER_STATS(this.session.serverUrl);
194
+ protoFile = 'PlayerStats.proto';
195
+ payload.matchmode = matchMode;
196
+ } else {
197
+ const types = { 'CAREER': 0, 'NORMAL': 1, 'RANKED': 6 };
198
+ matchMode = types[typeUpper] !== undefined ? types[typeUpper] : 0;
199
+ url = URLS.PLAYER_CS_STATS(this.session.serverUrl);
200
+ protoFile = 'PlayerCSStats.proto';
201
+ payload.gamemode = 15; // CS default
202
+ payload.matchmode = matchMode;
203
+ }
204
+
205
+ const encryptedBody = await protoHandler.encode(protoFile, 'request', payload, true);
206
+
207
+ try {
208
+ const response = await axios.post(url, encryptedBody, {
209
+ headers: {
210
+ ...HEADERS.COMMON,
211
+ 'Authorization': `Bearer ${this.session.token}`
212
+ },
213
+ responseType: 'arraybuffer'
214
+ });
215
+
216
+ return await protoHandler.decode(protoFile, 'response', response.data);
217
+ } catch (error) {
218
+ throw new Error(`Get Stats Failed: ${error.message}`);
219
+ }
220
+ }
221
+ // ----- Auto login if no session with Default Data.
222
+ async _checkSession() {
223
+ if (!this.session.token || !this.session.serverUrl) {
224
+ await this.login();
225
+ }
226
+ }
227
+ }
228
+
229
+ module.exports = FreeFireAPI;
@@ -0,0 +1,115 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function parseSimpleYaml(content) {
5
+ const result = {};
6
+ const lines = content.split(/\r?\n/);
7
+
8
+ for (const rawLine of lines) {
9
+ const line = rawLine.trim();
10
+ if (!line || line.startsWith('#')) continue;
11
+
12
+ const separatorIndex = line.indexOf(':');
13
+ if (separatorIndex === -1) continue;
14
+
15
+ const key = line.slice(0, separatorIndex).trim();
16
+ let value = line.slice(separatorIndex + 1).trim();
17
+
18
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
19
+ value = value.slice(1, -1);
20
+ }
21
+
22
+ result[key] = value;
23
+ }
24
+
25
+ return result;
26
+ }
27
+
28
+ function loadYamlFile(filePath) {
29
+ try {
30
+ const yamlRaw = fs.readFileSync(filePath, 'utf8');
31
+ return parseSimpleYaml(yamlRaw);
32
+ } catch (error) {
33
+ return {};
34
+ }
35
+ }
36
+
37
+ function readConfigValue(config, key, fallback) {
38
+ const value = config[key];
39
+ if (value === undefined || value === null || value === '') return fallback;
40
+ return String(value);
41
+ }
42
+
43
+ function requireConfigValue(config, key) {
44
+ const value = readConfigValue(config, key, '');
45
+ if (!value) {
46
+ throw new Error(`Missing required setting in config/settings.yaml: ${key}`);
47
+ }
48
+ return value;
49
+ }
50
+
51
+ function loadDefaultCredentials() {
52
+ const parsed = loadYamlFile(path.join(__dirname, '../config/credentials.yaml'));
53
+ return {
54
+ UID: readConfigValue(parsed, 'UID', ''),
55
+ PASSWORD: readConfigValue(parsed, 'PASSWORD', '')
56
+ };
57
+ }
58
+
59
+ function loadSettings() {
60
+ const parsed = loadYamlFile(path.join(__dirname, '../config/settings.yaml'));
61
+
62
+ return {
63
+ AE: {
64
+ MAIN_KEY: Buffer.from(requireConfigValue(parsed, 'AE_MAIN_KEY'), "binary"),
65
+ MAIN_IV: Buffer.from(requireConfigValue(parsed, 'AE_MAIN_IV'), "binary")
66
+ },
67
+ HEADERS: {
68
+ COMMON: {
69
+ 'User-Agent': requireConfigValue(parsed, 'HEADERS_COMMON_USER_AGENT'),
70
+ 'Connection': requireConfigValue(parsed, 'HEADERS_COMMON_CONNECTION'),
71
+ 'Accept-Encoding': requireConfigValue(parsed, 'HEADERS_COMMON_ACCEPT_ENCODING'),
72
+ 'Expect': requireConfigValue(parsed, 'HEADERS_COMMON_EXPECT'),
73
+ 'X-Unity-Version': requireConfigValue(parsed, 'HEADERS_COMMON_X_UNITY_VERSION'),
74
+ 'X-GA': requireConfigValue(parsed, 'HEADERS_COMMON_X_GA'),
75
+ 'ReleaseVersion': requireConfigValue(parsed, 'HEADERS_COMMON_RELEASE_VERSION'),
76
+ 'Content-Type': requireConfigValue(parsed, 'HEADERS_COMMON_CONTENT_TYPE')
77
+ },
78
+ GARENA_AUTH: {
79
+ 'User-Agent': requireConfigValue(parsed, 'HEADERS_GARENA_AUTH_USER_AGENT'),
80
+ 'Connection': requireConfigValue(parsed, 'HEADERS_GARENA_AUTH_CONNECTION'),
81
+ 'Accept-Encoding': requireConfigValue(parsed, 'HEADERS_GARENA_AUTH_ACCEPT_ENCODING')
82
+ }
83
+ },
84
+ URLS: {
85
+ GARENA_TOKEN: requireConfigValue(parsed, 'URL_GARENA_TOKEN'),
86
+ MAJOR_LOGIN: requireConfigValue(parsed, 'URL_MAJOR_LOGIN'),
87
+ SEARCH_PATH: requireConfigValue(parsed, 'URL_PATH_SEARCH'),
88
+ PERSONAL_SHOW_PATH: requireConfigValue(parsed, 'URL_PATH_PERSONAL_SHOW'),
89
+ PLAYER_STATS_PATH: requireConfigValue(parsed, 'URL_PATH_PLAYER_STATS'),
90
+ PLAYER_CS_STATS_PATH: requireConfigValue(parsed, 'URL_PATH_PLAYER_CS_STATS')
91
+ },
92
+ GARENA_CLIENT: {
93
+ CLIENT_ID: requireConfigValue(parsed, 'GARENA_CLIENT_ID'),
94
+ CLIENT_SECRET: requireConfigValue(parsed, 'GARENA_CLIENT_SECRET')
95
+ }
96
+ };
97
+ }
98
+
99
+ const settings = loadSettings();
100
+ const paths = settings.URLS;
101
+
102
+ module.exports = {
103
+ AE: settings.AE,
104
+ HEADERS: settings.HEADERS,
105
+ URLS: {
106
+ GARENA_TOKEN: paths.GARENA_TOKEN,
107
+ MAJOR_LOGIN: paths.MAJOR_LOGIN,
108
+ SEARCH: (serverUrl) => `${serverUrl}${paths.SEARCH_PATH}`,
109
+ PERSONAL_SHOW: (serverUrl) => `${serverUrl}${paths.PERSONAL_SHOW_PATH}`,
110
+ PLAYER_STATS: (serverUrl) => `${serverUrl}${paths.PLAYER_STATS_PATH}`,
111
+ PLAYER_CS_STATS: (serverUrl) => `${serverUrl}${paths.PLAYER_CS_STATS_PATH}`
112
+ },
113
+ GARENA_CLIENT: settings.GARENA_CLIENT,
114
+ DEFAULT_CREDENTIALS: loadDefaultCredentials()
115
+ };
package/lib/crypto.js ADDED
@@ -0,0 +1,15 @@
1
+ const crypto = require('crypto');
2
+ const { AE } = require('./constants');
3
+
4
+ /**
5
+ * Encrypts data using AES-128-CBC with PKCS7 padding.
6
+ * @param {Buffer} buffer - The data to encrypt
7
+ * @returns {Buffer} Encrypted data
8
+ */
9
+ function encrypt(buffer) {
10
+ const cipher = crypto.createCipheriv('aes-128-cbc', AE.MAIN_KEY, AE.MAIN_IV);
11
+ const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
12
+ return encrypted;
13
+ }
14
+
15
+ module.exports = { encrypt };
@@ -0,0 +1,51 @@
1
+ const protobuf = require('protobufjs');
2
+ const path = require('path');
3
+ const { encrypt } = require('./crypto');
4
+
5
+ const PROTO_DIR = path.join(__dirname, '../proto');
6
+
7
+ class ProtoHandler {
8
+ constructor() {
9
+ this.roots = {};
10
+ }
11
+
12
+ async load(filename) {
13
+ if (!this.roots[filename]) {
14
+ this.roots[filename] = await protobuf.load(path.join(PROTO_DIR, filename));
15
+ }
16
+ return this.roots[filename];
17
+ }
18
+
19
+ async encode(filename, messageName, payload, shouldEncrypt = true) {
20
+ const root = await this.load(filename);
21
+ const Type = root.lookupType(messageName);
22
+
23
+ // Verify payload matches connection
24
+ const errMsg = Type.verify(payload);
25
+ if (errMsg) throw Error(errMsg);
26
+
27
+ const message = Type.create(payload);
28
+ const buffer = Type.encode(message).finish();
29
+
30
+ if (shouldEncrypt) {
31
+ return encrypt(buffer);
32
+ }
33
+ return buffer;
34
+ }
35
+
36
+ async decode(filename, messageName, buffer) {
37
+ const root = await this.load(filename);
38
+ const Type = root.lookupType(messageName);
39
+
40
+ const message = Type.decode(buffer);
41
+ return Type.toObject(message, {
42
+ longs: String,
43
+ enums: String,
44
+ bytes: String,
45
+ defaults: true,
46
+ arrays: true
47
+ });
48
+ }
49
+ }
50
+
51
+ module.exports = new ProtoHandler();
package/lib/utils.js ADDED
@@ -0,0 +1,112 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ let itemsDb = null;
5
+
6
+ function loadItems() {
7
+ if (itemsDb) return itemsDb;
8
+ try {
9
+ const data = fs.readFileSync(path.join(__dirname, '../data/items.json'), 'utf8');
10
+ const itemsList = JSON.parse(data);
11
+ itemsDb = {};
12
+ itemsList.forEach(item => {
13
+ const id = item.id || item.itemID;
14
+ if (id) itemsDb[String(id)] = item;
15
+ });
16
+ console.log(`Loaded ${Object.keys(itemsDb).length} items into database.`);
17
+ } catch (e) {
18
+ console.error("Failed to load items database:", e);
19
+ itemsDb = {};
20
+ }
21
+ return itemsDb;
22
+ }
23
+
24
+ // Initialize immediately
25
+ loadItems();
26
+
27
+ function getItemDetails(itemId) {
28
+ const itemStrId = String(itemId);
29
+
30
+ const currentItem = {
31
+ id: itemId,
32
+ name: "Unknown Item",
33
+ type: "UNKNOWN",
34
+ rarity: "NONE",
35
+ description: "",
36
+ is_unique: false,
37
+ image: `https://raw.githubusercontent.com/ashqking/FF-Items/main/ICONS/${itemId}.png`,
38
+ image_fallback: `https://raw.githubusercontent.com/I-SHOW-AKIRU200/AKIRU-ICONS/main/ICONS/${itemId}.png`
39
+ };
40
+
41
+ if (itemsDb && itemsDb[itemStrId]) {
42
+ const itemData = itemsDb[itemStrId];
43
+ currentItem.name = itemData.name || itemData.description || "Unknown Name";
44
+ currentItem.type = itemData.type || "UNKNOWN";
45
+ currentItem.collection_type = itemData.collection_type || "NONE";
46
+ currentItem.rarity = itemData.rare || "NONE";
47
+ currentItem.description = itemData.description || "";
48
+ currentItem.is_unique = itemData.is_unique || false;
49
+ currentItem.icon_code = itemData.icon || "";
50
+ }
51
+
52
+ return currentItem;
53
+ }
54
+
55
+ function processPlayerItems(playerData) {
56
+ const profileInfo = playerData.profileinfo || {};
57
+ const basicInfo = playerData.basicinfo || {};
58
+ const petInfo = playerData.petinfo || {};
59
+
60
+ // Clothes
61
+ const outfitIds = profileInfo.clothes || [];
62
+ const outfitDetails = outfitIds.map(getItemDetails);
63
+
64
+ // Weapons (shown skins)
65
+ const weaponIds = basicInfo.weaponskinshows || [];
66
+ const weaponDetails = weaponIds.map(getItemDetails);
67
+
68
+ // Skills
69
+ const skillIds = profileInfo.equipedskills || [];
70
+ const skillDetails = skillIds.map(getItemDetails);
71
+
72
+ // Pet
73
+ let petDetails = null;
74
+ if (petInfo && (petInfo.id || petInfo.skinid)) {
75
+ petDetails = {
76
+ id: getItemDetails(petInfo.id),
77
+ name: petInfo.name,
78
+ level: petInfo.level,
79
+ skin: getItemDetails(petInfo.skinid),
80
+ selected_skill: getItemDetails(petInfo.selectedskillid)
81
+ };
82
+ }
83
+
84
+ const normalizedBasicInfo = {
85
+ accountid: basicInfo.accountid || "",
86
+ nickname: basicInfo.nickname || "",
87
+ level: basicInfo.level || 0,
88
+ region: basicInfo.region || "",
89
+ liked: basicInfo.liked || "",
90
+ signature: basicInfo.signature || ""
91
+ };
92
+
93
+ return {
94
+ basic_info: normalizedBasicInfo,
95
+ items: {
96
+ outfit: outfitDetails,
97
+ skills: {
98
+ equipped: skillDetails
99
+ },
100
+ weapons: {
101
+ shown_skins: weaponDetails
102
+ },
103
+ pet: petDetails
104
+ }
105
+ };
106
+ }
107
+
108
+ module.exports = {
109
+ loadItems,
110
+ getItemDetails,
111
+ processPlayerItems
112
+ };
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@spinzaf/freefire-api",
3
+ "version": "1.0.0",
4
+ "description": "A powerful Node.js library to interact with Garena Free Fire API using Protobuf. Login, Search Players, and get Profile Stats.",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "node test/search.js",
8
+ "test:login": "node test/login.js",
9
+ "test:search": "node test/search.js",
10
+ "test:profile": "node test/profile.js",
11
+ "test:stats": "node test/stats.js",
12
+ "test:items": "node test/items.js",
13
+ "test:all": "node test/all.js"
14
+ },
15
+ "keywords": [
16
+ "freefire",
17
+ "checker",
18
+ "game-api",
19
+ "player-stats",
20
+ "ff",
21
+ "freefire-api",
22
+ "garena-freefire",
23
+ "player-info",
24
+ "stats-checker",
25
+ "game-data",
26
+ "sdk",
27
+ "wrapper",
28
+ "nodejs",
29
+ "javascript"
30
+ ],
31
+ "author": "spinzaf",
32
+ "license": "GPL-3.0",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/spinzaf/freefire-api.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/spinzaf/freefire-api/issues"
39
+ },
40
+ "homepage": "https://github.com/spinzaf/freefire-api#readme",
41
+ "dependencies": {
42
+ "axios": "^1.6.0",
43
+ "protobufjs": "^7.2.0"
44
+ }
45
+ }