@opentunnel/client 0.0.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.
@@ -0,0 +1,120 @@
1
+ import { Effect, Exit, ManagedRuntime, Scope, Stream } from "effect";
2
+ import {
3
+ OpenTunnelClient,
4
+ type OpenTunnelClientOptions as EffectClientOptions,
5
+ } from "../effect/client.js";
6
+ import type {
7
+ OpenTunnelClientEvent,
8
+ OpenTunnelConnectOptions,
9
+ OpenTunnelIdentity,
10
+ OpenTunnelPendingIdentity,
11
+ OpenTunnelProfileOptions,
12
+ OpenTunnelProvisionStage,
13
+ OpenTunnelRoutes,
14
+ OpenTunnelStatus,
15
+ OpenTunnelStoredTunnel,
16
+ } from "../effect/types.js";
17
+ import { toEffectStorage, type OpenTunnelStorage } from "./storage.js";
18
+
19
+ export interface OpenTunnelClientOptions {
20
+ readonly api?: URL | string;
21
+ readonly store?: OpenTunnelStorage;
22
+ }
23
+
24
+ export interface OpenTunnelConnection {
25
+ readonly tunnel: OpenTunnelIdentity;
26
+ readonly events: AsyncIterable<OpenTunnelClientEvent>;
27
+ readonly status: () => OpenTunnelStatus;
28
+ /**
29
+ * Replaces the routes. Changing only targets applies immediately; adding or
30
+ * removing names re-attaches the bridge.
31
+ */
32
+ readonly setRoutes: (routes: OpenTunnelRoutes) => Promise<void>;
33
+ /** Settles when the tunnel stops after a fatal error or is closed. */
34
+ readonly closed: Promise<void>;
35
+ readonly close: () => Promise<void>;
36
+ }
37
+
38
+ export interface OpenTunnelPromiseClient {
39
+ readonly profile: {
40
+ readonly list: () => Promise<ReadonlyArray<string>>;
41
+ };
42
+ readonly tunnel: {
43
+ readonly list: () => Promise<ReadonlyArray<OpenTunnelStoredTunnel>>;
44
+ readonly get: (options?: OpenTunnelProfileOptions) => Promise<OpenTunnelIdentity | undefined>;
45
+ readonly pending: (
46
+ options?: OpenTunnelProfileOptions,
47
+ ) => Promise<Pick<OpenTunnelPendingIdentity, "id" | "hostname"> | undefined>;
48
+ readonly resume: (
49
+ options?: OpenTunnelProfileOptions & {
50
+ readonly onProgress?: (stage: OpenTunnelProvisionStage) => void;
51
+ },
52
+ ) => Promise<OpenTunnelIdentity | undefined>;
53
+ readonly create: (
54
+ options?: OpenTunnelProfileOptions & {
55
+ readonly onProgress?: (stage: OpenTunnelProvisionStage) => void;
56
+ },
57
+ ) => Promise<OpenTunnelIdentity>;
58
+ readonly ensure: (
59
+ options?: OpenTunnelProfileOptions,
60
+ ) => Promise<OpenTunnelIdentity>;
61
+ readonly remove: (options?: OpenTunnelProfileOptions) => Promise<void>;
62
+ /**
63
+ * Starts forwarding routes for the profile's tunnel, creating it if
64
+ * needed. Resolves once the bridge first attaches; reconnects until closed.
65
+ */
66
+ readonly connect: (
67
+ options: OpenTunnelConnectOptions & { readonly signal?: AbortSignal },
68
+ ) => Promise<OpenTunnelConnection>;
69
+ };
70
+ readonly dispose: () => Promise<void>;
71
+ }
72
+
73
+ export function create(options: OpenTunnelClientOptions = {}): OpenTunnelPromiseClient {
74
+ const effectOptions: EffectClientOptions = {
75
+ api: options.api,
76
+ ...(options.store ? { storage: toEffectStorage(options.store) } : {}),
77
+ };
78
+ const runtime = ManagedRuntime.make(OpenTunnelClient.layer(effectOptions));
79
+ const withClient = <A, E>(
80
+ f: (client: OpenTunnelClient["Service"]) => Effect.Effect<A, E>,
81
+ ) => runtime.runPromise(Effect.flatMap(OpenTunnelClient.asEffect(), f));
82
+
83
+ return {
84
+ profile: { list: () => withClient((client) => client.profile.list()) },
85
+ tunnel: {
86
+ list: () => withClient((client) => client.tunnel.list()),
87
+ get: (input) => withClient((client) => client.tunnel.get(input)),
88
+ pending: (input) => withClient((client) => client.tunnel.pending(input)),
89
+ resume: (input) => withClient((client) => client.tunnel.resume(input)),
90
+ create: (input) => withClient((client) => client.tunnel.create(input)),
91
+ ensure: (input) => withClient((client) => client.tunnel.ensure(input)),
92
+ remove: (input) => withClient((client) => client.tunnel.remove(input)),
93
+ connect: async (input) => {
94
+ const scope = await runtime.runPromise(Scope.make());
95
+ const closeScope = () => runtime.runPromise(Scope.close(scope, Exit.void));
96
+ const connection = await runtime
97
+ .runPromise(
98
+ Effect.flatMap(OpenTunnelClient.asEffect(), (client) => client.tunnel.connect(input)).pipe(
99
+ Effect.provideService(Scope.Scope, scope),
100
+ ),
101
+ )
102
+ .catch(async (error) => {
103
+ await closeScope();
104
+ throw error;
105
+ });
106
+ const close = () => runtime.runPromise(connection.close).finally(closeScope);
107
+ input.signal?.addEventListener("abort", () => void close(), { once: true });
108
+ return {
109
+ tunnel: connection.tunnel,
110
+ events: Stream.toAsyncIterable(connection.events),
111
+ status: () => runtime.runSync(connection.status),
112
+ setRoutes: (routes) => runtime.runPromise(connection.setRoutes(routes)),
113
+ closed: runtime.runPromise(connection.closed).finally(closeScope),
114
+ close,
115
+ };
116
+ },
117
+ },
118
+ dispose: () => runtime.dispose(),
119
+ };
120
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./client.js";
2
+ export * from "./storage.js";
3
+ export { OpenTunnelAttachError } from "../effect/tunnel.js";
4
+ export type {
5
+ OpenTunnelClientEvent,
6
+ OpenTunnelConnectOptions,
7
+ OpenTunnelIdentity,
8
+ OpenTunnelPendingIdentity,
9
+ OpenTunnelProfileOptions,
10
+ OpenTunnelProvisionStage,
11
+ OpenTunnelRoutes,
12
+ OpenTunnelStatus,
13
+ OpenTunnelStoredTunnel,
14
+ } from "../effect/types.js";
@@ -0,0 +1,54 @@
1
+ import { Effect } from "effect";
2
+ import {
3
+ OpenTunnelStorage as EffectStorage,
4
+ type OpenTunnelStorage as EffectStorageType,
5
+ } from "../effect/storage.js";
6
+ import type {
7
+ OpenTunnelIdentity,
8
+ OpenTunnelPendingIdentity,
9
+ OpenTunnelStoredTunnel,
10
+ } from "../effect/types.js";
11
+
12
+ const EffectStorageSymbol = Symbol.for("@opentunnel/client/EffectStorage");
13
+
14
+ export interface OpenTunnelStorage {
15
+ readonly profiles: () => Promise<ReadonlyArray<string>>;
16
+ readonly load: (profile: string) => Promise<OpenTunnelIdentity | undefined>;
17
+ readonly save: (profile: string, tunnel: OpenTunnelIdentity) => Promise<void>;
18
+ readonly loadPending: (profile: string) => Promise<OpenTunnelPendingIdentity | undefined>;
19
+ readonly savePending: (profile: string, tunnel: OpenTunnelPendingIdentity) => Promise<void>;
20
+ readonly remove: (profile: string) => Promise<void>;
21
+ readonly list: () => Promise<ReadonlyArray<OpenTunnelStoredTunnel>>;
22
+ }
23
+
24
+ type WrappedStorage = OpenTunnelStorage & { readonly [EffectStorageSymbol]: EffectStorageType };
25
+
26
+ const wrap = (storage: EffectStorageType): WrappedStorage => ({
27
+ [EffectStorageSymbol]: storage,
28
+ profiles: () => Effect.runPromise(storage.profiles()),
29
+ load: (profile) => Effect.runPromise(storage.load(profile)),
30
+ save: (profile, tunnel) => Effect.runPromise(storage.save(profile, tunnel)),
31
+ loadPending: (profile) => Effect.runPromise(storage.loadPending(profile)),
32
+ savePending: (profile, tunnel) => Effect.runPromise(storage.savePending(profile, tunnel)),
33
+ remove: (profile) => Effect.runPromise(storage.remove(profile)),
34
+ list: () => Effect.runPromise(storage.list()),
35
+ });
36
+
37
+ export const OpenTunnelStorage = {
38
+ memory: (): OpenTunnelStorage => wrap(EffectStorage.memory()),
39
+ xdg: (options?: { readonly env?: NodeJS.ProcessEnv; readonly home?: string }): OpenTunnelStorage =>
40
+ wrap(EffectStorage.xdg(options)),
41
+ };
42
+
43
+ export function toEffectStorage(storage: OpenTunnelStorage): EffectStorageType {
44
+ if (EffectStorageSymbol in storage) return (storage as WrappedStorage)[EffectStorageSymbol];
45
+ return {
46
+ profiles: () => Effect.tryPromise(() => storage.profiles()),
47
+ load: (profile) => Effect.tryPromise(() => storage.load(profile)),
48
+ save: (profile, tunnel) => Effect.tryPromise(() => storage.save(profile, tunnel)),
49
+ loadPending: (profile) => Effect.tryPromise(() => storage.loadPending(profile)),
50
+ savePending: (profile, tunnel) => Effect.tryPromise(() => storage.savePending(profile, tunnel)),
51
+ remove: (profile) => Effect.tryPromise(() => storage.remove(profile)),
52
+ list: () => Effect.tryPromise(() => storage.list()),
53
+ };
54
+ }