@typescriptprime/securereq 1.0.0 → 1.1.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/README.md +57 -97
- package/dist/index.js +114 -1
- package/dist/index.js.map +2 -2
- package/dist/types/index.d.ts +14 -0
- package/dist/types/type.d.ts +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,145 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# SecureReq 🔐
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
This package provides two small helpers:
|
|
6
|
-
|
|
7
|
-
- `PreProcessing` — trims `process.argv` to remove the node executable and optional script filename, returning the array of option tokens.
|
|
8
|
-
- `PostProcessing` — parses the token list that looks like `--Name Value` into a typed options object and an array of positional arguments (everything after `--`).
|
|
3
|
+
**SecureReq** is a lightweight TypeScript utility for making secure HTTPS requests with strict TLS defaults and typed response parsing.
|
|
9
4
|
|
|
10
5
|
---
|
|
11
6
|
|
|
12
|
-
##
|
|
13
|
-
|
|
14
|
-
```bash
|
|
15
|
-
npm install @typescriptprime/parsing
|
|
16
|
-
```
|
|
7
|
+
## 🚀 Quick Summary
|
|
17
8
|
|
|
18
|
-
|
|
19
|
-
|
|
9
|
+
- **Small, dependency-light** wrapper around Node's `https` for typed responses and safer TLS defaults.
|
|
10
|
+
- Defaults to **TLSv1.3**, Post Quantum Cryptography key exchange, a limited set of strongest ciphers, and a `User-Agent` header.
|
|
11
|
+
- Supports typed response parsing: `JSON`, `String`, or raw `ArrayBuffer`.
|
|
20
12
|
|
|
21
13
|
---
|
|
22
14
|
|
|
23
|
-
##
|
|
24
|
-
|
|
25
|
-
Import the helpers from the package and combine them to parse `process.argv`:
|
|
15
|
+
## 📦 Installation
|
|
26
16
|
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async function Main(Argv: string[]) {
|
|
31
|
-
// Step 1: Trim the node executable and optional script path
|
|
32
|
-
const Filtered = PreProcessing(Argv)
|
|
33
|
-
|
|
34
|
-
// Step 2: Parse CLI-style parameters into an options object and positional arguments
|
|
35
|
-
const { Options, Positional } = await PostProcessing(Filtered)
|
|
36
|
-
|
|
37
|
-
console.log('Options:', Options)
|
|
38
|
-
console.log('Positional args:', Positional)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
Main(process.argv)
|
|
17
|
+
```bash
|
|
18
|
+
npm install @typescriptprime/securereq
|
|
42
19
|
```
|
|
43
20
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
By default, `PostProcessing` converts parameter names into PascalCase using `es-toolkit` (so `--parameter-name` becomes `ParameterName`). You can supply a custom `NamingConvention` function via `IParsingOptions`:
|
|
47
|
-
|
|
48
|
-
```typescript
|
|
49
|
-
import * as ESToolkit from 'es-toolkit'
|
|
50
|
-
|
|
51
|
-
await PostProcessing(argv, { NamingConvention: ESToolkit.camelCase })
|
|
52
|
-
// or custom:
|
|
53
|
-
await PostProcessing(argv, { NamingConvention: (s) => s.replace(/^--/, '') })
|
|
54
|
-
```
|
|
21
|
+
**Requirements:** Node.js >= 24
|
|
55
22
|
|
|
56
23
|
---
|
|
57
24
|
|
|
58
|
-
##
|
|
25
|
+
## Usage Examples 🔧
|
|
59
26
|
|
|
60
|
-
|
|
27
|
+
Import and call the helper:
|
|
61
28
|
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const Tokens = PreProcessing(Argv)
|
|
66
|
-
// tokens === ['--enable-feature', '--parameter', 'value', '--', 'positional1', 'positional2']
|
|
29
|
+
```ts
|
|
30
|
+
import { HTTPSRequest } from '@typescriptprime/securereq'
|
|
67
31
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
32
|
+
// JSON (auto-detected by .json path) or explicit
|
|
33
|
+
const url = new URL('https://api64.ipify.org?format=json')
|
|
34
|
+
const res = await HTTPSRequest(url)
|
|
35
|
+
console.log(res.StatusCode) // number
|
|
36
|
+
console.log(res.Body) // ArrayBuffer or parsed JSON depending on `ExpectedAs` and URL
|
|
72
37
|
|
|
73
|
-
|
|
38
|
+
// Force string
|
|
39
|
+
const html = await HTTPSRequest(new URL('https://www.example.com/'), { ExpectedAs: 'String' })
|
|
40
|
+
console.log(typeof html.Body) // 'string'
|
|
74
41
|
|
|
75
|
-
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
const { Options } = await PostProcessing(Tokens)
|
|
79
|
-
// Options === { Flag: true, Other: 'value' }
|
|
42
|
+
// Force ArrayBuffer
|
|
43
|
+
const raw = await HTTPSRequest(new URL('https://example.com/'), { ExpectedAs: 'ArrayBuffer' })
|
|
44
|
+
console.log(raw.Body instanceof ArrayBuffer)
|
|
80
45
|
```
|
|
81
46
|
|
|
82
47
|
---
|
|
83
48
|
|
|
84
|
-
## API
|
|
85
|
-
|
|
86
|
-
- `PreProcessing(argv: typeof process.argv): string[]`
|
|
87
|
-
- Removes the node executable and optional file argument and returns the tokens starting at the first option.
|
|
49
|
+
## API Reference 📚
|
|
88
50
|
|
|
89
|
-
|
|
90
|
-
- Converts `--key value` pairs into a typed `Options` object; flags without values are treated as `true`.
|
|
91
|
-
- Stops parsing options when it hits `--` and returns the rest as `Positional` arguments.
|
|
51
|
+
### HTTPSRequest(Url, Options?)
|
|
92
52
|
|
|
93
|
-
|
|
53
|
+
- `Url: URL` — Target URL (must be an instance of `URL`).
|
|
54
|
+
- `Options?: HTTPSRequestOptions` — Optional configuration object.
|
|
94
55
|
|
|
95
|
-
|
|
96
|
-
- `IParsingOptions` — currently supports:
|
|
97
|
-
- `NamingConvention?: (PropertyName: string) => string | Promise<string>`
|
|
56
|
+
Returns: `Promise<HTTPSResponse<T>>` where `T` is determined by `ExpectedAs`.
|
|
98
57
|
|
|
99
|
-
|
|
58
|
+
Throws:
|
|
59
|
+
- `TypeError` when `Url` is not a `URL` instance.
|
|
60
|
+
- `Error` on request failure or on failed response parsing (e.g., invalid JSON).
|
|
100
61
|
|
|
101
|
-
|
|
62
|
+
### HTTPSRequestOptions
|
|
102
63
|
|
|
103
|
-
|
|
104
|
-
- `
|
|
105
|
-
- `
|
|
64
|
+
Fields:
|
|
65
|
+
- `TLS?: { IsHTTPSEnforced?: boolean, MinTLSVersion?: 'TLSv1.2'|'TLSv1.3', MaxTLSVersion?: 'TLSv1.2'|'TLSv1.3', Ciphers?: string[], KeyExchanges?: string[] }`
|
|
66
|
+
- Defaults: `IsHTTPSEnforced: true`, both Min and Max set to `TLSv1.3`, a small secure cipher list and key exchange choices.
|
|
67
|
+
- When `IsHTTPSEnforced` is `true`, a non-`https:` URL will throw.
|
|
68
|
+
- `HttpHeaders?: Record<string,string>` — Custom headers. A `User-Agent` header is provided by default.
|
|
69
|
+
- `ExpectedAs?: 'JSON'|'String'|'ArrayBuffer'` — How to parse the response body.
|
|
106
70
|
|
|
107
|
-
|
|
71
|
+
### HTTPSResponse
|
|
108
72
|
|
|
109
|
-
- `
|
|
110
|
-
- `npm run build:tsc` — emit TypeScript declarations only
|
|
73
|
+
- `{ StatusCode: number, Headers: Record<string,string|string[]|undefined>, Body: T }`
|
|
111
74
|
|
|
112
|
-
|
|
75
|
+
Notes:
|
|
76
|
+
- If `ExpectedAs` is omitted, a heuristic is used: `.json` → `JSON`, `.txt` → `String`, otherwise `ArrayBuffer`.
|
|
77
|
+
- When `ExpectedAs` is `JSON`, the body is parsed and an error is thrown if parsing fails.
|
|
113
78
|
|
|
114
79
|
---
|
|
115
80
|
|
|
116
|
-
##
|
|
81
|
+
## Security & Behavior Notes 🔐
|
|
117
82
|
|
|
118
|
-
|
|
83
|
+
- Strict TLS defaults lean on **TLSv1.3** and a reduced cipher list to encourage secure transport out of the box.
|
|
84
|
+
- TLS options are forwarded to Node's HTTPS layer (`minVersion`, `maxVersion`, `ciphers`, `ecdhCurve`).
|
|
85
|
+
- The library uses `zod` for runtime validation of options.
|
|
119
86
|
|
|
120
|
-
|
|
121
|
-
- PostProcessing: parsing single/two parameters, boolean flags, and the `--` positional argument separator.
|
|
87
|
+
---
|
|
122
88
|
|
|
123
|
-
|
|
89
|
+
## Development & Testing 🧪
|
|
124
90
|
|
|
125
|
-
|
|
126
|
-
npm test
|
|
127
|
-
|
|
91
|
+
- Build: `npm run build` (uses `esbuild` + `tsc` for types)
|
|
92
|
+
- Test: `npm test` (uses `ava`)
|
|
93
|
+
- Lint: `npm run lint`
|
|
128
94
|
|
|
129
95
|
---
|
|
130
96
|
|
|
131
|
-
##
|
|
97
|
+
## Contributing
|
|
132
98
|
|
|
133
|
-
|
|
134
|
-
- `index.ts` — entry exports
|
|
135
|
-
- `preprocessing/index.ts` — `PreProcessing` implementation
|
|
136
|
-
- `postprocessing/index.ts` — `PostProcessing` implementation
|
|
137
|
-
- `types.ts` — JSON types and `IParsingOptions`
|
|
138
|
-
- `dist/` — build output (ignored in source control)
|
|
139
|
-
- `tests/` — unit tests using AVA
|
|
99
|
+
Contributions, bug reports and PRs are welcome — please follow the repository's contribution guidelines.
|
|
140
100
|
|
|
141
101
|
---
|
|
142
102
|
|
|
143
103
|
## License
|
|
144
104
|
|
|
145
|
-
|
|
105
|
+
This project is licensed under the **Apache-2.0** License. See the `LICENSE` file for details.
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ var __export = (target, all) => {
|
|
|
6
6
|
|
|
7
7
|
// sources/index.ts
|
|
8
8
|
import * as HTTPS from "node:https";
|
|
9
|
+
import * as HTTP2 from "node:http2";
|
|
9
10
|
import * as TLS from "node:tls";
|
|
10
11
|
import * as Process from "node:process";
|
|
11
12
|
|
|
@@ -4241,6 +4242,7 @@ async function HTTPSRequest(Url, Options) {
|
|
|
4241
4242
|
Ciphers: ["TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"],
|
|
4242
4243
|
KeyExchanges: ["X25519MLKEM768", "X25519"]
|
|
4243
4244
|
},
|
|
4245
|
+
HttpMethod: "GET",
|
|
4244
4246
|
HttpHeaders: {
|
|
4245
4247
|
"User-Agent": `node/${Process.version} ${Process.platform} ${Process.arch} workspace/false`
|
|
4246
4248
|
}
|
|
@@ -4258,6 +4260,7 @@ async function HTTPSRequest(Url, Options) {
|
|
|
4258
4260
|
KeyExchanges: array(string2()).optional()
|
|
4259
4261
|
}).partial().optional(),
|
|
4260
4262
|
HttpHeaders: record(string2(), string2()).optional(),
|
|
4263
|
+
HttpMethod: _enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]).optional(),
|
|
4261
4264
|
ExpectedAs: _enum(["JSON", "String", "ArrayBuffer"]).optional()
|
|
4262
4265
|
}).parseAsync(Options ?? {});
|
|
4263
4266
|
if (MergedOptions.TLS?.IsHTTPSEnforced && Url.protocol !== "https:") {
|
|
@@ -4274,7 +4277,8 @@ async function HTTPSRequest(Url, Options) {
|
|
|
4274
4277
|
minVersion: MergedOptions.TLS?.MinTLSVersion,
|
|
4275
4278
|
maxVersion: MergedOptions.TLS?.MaxTLSVersion,
|
|
4276
4279
|
ciphers: MergedOptions.TLS?.Ciphers?.join(":"),
|
|
4277
|
-
ecdhCurve: MergedOptions.TLS?.KeyExchanges?.join(":")
|
|
4280
|
+
ecdhCurve: MergedOptions.TLS?.KeyExchanges?.join(":"),
|
|
4281
|
+
method: MergedOptions.HttpMethod
|
|
4278
4282
|
}, (Res) => {
|
|
4279
4283
|
const Chunks = [];
|
|
4280
4284
|
Res.on("data", (Chunk) => {
|
|
@@ -4311,7 +4315,116 @@ async function HTTPSRequest(Url, Options) {
|
|
|
4311
4315
|
});
|
|
4312
4316
|
return HTTPSResponse;
|
|
4313
4317
|
}
|
|
4318
|
+
async function HTTPS2Request(Url, Options) {
|
|
4319
|
+
const DefaultOptions = {
|
|
4320
|
+
TLS: {
|
|
4321
|
+
IsHTTPSEnforced: true,
|
|
4322
|
+
MinTLSVersion: "TLSv1.3",
|
|
4323
|
+
MaxTLSVersion: "TLSv1.3",
|
|
4324
|
+
Ciphers: ["TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"],
|
|
4325
|
+
KeyExchanges: ["X25519MLKEM768", "X25519"]
|
|
4326
|
+
},
|
|
4327
|
+
HttpHeaders: {
|
|
4328
|
+
"User-Agent": `node/${Process.version} ${Process.platform} ${Process.arch} workspace/false`
|
|
4329
|
+
},
|
|
4330
|
+
HttpMethod: "GET"
|
|
4331
|
+
};
|
|
4332
|
+
const MergedOptions = { ...DefaultOptions, ...Options ?? {} };
|
|
4333
|
+
if (Url instanceof URL === false) {
|
|
4334
|
+
throw new TypeError("Url must be an instance of URL");
|
|
4335
|
+
}
|
|
4336
|
+
await strictObject({
|
|
4337
|
+
TLS: strictObject({
|
|
4338
|
+
IsHTTPSEnforced: boolean2().optional(),
|
|
4339
|
+
MinTLSVersion: _enum(["TLSv1.2", "TLSv1.3"]).optional(),
|
|
4340
|
+
MaxTLSVersion: _enum(["TLSv1.2", "TLSv1.3"]).optional(),
|
|
4341
|
+
Ciphers: array(string2().refine((Cipher) => TLS.getCiphers().map((C) => C.toLowerCase()).includes(Cipher.toLowerCase()))).optional(),
|
|
4342
|
+
KeyExchanges: array(string2()).optional()
|
|
4343
|
+
}).partial().optional(),
|
|
4344
|
+
HttpHeaders: record(string2(), string2()).optional(),
|
|
4345
|
+
ExpectedAs: _enum(["JSON", "String", "ArrayBuffer"]).optional(),
|
|
4346
|
+
HttpMethod: _enum(["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]).optional()
|
|
4347
|
+
}).parseAsync(Options ?? {});
|
|
4348
|
+
if (MergedOptions.TLS?.IsHTTPSEnforced && Url.protocol !== "https:") {
|
|
4349
|
+
throw new Error("HTTPS is enforced, but the URL protocol is not HTTPS");
|
|
4350
|
+
}
|
|
4351
|
+
const ExpectedAs = Options?.ExpectedAs ?? (Url.pathname.endsWith(".json") ? "JSON" : Url.pathname.endsWith(".txt") ? "String" : "ArrayBuffer");
|
|
4352
|
+
const HTTPSResponse = await new Promise((Resolve, Reject) => {
|
|
4353
|
+
const NormalizedHeaders = Object.fromEntries(Object.entries(MergedOptions.HttpHeaders ?? {}).map(([Key, Value]) => [Key.toLowerCase(), Value]));
|
|
4354
|
+
const HTTP2Session = HTTP2.connect(`https://${Url.hostname}${Url.port ? `:${Url.port}` : ""}`, {
|
|
4355
|
+
createConnection: () => TLS.connect({
|
|
4356
|
+
host: Url.hostname,
|
|
4357
|
+
port: Number(Url.port || 443),
|
|
4358
|
+
servername: Url.hostname,
|
|
4359
|
+
minVersion: MergedOptions.TLS?.MinTLSVersion,
|
|
4360
|
+
maxVersion: MergedOptions.TLS?.MaxTLSVersion,
|
|
4361
|
+
ciphers: MergedOptions.TLS?.Ciphers?.join(":"),
|
|
4362
|
+
ecdhCurve: MergedOptions.TLS?.KeyExchanges?.join(":"),
|
|
4363
|
+
ALPNProtocols: ["h2"]
|
|
4364
|
+
})
|
|
4365
|
+
});
|
|
4366
|
+
HTTP2Session.on("error", (Error2) => {
|
|
4367
|
+
Reject(Error2);
|
|
4368
|
+
});
|
|
4369
|
+
const RequestHeaders = {
|
|
4370
|
+
":method": MergedOptions.HttpMethod,
|
|
4371
|
+
":path": Url.pathname + Url.search,
|
|
4372
|
+
":scheme": "https",
|
|
4373
|
+
":authority": Url.host,
|
|
4374
|
+
...NormalizedHeaders
|
|
4375
|
+
};
|
|
4376
|
+
const Request = HTTP2Session.request(RequestHeaders);
|
|
4377
|
+
const Chunks = [];
|
|
4378
|
+
let StatusCode = 0;
|
|
4379
|
+
let ResponseHeaders = {};
|
|
4380
|
+
Request.on("response", (Headers) => {
|
|
4381
|
+
StatusCode = Number(Headers[":status"] ?? 0);
|
|
4382
|
+
ResponseHeaders = Object.fromEntries(Object.entries(Headers).filter(([Key]) => !Key.startsWith(":")).map(([Key, Value]) => {
|
|
4383
|
+
if (Array.isArray(Value)) {
|
|
4384
|
+
return [Key, Value.map((Item) => Item?.toString())];
|
|
4385
|
+
}
|
|
4386
|
+
return [Key, Value?.toString()];
|
|
4387
|
+
}));
|
|
4388
|
+
});
|
|
4389
|
+
Request.on("data", (Chunk) => {
|
|
4390
|
+
Chunks.push(Chunk.buffer.slice(Chunk.byteOffset, Chunk.byteOffset + Chunk.byteLength));
|
|
4391
|
+
});
|
|
4392
|
+
Request.on("end", () => {
|
|
4393
|
+
const BodyBuffer = ConcatArrayBuffers(Chunks);
|
|
4394
|
+
let Body;
|
|
4395
|
+
switch (ExpectedAs) {
|
|
4396
|
+
case "JSON":
|
|
4397
|
+
try {
|
|
4398
|
+
Body = JSON.parse(new TextDecoder("utf-8").decode(BodyBuffer));
|
|
4399
|
+
} catch (Error2) {
|
|
4400
|
+
HTTP2Session.close();
|
|
4401
|
+
return Reject(new Error2("Failed to parse JSON response body"));
|
|
4402
|
+
}
|
|
4403
|
+
break;
|
|
4404
|
+
case "String":
|
|
4405
|
+
Body = new TextDecoder("utf-8").decode(BodyBuffer);
|
|
4406
|
+
break;
|
|
4407
|
+
case "ArrayBuffer":
|
|
4408
|
+
Body = BodyBuffer;
|
|
4409
|
+
break;
|
|
4410
|
+
}
|
|
4411
|
+
HTTP2Session.close();
|
|
4412
|
+
Resolve({
|
|
4413
|
+
StatusCode,
|
|
4414
|
+
Headers: ResponseHeaders,
|
|
4415
|
+
Body
|
|
4416
|
+
});
|
|
4417
|
+
});
|
|
4418
|
+
Request.on("error", (Error2) => {
|
|
4419
|
+
HTTP2Session.close();
|
|
4420
|
+
Reject(Error2);
|
|
4421
|
+
});
|
|
4422
|
+
Request.end();
|
|
4423
|
+
});
|
|
4424
|
+
return HTTPSResponse;
|
|
4425
|
+
}
|
|
4314
4426
|
export {
|
|
4427
|
+
HTTPS2Request,
|
|
4315
4428
|
HTTPSRequest
|
|
4316
4429
|
};
|
|
4317
4430
|
//# sourceMappingURL=index.js.map
|