post-armor 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/LICENSE +21 -0
- package/README.md +143 -0
- package/dist/post-armor.d.ts +42 -0
- package/dist/post-armor.js +201 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Evelocore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="https://cdn.evelocore.com/files/Evelocore/projects/post-armor/icon.png" alt="PostArmor Logo" width="200" height="200">
|
|
3
|
+
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
# PostArmor 🛡️
|
|
7
|
+
|
|
8
|
+
[](https://badge.fury.io/js/post-armor)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
**PostArmor** is a zero-dependency, isomorphic security middleware designed to armor your API requests with time-locked encryption and binary obfuscation. It ensures your API requests are valid only within a specific time window and protects the payload from casual inspection.
|
|
12
|
+
|
|
13
|
+
Works seamlessly in:
|
|
14
|
+
- ✅ **Node.js** (Express, Nest, Hono, etc.)
|
|
15
|
+
- ✅ **Browsers** (React, Vue, Svelte, Angular, Vanilla JS)
|
|
16
|
+
|
|
17
|
+
## ✨ Features
|
|
18
|
+
|
|
19
|
+
- **⏳ Time-Locked Validation**: prevent replay attacks by validating requests only within a short time window (default: 5s).
|
|
20
|
+
- **🔒 Binary Obfuscation**: Payload is converted to a binary stream and XOR-encrypted to prevent easy reading in the Network tab.
|
|
21
|
+
- **📦 Zero Dependencies**: Lightweight and fast. No `Buffer` or `Express` runtime dependencies in the client bundle.
|
|
22
|
+
- **🌍 Isomorphic**: Import the same package in your backend and frontend.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📦 Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install post-armor
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🚀 Usage
|
|
35
|
+
|
|
36
|
+
### 1. Server-Side (Node.js + Express)
|
|
37
|
+
|
|
38
|
+
Wrap your protected endpoint with the `postArmor` middleware. This adds a `res.return()` method to send armored responses back.
|
|
39
|
+
|
|
40
|
+
> **Important**: You must use a raw body parser so PostArmor can handle the binary decoding itself.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import express from "express";
|
|
44
|
+
import { postArmor } from "post-armor";
|
|
45
|
+
|
|
46
|
+
const app = express();
|
|
47
|
+
|
|
48
|
+
// 1. Allow PostArmor to handle binary streams
|
|
49
|
+
app.use(express.raw({ type: "application/octet-stream" }));
|
|
50
|
+
|
|
51
|
+
// 2. Configure the Guard
|
|
52
|
+
const armorGate = postArmor({
|
|
53
|
+
key: "YOUR_SECRET_KEY", // Must match client key
|
|
54
|
+
delay: 5, // Allowed time difference in seconds
|
|
55
|
+
strict: true // Enforce strict response typing
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 3. Protect your route
|
|
59
|
+
app.post("/secure-api", armorGate, (req, res) => {
|
|
60
|
+
// req.body is automatically decoded to a JSON object here
|
|
61
|
+
console.log("Received:", req.body);
|
|
62
|
+
|
|
63
|
+
// Send an armored response back
|
|
64
|
+
res.return("object", { success: true, message: "Secure data" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
app.listen(3000, () => console.log("Server running"));
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
#### TypeScript Support (Express)
|
|
71
|
+
To get IntelliSense for `res.return`, add this to your `types.d.ts` or main server file:
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
declare global {
|
|
75
|
+
namespace Express {
|
|
76
|
+
interface Response {
|
|
77
|
+
return: (type: "string" | "number" | "boolean" | "object" | "any", body: any) => void;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 2. Client-Side (React / Vue / Browser)
|
|
84
|
+
|
|
85
|
+
Use `armoredPost` to send secure requests. This is a wrapper around `fetch` that handles the encryption/decryption handshake.
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { armoredPost } from "post-armor";
|
|
89
|
+
|
|
90
|
+
async function sendData() {
|
|
91
|
+
try {
|
|
92
|
+
const response = await armoredPost({
|
|
93
|
+
url: "http://localhost:3000/secure-api",
|
|
94
|
+
key: "YOUR_SECRET_KEY", // Must match server key
|
|
95
|
+
body: { secret: "data" }
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (response.ok) {
|
|
99
|
+
console.log("Server Response:", response.body);
|
|
100
|
+
} else {
|
|
101
|
+
console.error("Error:", response.statusText);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
console.error("Request failed:", err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## ⚙️ Configuration
|
|
112
|
+
|
|
113
|
+
### Server: `postArmor(config)`
|
|
114
|
+
|
|
115
|
+
| Option | Type | Default | Description |
|
|
116
|
+
|---|---|---|---|
|
|
117
|
+
| `key` | `string` | **Required** | Shared secret key for encryption. |
|
|
118
|
+
| `delay` | `number` | `5` | Validity window in seconds. |
|
|
119
|
+
| `strict` | `boolean` | `true` | Enforce strict type checking on `res.return`. |
|
|
120
|
+
| `headerName` | `string` | `"post-armor-token"` | Custom header name for the secure token. |
|
|
121
|
+
| `sourceName` | `string` | `"post-armor"` | Custom source name in payload metadata. |
|
|
122
|
+
|
|
123
|
+
### Client: `armoredPost(options)`
|
|
124
|
+
|
|
125
|
+
| Option | Type | Default | Description |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| `url` | `string` | **Required** | Target API URL. |
|
|
128
|
+
| `key` | `string` | **Required** | Shared secret key. |
|
|
129
|
+
| `body` | `any` | **Required** | JSON payload to send. |
|
|
130
|
+
| `headers` | `object` | `{}` | Additional headers to include. |
|
|
131
|
+
| `headerName` | `string` | `"post-armor-token"` | Custom header name. |
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 🛡️ How it works
|
|
136
|
+
1. **Client** generates a timestamp, encrypts it with the `key`, and sends it in a header.
|
|
137
|
+
2. **Client** encrypts requests body to a binary stream.
|
|
138
|
+
3. **Server** validates the header timestamp is within the `delay` window (preventing replays).
|
|
139
|
+
4. **Server** decrypts binary body to JSON.
|
|
140
|
+
5. **Response** follows the same encrypted path back to the client.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export declare function getTimestamp(): string;
|
|
2
|
+
export declare function getSecureToken(key: string): string;
|
|
3
|
+
export declare function validateToken(token: string | undefined, key: string, delay?: number): boolean;
|
|
4
|
+
export interface SecurePostOptions {
|
|
5
|
+
url: string;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body: any;
|
|
8
|
+
key: string;
|
|
9
|
+
headerName?: string;
|
|
10
|
+
sourceName?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Sends a secure POST request with the given options.
|
|
14
|
+
* @param options The options for the request.
|
|
15
|
+
* @returns A Promise that resolves to the response from the server.
|
|
16
|
+
*/
|
|
17
|
+
export declare function armoredPost({ url, headers, body, key, headerName, sourceName, }: SecurePostOptions): Promise<{
|
|
18
|
+
status: number;
|
|
19
|
+
statusText: string;
|
|
20
|
+
body: undefined;
|
|
21
|
+
type: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function";
|
|
22
|
+
ok: boolean;
|
|
23
|
+
url: string;
|
|
24
|
+
redirected: boolean;
|
|
25
|
+
headers: Headers;
|
|
26
|
+
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Configuration for PostArmor middleware.
|
|
29
|
+
*/
|
|
30
|
+
export interface PostArmorConfig {
|
|
31
|
+
key: string;
|
|
32
|
+
delay?: number;
|
|
33
|
+
strict?: boolean;
|
|
34
|
+
headerName?: string;
|
|
35
|
+
sourceName?: string;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Creates a PostArmor middleware instance with the given configuration.
|
|
39
|
+
* This middleware is built using Web Standards and can be safely included in
|
|
40
|
+
* browser bundles without causing errors.
|
|
41
|
+
*/
|
|
42
|
+
export declare function postArmor(config: PostArmorConfig): (req: any, res: any, next: any) => void;
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const LIB_VERSION = "1.0.0";
|
|
2
|
+
export function getTimestamp() {
|
|
3
|
+
const date = new Date();
|
|
4
|
+
return date
|
|
5
|
+
.toLocaleTimeString("en-GB", {
|
|
6
|
+
timeZone: "Asia/Kolkata",
|
|
7
|
+
hour12: false,
|
|
8
|
+
hour: "2-digit",
|
|
9
|
+
minute: "2-digit",
|
|
10
|
+
second: "2-digit",
|
|
11
|
+
})
|
|
12
|
+
.replace(/:/g, ""); // Returns "HHmmss"
|
|
13
|
+
}
|
|
14
|
+
function encryptTime(time, key) {
|
|
15
|
+
let encrypted = "";
|
|
16
|
+
for (let i = 0; i < time.length; i++) {
|
|
17
|
+
const charCode = time.charCodeAt(i) ^ key.charCodeAt(i % key.length);
|
|
18
|
+
encrypted += String.fromCharCode(charCode);
|
|
19
|
+
}
|
|
20
|
+
const random1 = Math.random().toString(36).substring(2, 7);
|
|
21
|
+
const random2 = Math.random().toString(36).substring(2, 7);
|
|
22
|
+
// Reverse the XOR result
|
|
23
|
+
encrypted = encrypted.split("").reverse().join("");
|
|
24
|
+
// Combine with random salts and encode to Base64 (stripping padding)
|
|
25
|
+
return btoa(random1 + encrypted + random2).replace(/=/g, "");
|
|
26
|
+
}
|
|
27
|
+
function decryptTime(raw, key) {
|
|
28
|
+
if (raw.length < 10)
|
|
29
|
+
return "";
|
|
30
|
+
let encrypted = raw.substring(5, raw.length - 5);
|
|
31
|
+
encrypted = encrypted.split("").reverse().join("");
|
|
32
|
+
let decrypted = "";
|
|
33
|
+
for (let i = 0; i < encrypted.length; i++) {
|
|
34
|
+
decrypted += String.fromCharCode(encrypted.charCodeAt(i) ^ key.charCodeAt(i % key.length));
|
|
35
|
+
}
|
|
36
|
+
return decrypted;
|
|
37
|
+
}
|
|
38
|
+
export function getSecureToken(key) {
|
|
39
|
+
const time = getTimestamp();
|
|
40
|
+
return encryptTime(time, key);
|
|
41
|
+
}
|
|
42
|
+
export function validateToken(token, key, delay = 5) {
|
|
43
|
+
try {
|
|
44
|
+
if (!token)
|
|
45
|
+
return false;
|
|
46
|
+
// Restore Base64 padding if missing
|
|
47
|
+
const paddedToken = token.padEnd(token.length + ((4 - (token.length % 4)) % 4), "=");
|
|
48
|
+
const raw = atob(paddedToken);
|
|
49
|
+
const decryptedTime = decryptTime(raw, key);
|
|
50
|
+
if (decryptedTime.length !== 6)
|
|
51
|
+
return false;
|
|
52
|
+
const serverTime = getTimestamp();
|
|
53
|
+
const toSeconds = (t) => {
|
|
54
|
+
const h = parseInt(t.substring(0, 2), 10);
|
|
55
|
+
const m = parseInt(t.substring(2, 4), 10);
|
|
56
|
+
const s = parseInt(t.substring(4, 6), 10);
|
|
57
|
+
return h * 3600 + m * 60 + s;
|
|
58
|
+
};
|
|
59
|
+
const serverSec = toSeconds(serverTime);
|
|
60
|
+
const clientSec = toSeconds(decryptedTime);
|
|
61
|
+
let diff = serverSec - clientSec;
|
|
62
|
+
// Handle midnight wrap-around
|
|
63
|
+
if (diff < -80000)
|
|
64
|
+
diff += 86400;
|
|
65
|
+
if (diff > 80000)
|
|
66
|
+
diff -= 86400;
|
|
67
|
+
return Math.abs(diff) <= delay;
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.error("Token validation error:", e?.message);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Sends a secure POST request with the given options.
|
|
76
|
+
* @param options The options for the request.
|
|
77
|
+
* @returns A Promise that resolves to the response from the server.
|
|
78
|
+
*/
|
|
79
|
+
export async function armoredPost({ url, headers = {}, body, key, headerName = "post-armor-token", sourceName = "post-armor", }) {
|
|
80
|
+
const _headers = {
|
|
81
|
+
...headers,
|
|
82
|
+
"Content-Type": "application/octet-stream",
|
|
83
|
+
[headerName]: getSecureToken(key),
|
|
84
|
+
};
|
|
85
|
+
const encoder = new TextEncoder();
|
|
86
|
+
const _body = encoder.encode(JSON.stringify({
|
|
87
|
+
body,
|
|
88
|
+
_: {
|
|
89
|
+
source: sourceName,
|
|
90
|
+
version: LIB_VERSION,
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
const response = await fetch(url, {
|
|
94
|
+
method: "POST",
|
|
95
|
+
headers: _headers,
|
|
96
|
+
body: _body,
|
|
97
|
+
});
|
|
98
|
+
let received = {
|
|
99
|
+
body: undefined,
|
|
100
|
+
type: "undefined",
|
|
101
|
+
_: {
|
|
102
|
+
source: "",
|
|
103
|
+
version: "",
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
if (response.status == 200) {
|
|
107
|
+
const buffer = await response.arrayBuffer();
|
|
108
|
+
const decoder = new TextDecoder();
|
|
109
|
+
const text = decoder.decode(buffer);
|
|
110
|
+
try {
|
|
111
|
+
received = JSON.parse(text);
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
throw new Error("Failed to parse armored response");
|
|
115
|
+
}
|
|
116
|
+
if (received._?.source !== sourceName || received._?.version !== LIB_VERSION) {
|
|
117
|
+
throw new Error(`Invalid response source: expected ${sourceName}, got ${received._?.source}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
status: response.status,
|
|
122
|
+
statusText: response.statusText,
|
|
123
|
+
body: received.body || undefined,
|
|
124
|
+
type: typeof received.body,
|
|
125
|
+
ok: response.ok,
|
|
126
|
+
url: response.url,
|
|
127
|
+
redirected: response.redirected,
|
|
128
|
+
headers: response.headers,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Creates a PostArmor middleware instance with the given configuration.
|
|
133
|
+
* This middleware is built using Web Standards and can be safely included in
|
|
134
|
+
* browser bundles without causing errors.
|
|
135
|
+
*/
|
|
136
|
+
export function postArmor(config) {
|
|
137
|
+
const KEY = config.key;
|
|
138
|
+
const MAX_DELAY = config.delay ?? 5;
|
|
139
|
+
const IS_STRICT = config.strict ?? true;
|
|
140
|
+
const HEADER_NAME = (config.headerName || "post-armor-token").toLowerCase();
|
|
141
|
+
const SOURCE_NAME = config.sourceName || "post-armor";
|
|
142
|
+
if (!KEY) {
|
|
143
|
+
throw new Error("PostArmor: config.key is required");
|
|
144
|
+
}
|
|
145
|
+
// Using 'any' for types here to avoid importing 'express' which breaks browser builds.
|
|
146
|
+
// The behavior remains identical when used in an Express app.
|
|
147
|
+
return (req, res, next) => {
|
|
148
|
+
try {
|
|
149
|
+
const token = req.headers[HEADER_NAME];
|
|
150
|
+
if (!validateToken(token, KEY, MAX_DELAY)) {
|
|
151
|
+
console.warn(`[PostArmor] Invalid or expired token for header ${HEADER_NAME}`);
|
|
152
|
+
res.status(401).json({ error: "Unauthorized: Invalid secure token" });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!req.body || (req.body instanceof Uint8Array && req.body.length === 0)) {
|
|
156
|
+
res.status(400).json({ error: "Missing request body" });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
let received;
|
|
160
|
+
try {
|
|
161
|
+
// Buffer is replaced by Uint8Array and TextDecoder for universal compatibility
|
|
162
|
+
const decodedBody = typeof req.body === "string"
|
|
163
|
+
? req.body
|
|
164
|
+
: (req.body instanceof Uint8Array || (typeof Buffer !== "undefined" && Buffer.isBuffer(req.body)))
|
|
165
|
+
? new TextDecoder().decode(req.body)
|
|
166
|
+
: JSON.stringify(req.body);
|
|
167
|
+
received = JSON.parse(decodedBody);
|
|
168
|
+
}
|
|
169
|
+
catch (e) {
|
|
170
|
+
res.status(400).json({ error: "Malformed request payload" });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!received._ || received._.source !== SOURCE_NAME) {
|
|
174
|
+
res.status(400).json({ error: "Invalid request source" });
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// Replace body with actual payload
|
|
178
|
+
req.body = received.body;
|
|
179
|
+
// Custom responder
|
|
180
|
+
res.return = (type, body) => {
|
|
181
|
+
if (IS_STRICT && type !== "any" && typeof body !== type) {
|
|
182
|
+
throw new Error(`Invalid response type: expected ${type}, got ${typeof body}`);
|
|
183
|
+
}
|
|
184
|
+
const responsePayload = JSON.stringify({
|
|
185
|
+
body,
|
|
186
|
+
_: {
|
|
187
|
+
source: SOURCE_NAME,
|
|
188
|
+
version: LIB_VERSION,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
// Use TextEncoder to return a Uint8Array (compatible with Express res.send)
|
|
192
|
+
res.send(new TextEncoder().encode(responsePayload));
|
|
193
|
+
};
|
|
194
|
+
next();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
console.error(`[PostArmor] Error processing request on ${req.path}:`, error?.message);
|
|
198
|
+
res.status(400).json({ error: "Internal processing error" });
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "post-armor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency binary request obfuscation and time-locked security middleware for Node.js and Browsers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/post-armor.js",
|
|
7
|
+
"module": "./dist/post-armor.js",
|
|
8
|
+
"types": "./dist/post-armor.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist/post-armor.js",
|
|
11
|
+
"dist/post-armor.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/post-armor.js",
|
|
16
|
+
"types": "./dist/post-armor.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"author": "K.Prabhasha",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"keywords": ["post-armor", "security", "middleware", "encryption", "obfuscation", "time-locked", "binary", "request", "nodejs", "express", "browser", "react", "vue"],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"dev": "tsx watch receive.ts",
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"start": "node dist/receive.js",
|
|
26
|
+
"test-client": "tsx web/send.ts"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/cors": "^2.8.17",
|
|
30
|
+
"@types/express": "^4.17.21",
|
|
31
|
+
"@types/node": "^20.11.0",
|
|
32
|
+
"cors": "^2.8.5",
|
|
33
|
+
"express": "^5.2.1",
|
|
34
|
+
"javascript-obfuscator": "^5.1.0",
|
|
35
|
+
"tsx": "^4.21.0",
|
|
36
|
+
"typescript": "^5.3.3"
|
|
37
|
+
}
|
|
38
|
+
}
|