fnlb 1.1.16 → 1.1.17
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/README.md +20 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -92,6 +92,26 @@ await fnlb.start({
|
|
|
92
92
|
});
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
## 🎯 Starting Specific Bots by ID
|
|
96
|
+
|
|
97
|
+
Run exact bots without filtering by category using the `bots` array. Get bot IDs from [app.fnlb.net/bots](https://app.fnlb.net/bots) → select a bot → **About this bot** → **FNLB ID**.
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import FNLB from 'fnlb';
|
|
101
|
+
|
|
102
|
+
const fnlb = new FNLB();
|
|
103
|
+
|
|
104
|
+
await fnlb.start({
|
|
105
|
+
apiToken: 'your-api-token',
|
|
106
|
+
bots: ['bot-id-1', 'bot-id-2'],
|
|
107
|
+
botsPerShard: 2
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Multiple shards can share the same `bots` list, the gateway distributes IDs across shards with no duplicates. You can combine `bots` and `categories` to narrow selection further.
|
|
112
|
+
|
|
113
|
+
Omit `categories` (or pass `[]`) to allow bots from any category. When `categories` is set, bots without a category are still included.
|
|
114
|
+
|
|
95
115
|
## ⛔ Stopping All Bots
|
|
96
116
|
|
|
97
117
|
Shut everything down cleanly using the `stop()` method:
|
package/dist/index.d.ts
CHANGED
|
@@ -31,8 +31,10 @@ export declare enum LogLevel {
|
|
|
31
31
|
Debug = "DEBUG"
|
|
32
32
|
}
|
|
33
33
|
export interface StartConfig {
|
|
34
|
-
apiToken
|
|
34
|
+
apiToken?: string;
|
|
35
|
+
token?: string;
|
|
35
36
|
categories?: string[];
|
|
37
|
+
bots?: string[];
|
|
36
38
|
numberOfShards?: number;
|
|
37
39
|
botsPerShard?: number;
|
|
38
40
|
hideUsernames?: boolean;
|
|
@@ -54,8 +56,9 @@ declare class FNLB {
|
|
|
54
56
|
private setupUpdater;
|
|
55
57
|
start(config: StartConfig): Promise<void>;
|
|
56
58
|
stop(): Promise<void>;
|
|
57
|
-
startShard(config: StartConfig, id: string, currentRunId: number): Promise<import("child_process").ChildProcess>;
|
|
59
|
+
startShard(config: StartConfig, id: string, currentRunId: number, authToken?: string): Promise<import("child_process").ChildProcess>;
|
|
58
60
|
update(force?: true): Promise<void>;
|
|
61
|
+
private resolveAuthToken;
|
|
59
62
|
private log;
|
|
60
63
|
private success;
|
|
61
64
|
private warn;
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{fork as R}from"node:child_process";import{resolve as f}from"node:path";import{createHash as p}from"node:crypto";import{mkdir as v,readFile as y,writeFile as D}from"node:fs/promises";import{resolve as b}from"node:path";import{createPublicKey as
|
|
1
|
+
import{fork as R}from"node:child_process";import{resolve as f}from"node:path";import{createHash as p}from"node:crypto";import{mkdir as v,readFile as y,writeFile as D}from"node:fs/promises";import{resolve as b}from"node:path";import{createPublicKey as k,verify as S}from"node:crypto";class m{lockPromise;constructor(){this.lockPromise=void 0}get isLocked(){return!!this.lockPromise}wait(){return this.lockPromise?.promise||Promise.resolve()}lock(){let e,t=new Promise((o)=>{e=o});this.lockPromise={promise:t,resolve:e}}unlock(){this.lockPromise?.resolve(),this.lockPromise=void 0}}class g{static wait(e){return new Promise((t)=>setTimeout(t,e))}}function N(e){return Buffer.from(JSON.stringify({fileName:e.fileName,hash:e.hash,url:e.url,version:e.version}))}function P(e,t){if(!e.signature||!e.signatureKeyId)return"invalid";let o=t[e.signatureKeyId];if(!o)return"invalid";let n=k({key:Buffer.from(o,"base64"),format:"der",type:"spki"}),s=e.fileName??E(e.url);if(!s)return"invalid";return S(null,N({version:e.version,hash:e.hash,fileName:s,url:e.url}),n,Buffer.from(e.signature,"base64"))?"verified":"invalid"}function L(e,t){let o=t.replace(/\/$/,""),n=new URL(e.url),s="/dist/",a=n.pathname.indexOf("/dist/");if(a===-1)throw Error("Invalid release url: missing /dist/ path");let i=n.pathname.slice(a+6);return`${o}/dist/${i}`}function E(e){try{let t=new URL(e).pathname.split("/").pop();return t?decodeURIComponent(t):null}catch{return null}}class c{storageDir;targetFileName;displayName;releaseUrl;releaseProvider;releasePublicKeys;trustedDownloadOrigin;maxDownloadRetries;maxBackoffMs;initialDelayMs;staleMs;log;success;warn;error;isLoaded=!1;lastLoadedTime=0;lock=new m;constructor(e){this.storageDir=e.storageDir,this.targetFileName=e.targetFileName,this.displayName=e.displayName??e.targetFileName,this.releaseUrl=e.releaseUrl,this.releaseProvider=e.releaseProvider,this.releasePublicKeys=e.releasePublicKeys,this.trustedDownloadOrigin=e.trustedDownloadOrigin,this.maxDownloadRetries=e.maxDownloadRetries??1/0,this.maxBackoffMs=e.maxBackoffMs??60000,this.initialDelayMs=e.initialDelayMs??1000,this.staleMs=e.staleMs??0,this.log=e.log,this.success=e.success,this.warn=e.warn,this.error=e.error}async ensureUpToDate(e){await this.lock.wait(),this.lock.lock();try{let t=this.getFilePath(),o=e||!this.isLoaded;if(this.isLoaded&&!e&&this.staleMs>0){let r=Date.now()-this.lastLoadedTime;if(r>=this.staleMs)o=!0,this.log?.(`${this.displayName} is stale (${Math.round(r/1000)}s old), checking for updates...`)}if(!o)return t;let n=await y(t).catch(()=>null),s,a=0,i=this.initialDelayMs;this.log?.(n?"Checking for updates...":`Downloading ${this.displayName}...`);while(a<this.maxDownloadRetries)try{s=await this.fetchReleaseInfo();break}catch(r){let l=Math.min(i*2,this.maxBackoffMs);if(a++,this.error?.("Update error:",r),this.warn?.(`Check for updates attempt ${a} failed: ${r.message}. Retrying in ${l>=60000?`${~~(l/60000)}m`:`${~~(l/1000)}s`}...`),a>=this.maxDownloadRetries)break;await new Promise((h)=>setTimeout(h,i)),i=l}if(!s){if(n)return this.warn?.("Failed to check for updates. Using existing local version."),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Loaded existing ${this.displayName} version`),t;throw Error("[AutoUpdater] Failed to check for updates and no local file found.")}if(this.validateReleaseInfo(s),n){if(p("sha256").update(n).digest("hex")===s.hash)return this.success?.(`${this.displayName} v${s.version} is up to date`),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Finished loading ${this.displayName} v${s.version}`),t;this.log?.(`Downloading update for ${this.displayName} v${s.version}`)}a=0,i=this.initialDelayMs;while(a<this.maxDownloadRetries)try{let r=this.resolveDownloadUrl(s),l=await fetch(r);if(!l.ok)throw Error(`Download failed with status ${l.status}`);let h=Buffer.from(await l.arrayBuffer());if(p("sha256").update(h).digest("hex")!==s.hash)throw Error("Downloaded file hash mismatch...");return await v(this.storageDir,{recursive:!0}).catch(()=>{throw Error(`Failed to create the target directory on ${this.storageDir}`)}),await D(t,h),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Finished loading ${this.displayName} v${s.version}`),t}catch(r){let l=Math.min(i*2,this.maxBackoffMs);a++,this.error?.("Download error:",r),this.warn?.(`Download attempt ${a} failed: ${r.message}. Retrying in ${l>=60000?`${~~(l/60000)}m`:`${~~(l/1000)}s`}...`),await g.wait(i),i=l}if(n)return this.warn?.("Max retries reached. Using existing local version."),this.isLoaded=!0,this.lastLoadedTime=Date.now(),this.success?.(`Loaded existing ${this.displayName} version`),t;throw Error(`[AutoUpdater] Failed to download and verify update after ${a} attempts`)}finally{this.lock.unlock()}}getFilePath(){return b(this.storageDir,this.targetFileName)}async fetchReleaseInfo(){if(this.releaseProvider)return this.releaseProvider();if(!this.releaseUrl)throw Error("No release provider configured");let e=await fetch(this.releaseUrl);if(!e.ok)throw Error(`Status code: ${e.status}`);return await e.json()}validateReleaseInfo(e){if(!this.releasePublicKeys||Object.keys(this.releasePublicKeys).length===0)return;if(P(e,this.releasePublicKeys)!=="verified")throw Error("Invalid release signature");this.log?.(`Verified release signature for v${e.version}`)}resolveDownloadUrl(e){if(!this.trustedDownloadOrigin)return e.url;let t=L(e,this.trustedDownloadOrigin);if(t!==e.url)this.warn?.("Release url mismatch, using trusted origin path");return t}}var w={"fnlb-release-key-2026-06":"MCowBQYDK2VwAyEA2TWIe/P4JlHT4sK1jJn1o52fsCUqUXAfCH1xK71TMuE="};class u{static wait(e){return new Promise((t)=>setTimeout(t,e))}}class d{config;activeProcesses=new Map;packageName=`${process.versions.bun?"zenith-bun":"zenith"}`;fnlbDir;updater;lastChannel;shouldRestart=!0;runId=0;constructor(e){this.config=e,this.fnlbDir=e?.fnlbPath?f(e?.fnlbPath,".fnlb"):f(process.cwd(),".fnlb"),this.setupUpdater(e?.channel??"stable")}setupUpdater(e){this.updater=new c({storageDir:this.fnlbDir,targetFileName:`${this.packageName}.mjs`,displayName:"FNLB",releaseUrl:`https://dist.fnlb.net/packages/${this.packageName}/release?channel=${encodeURIComponent(e)}`,releasePublicKeys:w,trustedDownloadOrigin:"https://cdn.fnlb.net",maxDownloadRetries:this.config?.maxDownloadRetries??1/0,maxBackoffMs:this.config?.maxBackoffMs??60000,staleMs:this.config?.updateIntervalMs??3600000,log:(...t)=>this.log(...t),success:(...t)=>this.success(...t),warn:(...t)=>this.warn(...t),error:(...t)=>this.error(...t)}),this.lastChannel=e}async start(e){await this.stop(),this.shouldRestart=!0,this.runId++;let t=this.runId,o=this.resolveAuthToken(e);if(!o)throw Error("[FNLB ShardingManager] Please provide an auth token.");let n=e.channel??this.config?.channel??"stable";if(n!==this.lastChannel)this.setupUpdater(n),await this.update(!0);else await this.update();let s=e.numberOfShards??1,a=(~~(Math.random()*1e4)).toString(36)+"fnlb"+(~~(Date.now()/1000)).toString(36);for(let i=0;i<s;i++){let r=`${a}-${i.toString().padStart(2,"0")}`,l=await this.startShard(e,r,t,o);this.activeProcesses.set(r,l)}}async stop(){if(this.shouldRestart=!1,this.runId++,this.activeProcesses.size===0)return;this.log("Stopping all active shards...");for(let[e,t]of this.activeProcesses)this.log(`Stopping shard with ID: ${e}`),t.kill();this.activeProcesses.clear(),this.log("All shards stopped.")}async startShard(e,t,o,n){let s=n??this.resolveAuthToken(e);if(!s||s.length<10)throw Error("[FNLB ShardingManager] Please provide a valid auth token.");this.log("Starting shard with ID:",t);let a=R(f(this.fnlbDir,`${this.packageName}.mjs`),[],{env:{...process.env,FORCE_COLOR:"1",SHARD_ID:t,API_TOKEN:s,...e.categories?.length?{CATEGORIES:e.categories.join(",")}:{},...e.bots?.length?{BOTS:e.bots.join(",")}:{},BOTS_PER_SHARD:(e.botsPerShard??1).toString(),HIDE_USERNAMES:e.hideUsernames?"true":"false",HIDE_EMAILS:e.hideEmails?"true":"false",LOG_LEVEL:e.logLevel,CLUSTER_ID:this.config?.clusterName?.trim().replace(/ +(?= )/g,"").toLowerCase().replaceAll(" ","-")??"unknown",CLUSTER_NAME:this.config?.clusterName?.trim(),FNLB_DIR:this.fnlbDir,...e.extraEnv},stdio:["inherit","pipe","pipe","ipc"]});if(!this.config?.disableSubProcessLogs)a.stdout?.on("data",(i)=>{let r=i.toString("utf8");process.stdout.write(r),this.config?.onSubProcessLogMessage?.({timestamp:Date.now(),content:r,format:0})});if(!this.config?.disableSubProcessErrorLogs)a.stderr?.on("data",(i)=>{let r=i.toString("utf8");process.stderr.write(r),this.config?.onSubProcessLogMessage?.({timestamp:Date.now(),content:r,format:4})});return a.on("close",async(i)=>{if(this.activeProcesses.delete(t),this.shouldRestart&&o===this.runId){if(i===0)this.warn("Shard exited with code:",i);else this.error("Shard exited with code:",i?.toString()??"none");this.log("Trying to restart shard..."),await this.update(!0),await u.wait(1e4);let r=await this.startShard(e,t,o,s);this.activeProcesses.set(t,r)}else this.log(`Shard ${t} stopped.`)}),a}async update(e){await this.updater.ensureUpToDate(e)}resolveAuthToken(e){return(e.token??e.apiToken)?.trim()||void 0}log(...e){if(!this.config?.disableLogs)console.log("[FNLB ShardingManager]",...e),this.config?.onLogMessage?.({timestamp:Date.now(),content:e.join(" "),format:0})}success(...e){if(!this.config?.disableLogs)console.log("[FNLB ShardingManager] [OK]",...e),this.config?.onLogMessage?.({timestamp:Date.now(),content:e.join(" "),format:1})}warn(...e){if(!this.config?.disableErrorLogs)console.warn("[FNLB ShardingManager] [WRN]",...e),this.config?.onLogMessage?.({timestamp:Date.now(),content:e.join(" "),format:3})}error(...e){if(!this.config?.disableErrorLogs)console.error("[FNLB ShardingManager] [ERR]",...e),this.config?.onLogMessage?.({timestamp:Date.now(),content:e.join(" "),format:4})}}var $;((o)=>{o.Info="INFO";o.Debug="DEBUG"})($||={});var V=d;export{V as default,$ as LogLevel};
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fnlb",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.17",
|
|
4
4
|
"author": "FNLB",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"homepage": "https://fnlb.net",
|
|
7
7
|
"devDependencies": {
|
|
8
8
|
"@biomejs/biome": "^2.5.1",
|
|
9
9
|
"@types/bun": "^1.3.14",
|
|
10
|
-
"@types/node": "^22.
|
|
10
|
+
"@types/node": "^22.20.0",
|
|
11
11
|
"bun-plugin-dts": "^0.3.0"
|
|
12
12
|
},
|
|
13
13
|
"description": "Easily run your own bot using FNLB, a powerful and scalable system for managing Fortnite bots.",
|
|
@@ -38,6 +38,6 @@
|
|
|
38
38
|
"type": "module",
|
|
39
39
|
"types": "dist/index.d.ts",
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@fnlb-project/shared": "^1.5.
|
|
41
|
+
"@fnlb-project/shared": "^1.5.138"
|
|
42
42
|
}
|
|
43
43
|
}
|