@opentunnel/client 0.1.1 → 0.2.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/package.json +3 -3
- package/src/effect/api.ts +9 -3
- package/src/effect/client.ts +11 -11
- package/src/effect/errors.ts +4 -4
- package/src/effect/tunnel.ts +11 -1
- package/src/promise/client.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opentunnel/client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "OpenTunnel SDK for Bun: expose local services through a blind TLS tunnel",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,8 +28,8 @@
|
|
|
28
28
|
"test": "bun test"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@opentunnel/protocol": "0.
|
|
32
|
-
"effect": "4.0.0-
|
|
31
|
+
"@opentunnel/protocol": "0.2.0",
|
|
32
|
+
"effect": "4.0.0-rc.112"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@peculiar/x509": "2.0.0",
|
package/src/effect/api.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { Effect, Layer,
|
|
1
|
+
import { Effect, Layer, Context } from "effect";
|
|
2
2
|
import { FetchHttpClient, HttpClient, HttpClientRequest } from "effect/unstable/http";
|
|
3
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
3
4
|
import { HttpApiClient } from "effect/unstable/httpapi";
|
|
4
5
|
import { Api } from "@opentunnel/protocol/api/api";
|
|
5
6
|
import { Tunnel } from "@opentunnel/protocol/tunnel";
|
|
6
7
|
|
|
8
|
+
/** How the SDK identifies itself to the server, on API calls and the bridge. */
|
|
9
|
+
export const USER_AGENT = `opentunnel-sdk/${pkg.version}`;
|
|
10
|
+
|
|
7
11
|
type Client = HttpApiClient.ForApi<typeof Api>;
|
|
8
12
|
|
|
9
13
|
interface OpenTunnelApi {
|
|
@@ -11,14 +15,16 @@ interface OpenTunnelApi {
|
|
|
11
15
|
readonly authorized: (token: Tunnel.Token) => Effect.Effect<Client>;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
export class OpenTunnelApiClient extends
|
|
18
|
+
export class OpenTunnelApiClient extends Context.Service<OpenTunnelApiClient, OpenTunnelApi>()(
|
|
15
19
|
"@opentunnel/client/OpenTunnelApiClient",
|
|
16
20
|
) {
|
|
17
21
|
static layer(options: { readonly api: URL | string }) {
|
|
18
22
|
return Layer.effect(
|
|
19
23
|
OpenTunnelApiClient,
|
|
20
24
|
Effect.gen(function* () {
|
|
21
|
-
const httpClient = yield* HttpClient.HttpClient
|
|
25
|
+
const httpClient = (yield* HttpClient.HttpClient).pipe(
|
|
26
|
+
HttpClient.mapRequest(HttpClientRequest.setHeader("user-agent", USER_AGENT)),
|
|
27
|
+
);
|
|
22
28
|
const client = yield* HttpApiClient.makeWith(Api, {
|
|
23
29
|
baseUrl: options.api,
|
|
24
30
|
httpClient,
|
package/src/effect/client.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Cause, Effect, Layer, Queue,
|
|
1
|
+
import { Cause, Effect, Layer, Queue, Context, Stream } from "effect";
|
|
2
2
|
import { CSR } from "@opentunnel/protocol/csr";
|
|
3
3
|
import { Tunnel } from "@opentunnel/protocol/tunnel";
|
|
4
4
|
import { OpenTunnelApiClient } from "./api.js";
|
|
@@ -33,7 +33,7 @@ export interface OpenTunnelClientOptions {
|
|
|
33
33
|
readonly storage?: Storage;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export class OpenTunnelClient extends
|
|
36
|
+
export class OpenTunnelClient extends Context.Service<
|
|
37
37
|
OpenTunnelClient,
|
|
38
38
|
OpenTunnelEffectClient
|
|
39
39
|
>()("@opentunnel/client/OpenTunnelClient") {
|
|
@@ -56,10 +56,10 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
56
56
|
readonly pending: OpenTunnelPendingIdentity;
|
|
57
57
|
readonly onProgress?: (stage: OpenTunnelProvisionStage) => void;
|
|
58
58
|
}) {
|
|
59
|
-
const authorized = yield* api.authorized(Tunnel.Token.
|
|
59
|
+
const authorized = yield* api.authorized(Tunnel.Token.make(options.pending.token));
|
|
60
60
|
yield* Effect.sync(() => options.onProgress?.("requesting-certificate"));
|
|
61
61
|
yield* authorized.tunnel["tunnel.bindCertificate"]({
|
|
62
|
-
params: { id: Tunnel.ID.
|
|
62
|
+
params: { id: Tunnel.ID.make(options.pending.id) },
|
|
63
63
|
payload: { csr: options.pending.csr as CSR.Raw },
|
|
64
64
|
}).pipe(
|
|
65
65
|
// A resumed provision may already have an issuance in flight.
|
|
@@ -71,7 +71,7 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
71
71
|
while (true) {
|
|
72
72
|
yield* Effect.sync(() => options.onProgress?.("waiting-certificate"));
|
|
73
73
|
const value = yield* authorized.tunnel["tunnel.getCertificate"]({
|
|
74
|
-
params: { id: Tunnel.ID.
|
|
74
|
+
params: { id: Tunnel.ID.make(options.pending.id) },
|
|
75
75
|
}).pipe(
|
|
76
76
|
Effect.mapError((cause) => clientError("Failed to read certificate", cause)),
|
|
77
77
|
);
|
|
@@ -176,9 +176,9 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
176
176
|
) {
|
|
177
177
|
const existing = yield* get(input);
|
|
178
178
|
if (!existing) return yield* create(input);
|
|
179
|
-
const authorized = yield* api.authorized(Tunnel.Token.
|
|
179
|
+
const authorized = yield* api.authorized(Tunnel.Token.make(existing.token));
|
|
180
180
|
const certificate = yield* authorized.tunnel["tunnel.getCertificate"]({
|
|
181
|
-
params: { id: Tunnel.ID.
|
|
181
|
+
params: { id: Tunnel.ID.make(existing.id) },
|
|
182
182
|
}).pipe(
|
|
183
183
|
Effect.mapError((cause) => clientError("Failed to read certificate", cause)),
|
|
184
184
|
);
|
|
@@ -197,9 +197,9 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
197
197
|
if (state.type !== "failed") return existing;
|
|
198
198
|
return yield* provision({
|
|
199
199
|
profile: profileName(input),
|
|
200
|
-
id: Tunnel.ID.
|
|
200
|
+
id: Tunnel.ID.make(existing.id),
|
|
201
201
|
hostname: existing.hostname,
|
|
202
|
-
token: Tunnel.Token.
|
|
202
|
+
token: Tunnel.Token.make(existing.token),
|
|
203
203
|
});
|
|
204
204
|
});
|
|
205
205
|
|
|
@@ -235,9 +235,9 @@ export class OpenTunnelClient extends ServiceMap.Service<
|
|
|
235
235
|
const profile = profileName(input);
|
|
236
236
|
const identity = yield* storage.load(profile);
|
|
237
237
|
if (!identity) return;
|
|
238
|
-
const authorized = yield* api.authorized(Tunnel.Token.
|
|
238
|
+
const authorized = yield* api.authorized(Tunnel.Token.make(identity.token));
|
|
239
239
|
yield* authorized.tunnel["tunnel.remove"]({
|
|
240
|
-
params: { id: Tunnel.ID.
|
|
240
|
+
params: { id: Tunnel.ID.make(identity.id) },
|
|
241
241
|
}).pipe(
|
|
242
242
|
Effect.mapError((cause) => clientError("Failed to remove tunnel", cause)),
|
|
243
243
|
);
|
package/src/effect/errors.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
|
|
3
|
-
export class OpenTunnelClientError extends Schema.
|
|
3
|
+
export class OpenTunnelClientError extends Schema.TaggedError<OpenTunnelClientError>()(
|
|
4
4
|
"OpenTunnelClientError",
|
|
5
|
-
{ message: Schema.String, cause: Schema.optional(Schema.Defect) },
|
|
5
|
+
{ message: Schema.String, cause: Schema.optional(Schema.Defect()) },
|
|
6
6
|
) {}
|
|
7
7
|
|
|
8
|
-
export class OpenTunnelStorageError extends Schema.
|
|
8
|
+
export class OpenTunnelStorageError extends Schema.TaggedError<OpenTunnelStorageError>()(
|
|
9
9
|
"OpenTunnelStorageError",
|
|
10
|
-
{ message: Schema.String, cause: Schema.optional(Schema.Defect) },
|
|
10
|
+
{ message: Schema.String, cause: Schema.optional(Schema.Defect()) },
|
|
11
11
|
) {}
|
|
12
12
|
|
|
13
13
|
export type OpenTunnelError = OpenTunnelClientError | OpenTunnelStorageError;
|
package/src/effect/tunnel.ts
CHANGED
|
@@ -4,6 +4,13 @@ import { Duplex } from "node:stream";
|
|
|
4
4
|
import * as Tls from "node:tls";
|
|
5
5
|
import { BridgeProtocol } from "@opentunnel/protocol/bridge-protocol";
|
|
6
6
|
import { Names } from "@opentunnel/protocol/names";
|
|
7
|
+
import { USER_AGENT } from "./api.js";
|
|
8
|
+
|
|
9
|
+
/** Bun's WebSocket also takes options with request headers, which the DOM typings don't know about. */
|
|
10
|
+
const BunWebSocket = WebSocket as unknown as new (
|
|
11
|
+
url: URL,
|
|
12
|
+
options: { readonly protocols: string[]; readonly headers: Record<string, string> },
|
|
13
|
+
) => WebSocket;
|
|
7
14
|
import type {
|
|
8
15
|
OpenTunnelClientEvent,
|
|
9
16
|
OpenTunnelIdentity,
|
|
@@ -308,7 +315,10 @@ class Session {
|
|
|
308
315
|
this.options.api,
|
|
309
316
|
);
|
|
310
317
|
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
311
|
-
const socket = new
|
|
318
|
+
const socket = new BunWebSocket(url, {
|
|
319
|
+
protocols: [BridgeProtocol.WEBSOCKET_SUBPROTOCOL],
|
|
320
|
+
headers: { "user-agent": USER_AGENT },
|
|
321
|
+
});
|
|
312
322
|
socket.binaryType = "arraybuffer";
|
|
313
323
|
this.socket = socket;
|
|
314
324
|
this.attachTimeout = setTimeout(
|
package/src/promise/client.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function create(options: OpenTunnelClientOptions = {}): OpenTunnelPromise
|
|
|
78
78
|
const runtime = ManagedRuntime.make(OpenTunnelClient.layer(effectOptions));
|
|
79
79
|
const withClient = <A, E>(
|
|
80
80
|
f: (client: OpenTunnelClient["Service"]) => Effect.Effect<A, E>,
|
|
81
|
-
) => runtime.runPromise(Effect.flatMap(OpenTunnelClient
|
|
81
|
+
) => runtime.runPromise(Effect.flatMap(OpenTunnelClient, f));
|
|
82
82
|
|
|
83
83
|
return {
|
|
84
84
|
profile: { list: () => withClient((client) => client.profile.list()) },
|
|
@@ -95,7 +95,7 @@ export function create(options: OpenTunnelClientOptions = {}): OpenTunnelPromise
|
|
|
95
95
|
const closeScope = () => runtime.runPromise(Scope.close(scope, Exit.void));
|
|
96
96
|
const connection = await runtime
|
|
97
97
|
.runPromise(
|
|
98
|
-
Effect.flatMap(OpenTunnelClient
|
|
98
|
+
Effect.flatMap(OpenTunnelClient, (client) => client.tunnel.connect(input)).pipe(
|
|
99
99
|
Effect.provideService(Scope.Scope, scope),
|
|
100
100
|
),
|
|
101
101
|
)
|