1import { DesktopSshEnvironmentTargetSchema, EnvironmentId } from "@t3tools/contracts";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4
5import {
6 BearerConnectionTarget,
7 PrimaryConnectionTarget,
8 RelayConnectionTarget,
9 SshConnectionTarget,
10 type ConnectionTarget,
11} from "./model.ts";
12
13const ConnectionProfileBase = {
14 connectionId: Schema.String,
15 environmentId: EnvironmentId,
16 label: Schema.String,
17};
18
19export class BearerConnectionProfile extends Schema.TaggedClass<BearerConnectionProfile>()(
20 "BearerConnectionProfile",
21 {
22 ...ConnectionProfileBase,
23 httpBaseUrl: Schema.String,
24 wsBaseUrl: Schema.String,
25 /**
26 * Set on a route the server reported while this client was connected,
27 * rather than one the user paired. Learned routes are replaced when the
28 * server reports a different address, for example after a DHCP change.
29 */
30 learned: Schema.optionalKey(Schema.Literal(true)),
31 /**
32 * "t3-connect" when the route authenticates with the environment's T3
33 * Connect credential instead of a stored bearer token.
34 */
35 authorization: Schema.optionalKey(Schema.Literals(["t3-connect", "hosted"])),
36 /**
37 * "tailscale" on a learned route the server found on its Tailscale
38 * interface. A bare 100.64.0.0/10 address could belong to any VPN.
39 */
40 network: Schema.optionalKey(Schema.Literal("tailscale")),
41 },
42) {}
43
44export class SshConnectionProfile extends Schema.TaggedClass<SshConnectionProfile>()(
45 "SshConnectionProfile",
46 {
47 ...ConnectionProfileBase,
48 target: DesktopSshEnvironmentTargetSchema,
49 },
50) {}
51
52export const ConnectionProfile = Schema.Union([BearerConnectionProfile, SshConnectionProfile]);
53export type ConnectionProfile = typeof ConnectionProfile.Type;
54
55/** One way to reach an environment: T3 Connect, a direct URL, or SSH. */
56export interface ConnectionRoute {
57 readonly target: ConnectionTarget;
58 readonly profile: Option.Option<ConnectionProfile>;
59}
60
61/**
62 * A saved environment. `target` and `profile` are its preferred route;
63 * `alternateRoutes` holds the others in preference order. Read them together
64 * with `connectionRoutes`.
65 */
66export interface ConnectionCatalogEntry {
67 readonly target: ConnectionTarget;
68 readonly profile: Option.Option<ConnectionProfile>;
69 readonly alternateRoutes?: ReadonlyArray<ConnectionRoute>;
70 /** False when the user switched the environment off: saved, but never connects. */
71 readonly enabled: boolean;
72 /** Discovery rejection stays visible while the saved connection is switched off. */
73 readonly unsupportedReason?: string;
74 /** The rejection came from an outdated host, which can still be updated remotely. */
75 readonly serverUpdateRequired?: boolean;
76}
77
78export class BearerConnectionCredential extends Schema.TaggedClass<BearerConnectionCredential>()(
79 "BearerConnectionCredential",
80 {
81 token: Schema.String,
82 },
83) {}
84
85export class HubConnectionCredential extends Schema.TaggedClass<HubConnectionCredential>()(
86 "HubConnectionCredential",
87 {
88 connectionId: Schema.String,
89 },
90) {}
91
92export class HostedConnectionCredential extends Schema.TaggedClass<HostedConnectionCredential>()(
93 "HostedConnectionCredential",
94 { serverUrl: Schema.String },
95) {}
96
97export const ConnectionCredential = Schema.Union([
98 BearerConnectionCredential,
99 HubConnectionCredential,
100 HostedConnectionCredential,
101]);
102export type ConnectionCredential = typeof ConnectionCredential.Type;
103
104export class PrimaryConnectionRegistration extends Schema.TaggedClass<PrimaryConnectionRegistration>()(
105 "PrimaryConnectionRegistration",
106 {
107 target: PrimaryConnectionTarget,
108 },
109) {}
110
111export class RelayConnectionRegistration extends Schema.TaggedClass<RelayConnectionRegistration>()(
112 "RelayConnectionRegistration",
113 {
114 target: RelayConnectionTarget,
115 },
116) {}
117
118export class BearerConnectionRegistration extends Schema.TaggedClass<BearerConnectionRegistration>()(
119 "BearerConnectionRegistration",
120 {
121 target: BearerConnectionTarget,
122 profile: BearerConnectionProfile,
123 credential: ConnectionCredential,
124 },
125) {}
126
127export class SshConnectionRegistration extends Schema.TaggedClass<SshConnectionRegistration>()(
128 "SshConnectionRegistration",
129 {
130 target: SshConnectionTarget,
131 profile: SshConnectionProfile,
132 },
133) {}
134
135export const ConnectionRegistration = Schema.Union([
136 RelayConnectionRegistration,
137 BearerConnectionRegistration,
138 SshConnectionRegistration,
139]);
140export type ConnectionRegistration = typeof ConnectionRegistration.Type;
141
142/**
143 * Platform-managed registrations are reconciled from the host (the desktop
144 * bootstrap IPC) rather than persisted by the user. They cover the primary
145 * local environment plus any additional desktop-local backends running
146 * alongside it (e.g. a parallel WSL backend). The primary stays on same-origin
147 * cookie auth (`PrimaryConnectionRegistration`); secondary local backends live
148 * on a separate loopback origin and authenticate with a bearer token minted
149 * from their bootstrap credential (`BearerConnectionRegistration`).
150 */
151export const PlatformConnectionRegistration = Schema.Union([
152 PrimaryConnectionRegistration,
153 BearerConnectionRegistration,
154]);
155export type PlatformConnectionRegistration = typeof PlatformConnectionRegistration.Type;
156
157export function connectionRegistrationCatalogEntry(
158 registration: ConnectionRegistration | PrimaryConnectionRegistration,
159): ConnectionCatalogEntry {
160 switch (registration._tag) {
161 case "PrimaryConnectionRegistration":
162 case "RelayConnectionRegistration":
163 return {
164 target: registration.target,
165 profile: Option.none(),
166 enabled: true,
167 };
168 case "BearerConnectionRegistration":
169 case "SshConnectionRegistration":
170 return {
171 target: registration.target,
172 profile: Option.some(registration.profile),
173 enabled: true,
174 };
175 }
176}