import * as Schema from "effect/Schema"; import { type ConnectionRegistration, ConnectionCredential, ConnectionProfile, } from "../connection/catalog.ts"; import { type ConnectionTarget, PersistedConnectionTarget } from "../connection/model.ts"; import * as TokenStore from "../authorization/tokenStore.ts"; export const StoredConnectionCredential = Schema.Struct({ connectionId: Schema.String, credential: ConnectionCredential, }); export type StoredConnectionCredential = typeof StoredConnectionCredential.Type; export const ConnectionCatalogDocument = Schema.Struct({ schemaVersion: Schema.Literal(1), targets: Schema.Array(PersistedConnectionTarget), profiles: Schema.Array(ConnectionProfile), credentials: Schema.Array(StoredConnectionCredential), remoteDpopTokens: Schema.Array(TokenStore.RemoteDpopAccessToken), }); export type ConnectionCatalogDocument = typeof ConnectionCatalogDocument.Type; export const EMPTY_CONNECTION_CATALOG_DOCUMENT: ConnectionCatalogDocument = Object.freeze({ schemaVersion: 1, targets: [], profiles: [], credentials: [], remoteDpopTokens: [], }); export function replaceCatalogValue( values: ReadonlyArray, key: (value: A) => string, next: A, ): ReadonlyArray { const nextKey = key(next); return [...values.filter((value) => key(value) !== nextKey), next]; } export function removeCatalogValue( values: ReadonlyArray, key: (value: A) => string, removedKey: string, ): ReadonlyArray { return values.filter((value) => key(value) !== removedKey); } function connectionIdOf(target: ConnectionTarget): string | null { switch (target._tag) { case "PrimaryConnectionTarget": case "RelayConnectionTarget": return null; case "BearerConnectionTarget": case "SshConnectionTarget": return target.connectionId; } } function removeConnectionMetadata( document: ConnectionCatalogDocument, target: ConnectionTarget, removeRemoteToken: boolean, ): ConnectionCatalogDocument { const connectionId = connectionIdOf(target); return { ...document, targets: removeCatalogValue( document.targets, (value) => value.environmentId, target.environmentId, ), profiles: connectionId === null ? document.profiles : removeCatalogValue(document.profiles, (value) => value.connectionId, connectionId), credentials: connectionId === null ? document.credentials : removeCatalogValue(document.credentials, (value) => value.connectionId, connectionId), remoteDpopTokens: removeRemoteToken ? removeCatalogValue( document.remoteDpopTokens, (value) => value.environmentId, target.environmentId, ) : document.remoteDpopTokens, }; } export function registerConnectionInCatalog( document: ConnectionCatalogDocument, registration: ConnectionRegistration, ): ConnectionCatalogDocument { const target = registration.target; const previous = document.targets.find( (candidate) => candidate.environmentId === target.environmentId, ); const cleaned = previous === undefined ? document : removeConnectionMetadata(document, previous, false); const next: ConnectionCatalogDocument = { ...cleaned, targets: replaceCatalogValue(cleaned.targets, (value) => value.environmentId, target), }; switch (registration._tag) { case "RelayConnectionRegistration": return next; case "BearerConnectionRegistration": return { ...next, profiles: replaceCatalogValue( next.profiles, (value) => value.connectionId, registration.profile, ), credentials: replaceCatalogValue(next.credentials, (value) => value.connectionId, { connectionId: registration.target.connectionId, credential: registration.credential, }), }; case "SshConnectionRegistration": return { ...next, profiles: replaceCatalogValue( next.profiles, (value) => value.connectionId, registration.profile, ), }; } } export function removeConnectionFromCatalog( document: ConnectionCatalogDocument, target: ConnectionTarget, ): ConnectionCatalogDocument { return removeConnectionMetadata(document, target, true); }