import type { DesktopSshEnvironmentTarget, EnvironmentId } from "@t3tools/contracts";
import { resolveRemotePairingTarget } from "@t3tools/shared/remote";
import * as Context from "effect/Context";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Schema from "effect/Schema";
import * as SubscriptionRef from "effect/SubscriptionRef";
import * as HttpClient from "effect/unstable/http/HttpClient";
import { bootstrapRemoteBearerSession } from "../authorization/remote.ts";
import { deriveWsBaseUrl, normalizeHttpBaseUrl } from "../environment/endpoint.ts";
import { fetchRemoteEnvironmentDescriptor } from "../environment/descriptor.ts";
import * as ClientCapabilities from "../platform/capabilities.ts";
import {
BearerConnectionCredential,
BearerConnectionProfile,
BearerConnectionRegistration,
type ConnectionCatalogEntry,
type ConnectionCredential,
SshConnectionProfile,
SshConnectionRegistration,
} from "./catalog.ts";
import * as ConnectionCredentialStore from "./credentialStore.ts";
import { mapRemoteEnvironmentError } from "./errors.ts";
import {
BearerConnectionTarget,
ConnectionBlockedError,
SshConnectionTarget,
type ConnectionAttemptError,
} from "./model.ts";
import * as Persistence from "../platform/persistence.ts";
import * as EnvironmentRegistry from "./registry.ts";
export interface PairingConnectionInput {
readonly pairingUrl?: string;
readonly host?: string;
readonly pairingCode?: string;
}
export interface SshConnectionInput {
readonly target: DesktopSshEnvironmentTarget;
readonly label?: string;
}
export interface BearerConnectionUpdateInput {
readonly environmentId: EnvironmentId;
readonly label: string;
readonly httpBaseUrl: string;
}
export class ConnectionOnboarding extends Context.Service<
ConnectionOnboarding,
{
readonly registerPairing: (
input: PairingConnectionInput,
) => Effect.Effect<
EnvironmentId,
ConnectionAttemptError | Persistence.ConnectionPersistenceError
>;
readonly registerSsh: (
input: SshConnectionInput,
) => Effect.Effect<
EnvironmentId,
ConnectionAttemptError | Persistence.ConnectionPersistenceError
>;
readonly updateBearer: (
input: BearerConnectionUpdateInput,
) => Effect.Effect<void, ConnectionAttemptError | Persistence.ConnectionPersistenceError>;
}
>()("@t3tools/client-runtime/connection/onboarding/ConnectionOnboarding") {}
const resolvePairingTarget = Effect.fn("clientRuntime.connection.onboarding.resolvePairingTarget")(
function* (input: PairingConnectionInput) {
return yield* Effect.try({
try: () => resolveRemotePairingTarget(input),
catch: (cause) =>
new ConnectionBlockedError({
reason: "configuration",
detail: cause instanceof Error ? cause.message : "The pairing details are invalid.",
}),
});
},
);
export const preparePairingRegistration = Effect.fn(
"clientRuntime.connection.onboarding.preparePairingRegistration",
)(function* (input: PairingConnectionInput) {
const target = yield* resolvePairingTarget(input);
const presentation = yield* ClientCapabilities.ClientPresentation;
const descriptor = yield* fetchRemoteEnvironmentDescriptor({
httpBaseUrl: target.httpBaseUrl,
}).pipe(Effect.mapError(mapRemoteEnvironmentError));
const access = yield* bootstrapRemoteBearerSession({
httpBaseUrl: target.httpBaseUrl,
credential: target.credential,
scopes: presentation.scopes,
clientMetadata: presentation.metadata,
}).pipe(Effect.mapError(mapRemoteEnvironmentError));
const connectionId = `bearer:${descriptor.environmentId}`;
return new BearerConnectionRegistration({
target: new BearerConnectionTarget({
environmentId: descriptor.environmentId,
label: descriptor.label,
connectionId,
}),
profile: new BearerConnectionProfile({
connectionId,
environmentId: descriptor.environmentId,
label: descriptor.label,
httpBaseUrl: target.httpBaseUrl,
wsBaseUrl: target.wsBaseUrl,
}),
credential: new BearerConnectionCredential({
token: access.access_token,
}),
});
});
export const registerPairingConnection = Effect.fn(
"clientRuntime.connection.onboarding.registerPairingConnection",
)(function* (input: PairingConnectionInput) {
const registration = yield* preparePairingRegistration(input);
const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
yield* registry.register(registration);
return registration.target.environmentId;
});
const isBearerCredential = Schema.is(BearerConnectionCredential);
const isBearerProfile = Schema.is(BearerConnectionProfile);
export const updateBearerConnection = Effect.fn(
"clientRuntime.connection.onboarding.updateBearerConnection",
)(function* (input: BearerConnectionUpdateInput) {
const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
const credentials = yield* ConnectionCredentialStore.ConnectionCredentialStore;
const entry = (yield* SubscriptionRef.get(registry.entries)).get(input.environmentId);
const credential =
entry?.target._tag === "BearerConnectionTarget"
? yield* credentials.get(entry.target.connectionId)
: Option.none();
const registration = yield* prepareBearerConnectionUpdate({
input,
entry: Option.fromUndefinedOr(entry),
credential,
});
yield* registry.register(registration);
});
export const prepareBearerConnectionUpdate = Effect.fn(
"clientRuntime.connection.onboarding.prepareBearerConnectionUpdate",
)(function* (options: {
readonly input: BearerConnectionUpdateInput;
readonly entry: Option.Option<ConnectionCatalogEntry>;
readonly credential: Option.Option<ConnectionCredential>;
}) {
const entry = Option.getOrNull(options.entry);
if (
entry === undefined ||
entry === null ||
entry.target._tag !== "BearerConnectionTarget" ||
Option.isNone(entry.profile) ||
!isBearerProfile(entry.profile.value)
) {
return yield* new ConnectionBlockedError({
reason: "configuration",
detail: "Only saved bearer environments can be edited.",
});
}
const credential = options.credential;
if (Option.isNone(credential) || !isBearerCredential(credential.value)) {
return yield* new ConnectionBlockedError({
reason: "authentication",
detail: "The saved bearer credential is unavailable.",
});
}
const label = options.input.label.trim();
if (label === "") {
return yield* new ConnectionBlockedError({
reason: "configuration",
detail: "Environment label cannot be empty.",
});
}
const httpBaseUrl = yield* Effect.try({
try: () => normalizeHttpBaseUrl(options.input.httpBaseUrl),
catch: (cause) =>
new ConnectionBlockedError({
reason: "configuration",
detail: cause instanceof Error ? cause.message : "The environment URL is invalid.",
}),
});
const connectionId = entry.target.connectionId;
return new BearerConnectionRegistration({
target: new BearerConnectionTarget({
environmentId: options.input.environmentId,
label,
connectionId,
}),
profile: new BearerConnectionProfile({
connectionId,
environmentId: options.input.environmentId,
label,
httpBaseUrl,
wsBaseUrl: deriveWsBaseUrl(httpBaseUrl),
}),
credential: credential.value,
});
});
export const prepareSshRegistration = Effect.fn(
"clientRuntime.connection.onboarding.prepareSshRegistration",
)(function* (input: SshConnectionInput) {
const gateway = yield* ClientCapabilities.SshEnvironmentGateway;
const provisioned = yield* gateway.provision(input.target);
const connectionId = `ssh:${provisioned.environmentId}`;
const label = input.label?.trim() || provisioned.label || provisioned.bootstrap.target.alias;
return new SshConnectionRegistration({
target: new SshConnectionTarget({
environmentId: provisioned.environmentId,
label,
connectionId,
}),
profile: new SshConnectionProfile({
connectionId,
environmentId: provisioned.environmentId,
label,
target: provisioned.bootstrap.target,
}),
});
});
export const registerSshConnection = Effect.fn(
"clientRuntime.connection.onboarding.registerSshConnection",
)(function* (input: SshConnectionInput) {
const registration = yield* prepareSshRegistration(input);
const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
yield* registry.register(registration);
return registration.target.environmentId;
});
export const make = Effect.gen(function* () {
const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
const presentation = yield* ClientCapabilities.ClientPresentation;
const httpClient = yield* HttpClient.HttpClient;
const ssh = yield* ClientCapabilities.SshEnvironmentGateway;
const credentials = yield* ConnectionCredentialStore.ConnectionCredentialStore;
return ConnectionOnboarding.of({
registerPairing: (input) =>
registerPairingConnection(input).pipe(
Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, registry),
Effect.provideService(ClientCapabilities.ClientPresentation, presentation),
Effect.provideService(HttpClient.HttpClient, httpClient),
),
registerSsh: (input) =>
registerSshConnection(input).pipe(
Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, registry),
Effect.provideService(ClientCapabilities.SshEnvironmentGateway, ssh),
),
updateBearer: (input) =>
updateBearerConnection(input).pipe(
Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, registry),
Effect.provideService(ConnectionCredentialStore.ConnectionCredentialStore, credentials),
),
});
});
export const layer = Layer.effect(ConnectionOnboarding, make);