@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opentunnel/client",
3
- "version": "0.1.1",
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.1.0",
32
- "effect": "4.0.0-beta.42"
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, ServiceMap } from "effect";
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 ServiceMap.Service<OpenTunnelApiClient, OpenTunnelApi>()(
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,
@@ -1,4 +1,4 @@
1
- import { Cause, Effect, Layer, Queue, ServiceMap, Stream } from "effect";
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 ServiceMap.Service<
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.makeUnsafe(options.pending.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.makeUnsafe(options.pending.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.makeUnsafe(options.pending.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.makeUnsafe(existing.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.makeUnsafe(existing.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.makeUnsafe(existing.id),
200
+ id: Tunnel.ID.make(existing.id),
201
201
  hostname: existing.hostname,
202
- token: Tunnel.Token.makeUnsafe(existing.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.makeUnsafe(identity.token));
238
+ const authorized = yield* api.authorized(Tunnel.Token.make(identity.token));
239
239
  yield* authorized.tunnel["tunnel.remove"]({
240
- params: { id: Tunnel.ID.makeUnsafe(identity.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
  );
@@ -1,13 +1,13 @@
1
1
  import { Schema } from "effect";
2
2
 
3
- export class OpenTunnelClientError extends Schema.TaggedErrorClass<OpenTunnelClientError>()(
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.TaggedErrorClass<OpenTunnelStorageError>()(
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;
@@ -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 WebSocket(url, BridgeProtocol.WEBSOCKET_SUBPROTOCOL);
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(
@@ -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.asEffect(), f));
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.asEffect(), (client) => client.tunnel.connect(input)).pipe(
98
+ Effect.flatMap(OpenTunnelClient, (client) => client.tunnel.connect(input)).pipe(
99
99
  Effect.provideService(Scope.Scope, scope),
100
100
  ),
101
101
  )