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 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
+ [![npm version](https://badge.fury.io/js/post-armor.svg)](https://badge.fury.io/js/post-armor)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ }