@opentunnel/client 0.1.0 → 0.1.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.
- package/README.md +5 -0
- package/package.json +7 -7
- package/src/effect/client.ts +3 -18
- package/src/effect/csr.ts +85 -0
package/README.md
CHANGED
|
@@ -87,6 +87,11 @@ import { create, OpenTunnelStorage } from "@opentunnel/client"
|
|
|
87
87
|
const client = create({ store: OpenTunnelStorage.memory() })
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
+
A memory store loses the tunnel's token when the process exits, and tunnels do
|
|
91
|
+
not expire, so a tunnel it created can no longer be deleted. Call
|
|
92
|
+
`client.tunnel.remove()` before exiting, or use the default store for tunnels
|
|
93
|
+
that should outlive the process.
|
|
94
|
+
|
|
90
95
|
## Backpressure
|
|
91
96
|
|
|
92
97
|
The SDK stops reading from a local socket while more than 1 MiB is queued on
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentunnel/client",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "OpenTunnel SDK for Bun: expose local services through a blind TLS tunnel",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,15 +28,15 @@
|
|
|
28
28
|
"test": "bun test"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@opentunnel/protocol": "0.
|
|
32
|
-
"
|
|
33
|
-
"effect": "4.0.0-beta.42",
|
|
34
|
-
"reflect-metadata": "0.2.2"
|
|
31
|
+
"@opentunnel/protocol": "0.1.0",
|
|
32
|
+
"effect": "4.0.0-beta.42"
|
|
35
33
|
},
|
|
36
34
|
"devDependencies": {
|
|
35
|
+
"@peculiar/x509": "2.0.0",
|
|
36
|
+
"@types/bun": "1.3.13",
|
|
37
37
|
"@types/node": "25.5.0",
|
|
38
38
|
"@typescript/native-preview": "7.0.0-dev.20260328.1",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
39
|
+
"reflect-metadata": "0.2.2",
|
|
40
|
+
"typescript": "6.0.2"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/effect/client.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
|
-
import "reflect-metadata";
|
|
2
1
|
import { Cause, Effect, Layer, Queue, ServiceMap, Stream } from "effect";
|
|
3
|
-
import {
|
|
4
|
-
Pkcs10CertificateRequestGenerator,
|
|
5
|
-
SubjectAlternativeNameExtension,
|
|
6
|
-
} from "@peculiar/x509";
|
|
7
2
|
import { CSR } from "@opentunnel/protocol/csr";
|
|
8
3
|
import { Tunnel } from "@opentunnel/protocol/tunnel";
|
|
9
4
|
import { OpenTunnelApiClient } from "./api.js";
|
|
5
|
+
import { certificateRequest } from "./csr.js";
|
|
10
6
|
import { OpenTunnelClientError } from "./errors.js";
|
|
11
7
|
import { OpenTunnelStorage, type OpenTunnelStorage as Storage } from "./storage.js";
|
|
12
8
|
import type {
|
|
@@ -122,18 +118,7 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
122
118
|
});
|
|
123
119
|
yield* Effect.sync(() => options.onProgress?.("generating-csr"));
|
|
124
120
|
const csr = yield* Effect.tryPromise({
|
|
125
|
-
try: () =>
|
|
126
|
-
Pkcs10CertificateRequestGenerator.create({
|
|
127
|
-
name: `CN=${options.hostname}`,
|
|
128
|
-
extensions: [
|
|
129
|
-
new SubjectAlternativeNameExtension([
|
|
130
|
-
{ type: "dns", value: options.hostname },
|
|
131
|
-
{ type: "dns", value: `*.${options.hostname}` },
|
|
132
|
-
]),
|
|
133
|
-
],
|
|
134
|
-
signingAlgorithm: { name: "ECDSA", hash: "SHA-256" },
|
|
135
|
-
keys,
|
|
136
|
-
}),
|
|
121
|
+
try: () => certificateRequest(options.hostname, keys),
|
|
137
122
|
catch: (cause) => clientError("Failed to generate certificate request", cause),
|
|
138
123
|
});
|
|
139
124
|
const exported = yield* Effect.tryPromise({
|
|
@@ -145,7 +130,7 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
145
130
|
hostname: options.hostname,
|
|
146
131
|
token: options.token,
|
|
147
132
|
privateKey: privateKeyPem(exported),
|
|
148
|
-
csr
|
|
133
|
+
csr,
|
|
149
134
|
};
|
|
150
135
|
yield* storage.savePending(options.profile, pending);
|
|
151
136
|
return yield* completePending({
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// A PKCS#10 certificate request for a tunnel: CN and SANs for the hostname and its wildcard, signed with the
|
|
2
|
+
// tunnel's P-256 key. Encoded here with WebCrypto rather than an X.509 library, so the SDK carries no ASN.1
|
|
3
|
+
// dependency whose schema registry can be split across duplicate installs in a consumer's tree.
|
|
4
|
+
|
|
5
|
+
const der = (tag: number, ...contents: Uint8Array[]) => {
|
|
6
|
+
const body = concat(contents);
|
|
7
|
+
const length =
|
|
8
|
+
body.length < 0x80
|
|
9
|
+
? [body.length]
|
|
10
|
+
: (() => {
|
|
11
|
+
const bytes: number[] = [];
|
|
12
|
+
for (let n = body.length; n > 0; n >>= 8) bytes.unshift(n & 0xff);
|
|
13
|
+
return [0x80 | bytes.length, ...bytes];
|
|
14
|
+
})();
|
|
15
|
+
return concat([new Uint8Array([tag, ...length]), body]);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const concat = (parts: readonly Uint8Array[]) => {
|
|
19
|
+
const out = new Uint8Array(parts.reduce((total, part) => total + part.length, 0));
|
|
20
|
+
let offset = 0;
|
|
21
|
+
for (const part of parts) {
|
|
22
|
+
out.set(part, offset);
|
|
23
|
+
offset += part.length;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const sequence = (...contents: Uint8Array[]) => der(0x30, ...contents);
|
|
29
|
+
const set = (...contents: Uint8Array[]) => der(0x31, ...contents);
|
|
30
|
+
const utf8 = (value: string) => der(0x0c, new TextEncoder().encode(value));
|
|
31
|
+
const dnsName = (value: string) => der(0x82, new TextEncoder().encode(value));
|
|
32
|
+
|
|
33
|
+
const oid = (dotted: string) => {
|
|
34
|
+
const [first, second, ...rest] = dotted.split(".").map(Number);
|
|
35
|
+
const bytes = [first! * 40 + second!];
|
|
36
|
+
for (const arc of rest) {
|
|
37
|
+
const encoded = [arc & 0x7f];
|
|
38
|
+
for (let n = arc >> 7; n > 0; n >>= 7) encoded.unshift((n & 0x7f) | 0x80);
|
|
39
|
+
bytes.push(...encoded);
|
|
40
|
+
}
|
|
41
|
+
return der(0x06, new Uint8Array(bytes));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** A DER INTEGER from unsigned big-endian bytes: leading zeros dropped, a zero prepended if the top bit is set. */
|
|
45
|
+
const integer = (bytes: Uint8Array) => {
|
|
46
|
+
let start = 0;
|
|
47
|
+
while (start < bytes.length - 1 && bytes[start] === 0) start++;
|
|
48
|
+
const trimmed = bytes.subarray(start);
|
|
49
|
+
return der(0x02, trimmed[0]! & 0x80 ? concat([new Uint8Array([0]), trimmed]) : trimmed);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const COMMON_NAME = "2.5.4.3";
|
|
53
|
+
const EXTENSION_REQUEST = "1.2.840.113549.1.9.14";
|
|
54
|
+
const SUBJECT_ALT_NAME = "2.5.29.17";
|
|
55
|
+
const ECDSA_WITH_SHA256 = "1.2.840.10045.4.3.2";
|
|
56
|
+
|
|
57
|
+
export async function certificateRequest(hostname: string, keys: CryptoKeyPair): Promise<string> {
|
|
58
|
+
const publicKey = new Uint8Array(await crypto.subtle.exportKey("spki", keys.publicKey));
|
|
59
|
+
const info = sequence(
|
|
60
|
+
der(0x02, new Uint8Array([0])),
|
|
61
|
+
sequence(set(sequence(oid(COMMON_NAME), utf8(hostname)))),
|
|
62
|
+
publicKey,
|
|
63
|
+
der(
|
|
64
|
+
0xa0,
|
|
65
|
+
sequence(
|
|
66
|
+
oid(EXTENSION_REQUEST),
|
|
67
|
+
set(
|
|
68
|
+
sequence(
|
|
69
|
+
sequence(oid(SUBJECT_ALT_NAME), der(0x04, sequence(dnsName(hostname), dnsName(`*.${hostname}`)))),
|
|
70
|
+
),
|
|
71
|
+
),
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
);
|
|
75
|
+
// WebCrypto signs ECDSA as r‖s; X.509 wants SEQUENCE { r INTEGER, s INTEGER }.
|
|
76
|
+
const raw = new Uint8Array(
|
|
77
|
+
await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, keys.privateKey, info),
|
|
78
|
+
);
|
|
79
|
+
const signature = sequence(integer(raw.subarray(0, 32)), integer(raw.subarray(32)));
|
|
80
|
+
const request = sequence(info, sequence(oid(ECDSA_WITH_SHA256)), der(0x03, new Uint8Array([0]), signature));
|
|
81
|
+
const base64 = Buffer.from(request)
|
|
82
|
+
.toString("base64")
|
|
83
|
+
.replace(/.{1,64}/g, "$&\n");
|
|
84
|
+
return `-----BEGIN CERTIFICATE REQUEST-----\n${base64}-----END CERTIFICATE REQUEST-----`;
|
|
85
|
+
}
|