@libertytools/libertyjs 1.0.1

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.
Files changed (3) hide show
  1. package/package.json +28 -0
  2. package/readme.md +359 -0
  3. package/src/index.js +309 -0
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@libertytools/libertyjs",
3
+ "version": "1.0.1",
4
+ "description": "An SDK to help developers create apps for the ER:LC Private Server API.",
5
+ "author": "novix",
6
+ "license": "ISC",
7
+
8
+ "type": "module",
9
+
10
+ "main": "./src/index.js",
11
+ "exports": {
12
+ ".": "./src/index.js"
13
+ },
14
+
15
+ "files": [
16
+ "src"
17
+ ],
18
+
19
+ "keywords": [
20
+ "erlc",
21
+ "police roleplay community",
22
+ "liberty county"
23
+ ],
24
+
25
+ "engines": {
26
+ "node": ">=18"
27
+ }
28
+ }
package/readme.md ADDED
@@ -0,0 +1,359 @@
1
+ # LibertyJS
2
+
3
+ A lightweight SDK for the **ER:LC Private Server API**, designed to simplify requests, enforce rate limits, and provide structured error handling.
4
+
5
+ ---
6
+
7
+ ## Features
8
+
9
+ * Automatic **rate limit handling** (GET + POST)
10
+ * Built-in **API key validation**
11
+ * **403 fail-safe** (halts after repeated failures)
12
+ * Optional **webhook integration**
13
+ * Structured **error responses**
14
+ * Automatic **JSON handling for POST requests**
15
+ * Built-in **command validation + argument checking**
16
+
17
+ ---
18
+
19
+ ## Importing
20
+
21
+ ```js
22
+ import LibertyJS from "libertyjs";
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Initialization
28
+
29
+ ```js
30
+ const LJS = new LibertyJS({
31
+ SERVER_KEY: process.env.SERVER_KEY,
32
+ PRIVATE_SERVER_API: "https://api.policeroleplay.community/v2/", // optional
33
+ WEBHOOK_URL: process.env.WEBHOOK_URL, // optional
34
+ WEBHOOK_TOKEN: process.env.WEBHOOK_TOKEN // required if WEBHOOK_URL is used
35
+ });
36
+ ```
37
+
38
+ ---
39
+
40
+ ## Configuration Options
41
+
42
+ | Option | Required | Description |
43
+ | -------------------- | -------- | ------------------------------------- |
44
+ | `SERVER_KEY` | ✅ | ER:LC private server API key |
45
+ | `PRIVATE_SERVER_API` | ❌ | Base API URL (defaults to PRC v2) |
46
+ | `WEBHOOK_URL` | ❌ | Webhook base URL |
47
+ | `WEBHOOK_TOKEN` | ⚠️ | Required if `WEBHOOK_URL` is provided |
48
+
49
+ ---
50
+
51
+ # Methods
52
+
53
+ ---
54
+
55
+ ## `getPrivateServerAPI(options, includeInvalid?)`
56
+
57
+ Fetch data from the ER:LC Private Server API.
58
+
59
+ ### Example
60
+
61
+ ```js
62
+ const data = await LJS.getPrivateServerAPI([
63
+ "Players",
64
+ "Staff"
65
+ ]);
66
+ ```
67
+
68
+ ---
69
+
70
+ ### Parameters
71
+
72
+ | Parameter | Type | Description |
73
+ | ---------------- | ---------- | ----------------------------------- |
74
+ | `options` | `string[]` | List of data types to request |
75
+ | `includeInvalid` | `boolean` | Include invalid options in response |
76
+
77
+ ---
78
+
79
+ ### Valid Options
80
+
81
+ * Players
82
+ * Staff
83
+ * JoinLogs
84
+ * Queue
85
+ * KillLogs
86
+ * CommandLogs
87
+ * ModCalls
88
+ * EmergencyCalls
89
+ * Vehicles
90
+
91
+ ---
92
+
93
+ ### Behavior
94
+
95
+ * Automatically appends `/server`
96
+ * Builds query string internally
97
+ * Filters invalid options
98
+ * Waits automatically if rate-limited
99
+
100
+ ---
101
+
102
+ ### Example with Invalid Tracking
103
+
104
+ ```js
105
+ const res = await LJS.getPrivateServerAPI(
106
+ ["Players", "InvalidOption"],
107
+ true
108
+ );
109
+
110
+ console.log(res);
111
+ /*
112
+ {
113
+ data: {...},
114
+ invalidOptions: ["InvalidOption"]
115
+ }
116
+ */
117
+ ```
118
+
119
+ ---
120
+
121
+ ### Errors
122
+
123
+ #### Invalid Input
124
+
125
+ ```js
126
+ {
127
+ error: "invalid_input",
128
+ message: "[LibertyJS.getPrivateServerAPI]: Options must be an array"
129
+ }
130
+ ```
131
+
132
+ ---
133
+
134
+ #### Forbidden (after 2 failures)
135
+
136
+ ```js
137
+ {
138
+ error: "forbidden",
139
+ message: "[LibertyJS]: Received a 403 error 2 times, suspending API calls as the server key may be invalid"
140
+ }
141
+ ```
142
+
143
+ ---
144
+
145
+ #### API Error
146
+
147
+ ```js
148
+ {
149
+ error: "api-error",
150
+ message: "[LibertyJS]: Encountered an error while attempting to fetch <url>",
151
+ apiResponse: { ... }
152
+ }
153
+ ```
154
+
155
+ ---
156
+
157
+ ## `sendPrivateServerCommand(commands)`
158
+
159
+ Send one or more commands to the private server.
160
+
161
+ ---
162
+
163
+ ### Example
164
+
165
+ ```js
166
+ const res = await LJS.sendPrivateServerCommand([
167
+ ":h Hello world!",
168
+ ":kick PlayerName Spamming"
169
+ ]);
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Parameters
175
+
176
+ | Parameter | Type | Description |
177
+ | ---------- | ---------- | ------------------------ |
178
+ | `commands` | `string[]` | Array of command strings |
179
+
180
+ ---
181
+
182
+ ### Behavior
183
+
184
+ * Validates:
185
+
186
+ * Command existence
187
+ * Argument count
188
+ * Ignores:
189
+
190
+ * Invalid commands
191
+ * Incorrect argument usage
192
+ * Sends commands **sequentially**
193
+ * Tracks success/failure per command
194
+
195
+ ---
196
+
197
+ ### Response
198
+
199
+ ```js
200
+ {
201
+ successes: 2,
202
+ failures: 0,
203
+ failureReasons: []
204
+ }
205
+ ```
206
+
207
+ ---
208
+
209
+ ### Failure Example
210
+
211
+ ```js
212
+ {
213
+ successes: 1,
214
+ failures: 1,
215
+ failureReasons: [
216
+ {
217
+ command: ":kick Player",
218
+ apiResponse: { ... }
219
+ }
220
+ ]
221
+ }
222
+ ```
223
+
224
+ ---
225
+
226
+ ### Errors
227
+
228
+ #### Invalid Input
229
+
230
+ ```js
231
+ {
232
+ error: "invalid_input",
233
+ message: "[LibertyJS.sendPrivateServerCommand]: Options must be an array"
234
+ }
235
+ ```
236
+
237
+ #### Empty Array
238
+
239
+ ```js
240
+ {
241
+ error: "invalid_input",
242
+ message: "[LibertyJS.sendPrivateServerCommand]: Options must have at least one item"
243
+ }
244
+ ```
245
+
246
+ ---
247
+
248
+ # Webhook API
249
+
250
+ > Webhooks are accessed via a **nested object**, not top-level methods.
251
+
252
+ ---
253
+
254
+ ## `webhook.status()`
255
+
256
+ Check webhook health.
257
+
258
+ ### Example
259
+
260
+ ```js
261
+ const res = await LJS.webhook.status();
262
+ ```
263
+
264
+ ---
265
+
266
+ ### Errors
267
+
268
+ #### Webhook Disabled
269
+
270
+ ```js
271
+ {
272
+ error: "webhook_disabled",
273
+ message: "[LibertyJS.webhook.status]: Webhook is not configured"
274
+ }
275
+ ```
276
+
277
+ ---
278
+
279
+ ## `webhook.events()`
280
+
281
+ Fetch webhook events.
282
+
283
+ ### Example
284
+
285
+ ```js
286
+ const events = await LJS.webhook.events();
287
+ ```
288
+
289
+ ---
290
+
291
+ ### Errors
292
+
293
+ #### Webhook Disabled
294
+
295
+ ```js
296
+ {
297
+ error: "webhook_disabled",
298
+ message: "[LibertyJS.webhook.events]: Webhook is not configured"
299
+ }
300
+ ```
301
+
302
+ ---
303
+
304
+ # Internal Behavior
305
+
306
+ ---
307
+
308
+ ## Rate Limiting
309
+
310
+ Tracked separately for GET and POST:
311
+
312
+ ```js
313
+ {
314
+ get: { limit, remaining, reset },
315
+ post: { limit, remaining, reset }
316
+ }
317
+ ```
318
+
319
+ ---
320
+
321
+ ### Details
322
+
323
+ * Uses headers:
324
+
325
+ * `X-RateLimit-Limit`
326
+ * `X-RateLimit-Remaining`
327
+ * `X-RateLimit-Reset`
328
+ * Automatically delays requests when:
329
+
330
+ ```
331
+ remaining === 0 && currentTime < reset
332
+ ```
333
+
334
+ * Logs:
335
+
336
+ ```
337
+ [LibertyJS]: Rate limited (GET/POST). Waiting Xs
338
+ ```
339
+
340
+ ---
341
+
342
+ ## Forbidden Protection
343
+
344
+ After **2 consecutive `403` responses**:
345
+
346
+ * All future API calls are blocked
347
+ * Prevents invalid API key spam
348
+
349
+ ---
350
+
351
+ ## Request Handling (`#fetchAPI`)
352
+
353
+ Handles:
354
+
355
+ * Authentication headers (`server-key`)
356
+ * JSON parsing (safe fallback to `null`)
357
+ * Rate limit tracking
358
+ * Error normalization
359
+ * Automatic JSON stringification for POST bodies
package/src/index.js ADDED
@@ -0,0 +1,309 @@
1
+ export default class LibertyJS {
2
+ #SERVER_KEY;
3
+ #PRIVATE_SERVER_API;
4
+ #useWebhook;
5
+ #WEBHOOK_URL;
6
+ #WEBHOOK_TOKEN;
7
+ #rateLimits;
8
+ #resStatus;
9
+
10
+ constructor({
11
+ SERVER_KEY,
12
+ PRIVATE_SERVER_API = "https://api.policeroleplay.community/v2/",
13
+ WEBHOOK_URL,
14
+ WEBHOOK_TOKEN
15
+ } = {}) {
16
+ if (!SERVER_KEY) {
17
+ throw new Error("[LibertyJS]: SERVER_KEY is required");
18
+ }
19
+
20
+ this.#SERVER_KEY = SERVER_KEY;
21
+ this.#PRIVATE_SERVER_API = PRIVATE_SERVER_API;
22
+
23
+ if (WEBHOOK_URL && !WEBHOOK_TOKEN) {
24
+ throw new Error("[LibertyJS]: WEBHOOK_TOKEN is required if you are using a webhook");
25
+ }
26
+
27
+ this.#useWebhook = Boolean(WEBHOOK_URL && WEBHOOK_TOKEN);
28
+
29
+ if (WEBHOOK_URL && WEBHOOK_TOKEN) {
30
+ this.#WEBHOOK_URL = WEBHOOK_URL;
31
+ this.#WEBHOOK_TOKEN = WEBHOOK_TOKEN;
32
+ }
33
+
34
+ this.#rateLimits = {
35
+ get: { limit: null, remaining: null, reset: null },
36
+ post: { limit: null, remaining: null, reset: null }
37
+ };
38
+
39
+ this.#resStatus = {
40
+ forbiddenErrors: 0
41
+ };
42
+ }
43
+
44
+ #wait(seconds) {
45
+ return new Promise(resolve => setTimeout(resolve, seconds * 1000));
46
+ }
47
+
48
+ async #fetchAPI(url, options = {}) {
49
+ if (!this.#SERVER_KEY) {
50
+ return {
51
+ error: "invalid_env",
52
+ message: "[LibertyJS]: SERVER_KEY was not provided in a .env file"
53
+ };
54
+ }
55
+
56
+ if (this.#resStatus.forbiddenErrors >= 2) {
57
+ return {
58
+ error: "forbidden",
59
+ message: "[LibertyJS]: Received a 403 error 2 times, suspending API calls as the server key may be invalid"
60
+ };
61
+ }
62
+
63
+ const isPRC = url.startsWith("https://api.policeroleplay.community/");
64
+ const method = (options.method || "GET").toUpperCase();
65
+
66
+ const headers = { ...(options.headers || {}) };
67
+
68
+ let body = options.body;
69
+ if (method === "POST" && body !== undefined) {
70
+ headers["Content-Type"] = "application/json";
71
+ body = typeof body === "string" ? body : JSON.stringify(body);
72
+ }
73
+
74
+ if (isPRC) {
75
+ headers["server-key"] = this.#SERVER_KEY;
76
+ }
77
+
78
+ const currentTime = Math.floor(Date.now() / 1000);
79
+
80
+ const handleRateLimit = async (rl) => {
81
+ if (rl.reset && rl.remaining === 0 && currentTime < rl.reset) {
82
+ const seconds = Math.max(0, rl.reset - currentTime);
83
+ console.log(`[LibertyJS]: Rate limited (${method}). Waiting ${seconds}s`);
84
+ await this.#wait(seconds);
85
+ }
86
+ };
87
+
88
+ if (isPRC) {
89
+ if (method === "GET") await handleRateLimit(this.#rateLimits.get);
90
+ if (method === "POST") await handleRateLimit(this.#rateLimits.post);
91
+ }
92
+
93
+ const res = await fetch(url, {
94
+ ...options,
95
+ method,
96
+ headers,
97
+ body
98
+ });
99
+
100
+ let data;
101
+ try {
102
+ data = await res.json();
103
+ } catch {
104
+ data = null;
105
+ }
106
+
107
+ if (!res.ok) {
108
+ if (res.status === 403 && isPRC) {
109
+ this.#resStatus.forbiddenErrors++;
110
+ }
111
+
112
+ return {
113
+ error: "api-error",
114
+ message: `[LibertyJS]: Encountered an error while attempting to fetch ${url}`,
115
+ apiResponse: data
116
+ };
117
+ }
118
+
119
+ if (isPRC) {
120
+ const target = method === "GET"
121
+ ? this.#rateLimits.get
122
+ : this.#rateLimits.post;
123
+
124
+ target.reset = Number(res.headers.get("X-RateLimit-Reset"));
125
+ target.limit = Number(res.headers.get("X-RateLimit-Limit"));
126
+ target.remaining = Number(res.headers.get("X-RateLimit-Remaining"));
127
+ }
128
+
129
+ return data;
130
+ }
131
+
132
+ async getPrivateServerAPI(options = [], includeInvalid = false) {
133
+ const valid = [
134
+ "Players",
135
+ "Staff",
136
+ "JoinLogs",
137
+ "Queue",
138
+ "KillLogs",
139
+ "CommandLogs",
140
+ "ModCalls",
141
+ "EmergencyCalls",
142
+ "Vehicles"
143
+ ];
144
+
145
+ if (!Array.isArray(options)) {
146
+ console.error("[LibertyJS.getPrivateServerAPI]: Options must be an array");
147
+ return {
148
+ error: "invalid_input",
149
+ message: "[LibertyJS.getPrivateServerAPI]: Options must be an array"
150
+ };
151
+ }
152
+
153
+ const params = [];
154
+ const invalidOptions = [];
155
+
156
+ for (const opt of options) {
157
+ if (valid.includes(opt)) {
158
+ params.push(`${opt}=true`);
159
+ } else {
160
+ console.log(`[LibertyJS]: Invalid option "${String(opt)}"`);
161
+ invalidOptions.push(opt);
162
+ }
163
+ }
164
+
165
+ const query = params.length ? `?${params.join("&")}` : "";
166
+ const url = this.#PRIVATE_SERVER_API + "server" + query;
167
+
168
+ if (!includeInvalid) {
169
+ return await this.#fetchAPI(url);
170
+ }
171
+
172
+ return {
173
+ data: await this.#fetchAPI(url),
174
+ invalidOptions
175
+ };
176
+ }
177
+
178
+ async sendPrivateServerCommand(options = []) {
179
+ const url = this.#PRIVATE_SERVER_API + "server/command";
180
+
181
+ const valid = {
182
+ ":wanted": ["Player"],
183
+ ":time": ["Number"],
184
+ ":stopfire": [],
185
+ ":respawn": ["Player"],
186
+ ":tp": ["Player", "Player"],
187
+ ":startnearfire": ["String"],
188
+ ":jail": ["Player"],
189
+ ":pt": ["Number"],
190
+ ":h": ["String"],
191
+ ":m": ["String"],
192
+ ":pm": ["Player", "String"],
193
+ ":refresh": ["Player"],
194
+ ":bring": ["Player"],
195
+ ":heal": ["Player"],
196
+ ":kick": ["Player", "String"],
197
+ ":startfire": ["String"],
198
+ ":unwanted": ["Player"],
199
+ ":prty": ["Number"],
200
+ ":stopdumpsterfire": [],
201
+ ":helper": ["Player/UserId"],
202
+ ":shutdown": [],
203
+ ":weather": ["String"],
204
+ ":unmod": ["Player/UserId"],
205
+ ":unloadlayout": ["String"],
206
+ ":unban": ["String"],
207
+ ":mod": ["Player/UserId"],
208
+ ":ban": ["Player/UserId"],
209
+ ":unhelper": ["Player/UserId"],
210
+ ":log": ["String"],
211
+ ":kill": ["Player"],
212
+ ":unadmin": ["Player/UserId"],
213
+ ":admin": ["Player/UserId"],
214
+ ":loadlayout": ["String"]
215
+ };
216
+
217
+ if (!Array.isArray(options)) {
218
+ console.error("[LibertyJS.sendPrivateServerCommand]: Options must be an array");
219
+ return {
220
+ error: "invalid_input",
221
+ message: "[LibertyJS.sendPrivateServerCommand]: Options must be an array"
222
+ };
223
+ }
224
+
225
+ if (options.length === 0) {
226
+ console.error("[LibertyJS.sendPrivateServerCommand]: Options must have at least one item");
227
+ return {
228
+ error: "invalid_input",
229
+ message: "[LibertyJS.sendPrivateServerCommand]: Options must have at least one item"
230
+ };
231
+ }
232
+
233
+ const commands = [];
234
+
235
+ for (const cmd of options) {
236
+ if (!cmd.startsWith(":")) continue;
237
+
238
+ const parts = cmd.trim().split(" ");
239
+ const name = parts[0];
240
+ const args = parts.slice(1);
241
+ const schema = valid[name];
242
+
243
+ if (!schema) {
244
+ console.error(`[LibertyJS]: Unsupported command "${name}"`);
245
+ continue;
246
+ }
247
+
248
+ const validArgs =
249
+ (schema.at(-1) === "String" && args.length >= schema.length) ||
250
+ (schema.at(-1) !== "String" && args.length === schema.length);
251
+
252
+ if (validArgs) {
253
+ commands.push(cmd);
254
+ } else {
255
+ console.log(`[LibertyJS]: "${name}" requires ${schema.length}${schema.at(-1) === "String" ? "+" : ""} args`);
256
+ }
257
+ }
258
+
259
+ let successes = 0;
260
+ let failures = 0;
261
+ const failureReasons = [];
262
+
263
+ for (const command of commands) {
264
+ const res = await this.#fetchAPI(url, {
265
+ method: "POST",
266
+ body: command
267
+ });
268
+
269
+ if (!res?.error) {
270
+ successes++;
271
+ } else {
272
+ failures++;
273
+ failureReasons.push({
274
+ command,
275
+ apiResponse: res.apiResponse
276
+ });
277
+ }
278
+ }
279
+
280
+ return { successes, failures, failureReasons };
281
+ }
282
+
283
+ webhook = {
284
+ status: async () => {
285
+ if (!this.#useWebhook) {
286
+ return {
287
+ error: "webhook_disabled",
288
+ message: "[LibertyJS.webhook.status]: Webhook is not configured"
289
+ };
290
+ }
291
+
292
+ const url = `${this.#WEBHOOK_URL}health`;
293
+
294
+ return await this.#fetchAPI(url, { method: "GET" });
295
+ },
296
+ events: async () => {
297
+ if (!this.#useWebhook) {
298
+ return {
299
+ error: "webhook_disabled",
300
+ message: "[LibertyJS.webhook.events]: Webhook is not configured"
301
+ };
302
+ }
303
+
304
+ const url = `${this.#WEBHOOK_URL}webhook/${this.#WEBHOOK_TOKEN}/events`;
305
+
306
+ return await this.#fetchAPI(url, { method: "GET" });
307
+ }
308
+ };
309
+ }